Skip to content

Commit

Permalink
Merge pull request #234 from jmeas/support-dots-in-keys
Browse files Browse the repository at this point in the history
Support bracket notation to support dots in keys
  • Loading branch information
jamesplease authored Oct 20, 2017
2 parents 9107edc + 417ea16 commit e5d006f
Show file tree
Hide file tree
Showing 10 changed files with 229 additions and 13 deletions.
2 changes: 1 addition & 1 deletion INDEX.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ import store from './store';
const state = store.getState();
// The second argument to this method is a path into the state tree. This method
// protects you from needing to check for undefined values.
const readStatus = getStatus(store, 'books.meta.24.readStatus');
const readStatus = getStatus(store, 'books.meta[24].readStatus');

if (readStatus.pending) {
console.log('The request is in flight.');
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ import store from './store';
const state = store.getState();
// The second argument to this method is a path into the state tree. This method
// protects you from needing to check for undefined values.
const readStatus = getStatus(store, 'books.meta.24.readStatus');
const readStatus = getStatus(store, 'books.meta[24].readStatus');

if (readStatus.pending) {
console.log('The request is in flight.');
Expand Down
4 changes: 2 additions & 2 deletions docs/api-reference/get-status.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ import { getStatus } from 'redux-resource';
import store from './store';
const state = store.getState();
const bookDeleteStatus = getStatus(state, 'books.meta.23.deleteStatus');
const bookDeleteStatus = getStatus(state, 'books.meta[23].deleteStatus');
```

In this example, we pass two locations:
Expand All @@ -79,7 +79,7 @@ const state = store.getState();
const bookReadStatus = getStatus(
state,
[
'articles.meta.23.readStatus',
'articles.meta[23].readStatus',
'comments.requests.detailsRead.status'
],
true
Expand Down
2 changes: 1 addition & 1 deletion docs/extras/redux-resource-prop-types.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ MyComponent.propTypes = {

mapStateToProps(state) {
return {
bookReadStatus: getStatus(state, 'books.meta.23.readStatus');
bookReadStatus: getStatus(state, 'books.meta[23].readStatus');
};
}
```
2 changes: 1 addition & 1 deletion docs/guides/request-statuses.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export class BookDetails extends Component {
function mapStateToProps(state, props) {
// A user can pass a `bookId` into this Component to view the book's data
const bookId = props.bookId;
const readStatus = getStatus(state, `books.meta.${bookId}.readStatus`, true);
const readStatus = getStatus(state, `books.meta[${bookId}].readStatus`, true);
const book = state.books.resources[book.id];

return {
Expand Down
8 changes: 4 additions & 4 deletions docs/recipes/user-feedback.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ this might look like:
```jsx
render() {
const { state } = this.props;
const readStatus = getStatus(state, 'books.meta.24.readStatus');
const readStatus = getStatus(state, 'books.meta[24].readStatus');

return (
<div>
Expand Down Expand Up @@ -77,7 +77,7 @@ If you're using React, an example code snippet of disabling a button is:
```jsx
render() {
const { state } = this.props;
const readStatus = getStatus(state, 'books.meta.24.readStatus');
const readStatus = getStatus(state, 'books.meta[24].readStatus');

return (
<div>
Expand Down Expand Up @@ -111,7 +111,7 @@ React, a typical error message might look like the following:
```jsx
render() {
const { state } = this.props;
const readStatus = getStatus(state, 'books.meta.24.readStatus');
const readStatus = getStatus(state, 'books.meta[24].readStatus');

return (
<div>
Expand Down Expand Up @@ -186,7 +186,7 @@ using React. For instance:
```jsx
render() {
const { state } = this.props;
const updateState = getStatus(state, 'books.meta.24.updateStatus');
const updateState = getStatus(state, 'books.meta[24].updateStatus');

return (
<div>
Expand Down
2 changes: 1 addition & 1 deletion examples/read-resource/src/components/Book.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { slicePropType } from 'redux-resource-prop-types';
class Book extends Component {
render() {
const { state, readBook, bookId } = this.props;
const readStatus = getStatus(state, 'meta.24.readStatus');
const readStatus = getStatus(state, 'meta[24].readStatus');
const book = state.resources[bookId];

return (
Expand Down
59 changes: 59 additions & 0 deletions packages/redux-resource/src/utils/get-path-parts.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Given a path, such as "meta[24].readStatus", this will return an array of "path parts." In
// that example, the path parts would be ['meta', '24', 'readStatus'].
// This method supports dot notation as well as bracket notation.
export default function getPathParts(path) {
const parts = [];
let i = 0;
let nextDot, nextOpenBracket, openQuote, nextCloseBracket;

while (i < path.length) {
nextDot = path.indexOf('.', i);
nextOpenBracket = path.indexOf('[', i);

// When there are no dots nor opening brackets ahead of the
// current index, then we've reached the final path part.
if (nextDot === -1 && nextOpenBracket === -1) {
parts.push(path.slice(i, path.length));
i = path.length;
}

// This handles dots. When there are no more open brackets, or the next dot is before
// the next open bracket, then we simply add the path part.
else if (nextOpenBracket === -1 || (nextDot !== -1 && nextDot < nextOpenBracket)) {
parts.push(path.slice(i, nextDot));
i = nextDot + 1;
}

// If neither of the above two conditions are met, then we're dealing with a bracket.
else {
if (nextOpenBracket > i) {
parts.push(path.slice(i, nextOpenBracket));
i = nextOpenBracket;
}

openQuote = path.slice(nextOpenBracket + 1, nextOpenBracket + 2);

// This handles the situation when we do not have quotes. For instance, [24] or [asdf]
if (openQuote !== '"' && openQuote !== "'") {
nextCloseBracket = path.indexOf(']', nextOpenBracket);
if (nextCloseBracket === -1) {
nextCloseBracket = path.length;
}
parts.push(path.slice(i + 1, nextCloseBracket));
i = (path.slice(nextCloseBracket + 1, nextCloseBracket + 2) === '.') ? nextCloseBracket + 2 : nextCloseBracket + 1;
}

// This handles brackets that are wrapped in quotes. For instance, ["hello"] or ['24']
else {
nextCloseBracket = path.indexOf(`${openQuote}]`, nextOpenBracket);
if (nextCloseBracket === -1) {
nextCloseBracket = path.length;
}
parts.push(path.slice(i + 2, nextCloseBracket));
i = (path.slice(nextCloseBracket + 2, nextCloseBracket + 3) === '.') ? nextCloseBracket + 3 : nextCloseBracket + 2;
}
}
}

return parts;
}
3 changes: 2 additions & 1 deletion packages/redux-resource/src/utils/get-status.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import requestStatuses from './request-statuses';
import getPathParts from './get-path-parts';
import warning from './warning';

function getSingleStatus(state, statusLocation, treatNullAsPending) {
const splitPath = statusLocation.split('.');
const splitPath = getPathParts(statusLocation);

let status;
let currentVal = state;
Expand Down
158 changes: 157 additions & 1 deletion packages/redux-resource/test/unit/utils/get-status.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,21 @@ describe('getStatus', function() {
updateStatus: requestStatuses.FAILED,
}
},
},
things: {
'pasta.is.tasty': {
readStatus: requestStatuses.SUCCEEDED
},
24: {
updateStatus: requestStatuses.FAILED
},
100: requestStatuses.PENDING,
'\\\'': {
deleteStatus: requestStatuses.PENDING
},
'[\'a\']': {
'[\\"a\\"]': requestStatuses.FAILED
}
}
};
});
Expand Down Expand Up @@ -65,7 +80,7 @@ describe('getStatus', function() {
});

describe('singular', () => {
it('should return a meta that exists', () => {
it('should return a request status that exists', () => {
expect(getStatus(this.state, 'sandwiches.meta.102.readStatus')).to.deep.equal({
null: false,
pending: false,
Expand All @@ -75,6 +90,16 @@ describe('getStatus', function() {
expect(console.error.callCount).to.equal(0);
});

it('should return a request status that exists; bracket syntax', () => {
expect(getStatus(this.state, 'sandwiches.meta[102].readStatus')).to.deep.equal({
null: false,
pending: false,
failed: false,
succeeded: true
});
expect(console.error.callCount).to.equal(0);
});

it('should return a request status that exists', () => {
expect(getStatus(this.state, 'books.requests.dashboardSearch.status')).to.deep.equal({
null: false,
Expand All @@ -95,6 +120,16 @@ describe('getStatus', function() {
expect(console.error.callCount).to.equal(0);
});

it('should return a meta that exists and is null; bracket syntax', () => {
expect(getStatus(this.state, 'sandwiches.meta[100].readStatus')).to.deep.equal({
null: true,
pending: false,
failed: false,
succeeded: false
});
expect(console.error.callCount).to.equal(0);
});

it('should return a meta that exists and is null with `treatNullAsPending` set to true', () => {
expect(getStatus(this.state, 'sandwiches.meta.100.readStatus', true)).to.deep.equal({
null: false,
Expand All @@ -105,6 +140,16 @@ describe('getStatus', function() {
expect(console.error.callCount).to.equal(0);
});

it('should return a meta that exists and is null with `treatNullAsPending` set to true; bracket syntax', () => {
expect(getStatus(this.state, 'sandwiches.meta[100].readStatus', true)).to.deep.equal({
null: false,
pending: true,
failed: false,
succeeded: false
});
expect(console.error.callCount).to.equal(0);
});

it('should return a meta that exists and is succeeded with `treatNullAsPending` set to true', () => {
expect(getStatus(this.state, 'sandwiches.meta.102.readStatus', true)).to.deep.equal({
null: false,
Expand All @@ -115,6 +160,16 @@ describe('getStatus', function() {
expect(console.error.callCount).to.equal(0);
});

it('should return a meta that exists and is succeeded with `treatNullAsPending` set to true; bracket syntax', () => {
expect(getStatus(this.state, 'sandwiches.meta[102].readStatus', true)).to.deep.equal({
null: false,
pending: false,
failed: false,
succeeded: true
});
expect(console.error.callCount).to.equal(0);
});

it('should return a meta that does not exist', () => {
expect(getStatus(this.state, 'books.meta.10.updateStatus')).to.deep.equal({
null: true,
Expand All @@ -134,6 +189,88 @@ describe('getStatus', function() {
});
expect(console.error.callCount).to.equal(0);
});

it('should support bracket syntax for keys with periods in them, double quotes', () => {
expect(getStatus(this.state, 'things["pasta.is.tasty"].readStatus')).to.deep.equal({
null: false,
pending: false,
failed: false,
succeeded: true
});
expect(console.error.callCount).to.equal(0);
});

it('should support bracket syntax for keys with periods in them, single quotes', () => {
expect(getStatus(this.state, "things['pasta.is.tasty'].readStatus")).to.deep.equal({
null: false,
pending: false,
failed: false,
succeeded: true
});
expect(console.error.callCount).to.equal(0);
});

it('should support bracket syntax for numbers', () => {
expect(getStatus(this.state, 'things[24].updateStatus')).to.deep.equal({
null: false,
pending: false,
failed: true,
succeeded: false
});
expect(console.error.callCount).to.equal(0);
});

it('should support bracket syntax, ending on a bracket', () => {
expect(getStatus(this.state, 'things[100]')).to.deep.equal({
null: false,
pending: true,
failed: false,
succeeded: false
});
expect(console.error.callCount).to.equal(0);
});

it('should support bracket syntax, ending on a bracket, but missing ending bracket', () => {
expect(getStatus(this.state, 'things[100')).to.deep.equal({
null: false,
pending: true,
failed: false,
succeeded: false
});
expect(console.error.callCount).to.equal(0);
});

it('should support bracket syntax, ending on a bracket, but missing ending bracket with quotes', () => {
expect(getStatus(this.state, 'things["100')).to.deep.equal({
null: false,
pending: true,
failed: false,
succeeded: false
});
expect(console.error.callCount).to.equal(0);
});

it('should support strings with escaped quotes', () => {
// eslint-disable-next-line
expect(getStatus(this.state, "things[\'\\\'\'].deleteStatus")).to.deep.equal({
null: false,
pending: true,
failed: false,
succeeded: false
});
expect(console.error.callCount).to.equal(0);
});

it('should support strings with escaped quotes', () => {
// eslint-disable-next-line
expect(getStatus(this.state, 'things["[\'a\']"][\'[\\"a\\"]\']')).to.deep.equal({
null: false,
pending: false,
failed: true,
succeeded: false
});
expect(console.error.callCount).to.equal(0);
});
});

describe('plural', () => {
Expand Down Expand Up @@ -216,5 +353,24 @@ describe('getStatus', function() {
});
expect(console.error.callCount).to.equal(0);
});

it('should fail when one is failed; bracket syntax', () => {
const result = getStatus(
this.state,
[
'sandwiches.meta[102].readStatus',
'books.requests.dashboardSearch.status',
'sandwiches.meta[102].updateStatus'
]
);

expect(result).to.deep.equal({
null: false,
pending: false,
failed: true,
succeeded: false
});
expect(console.error.callCount).to.equal(0);
});
});
});

0 comments on commit e5d006f

Please sign in to comment.