Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implemented handling of complex field names. #1086

Merged
merged 6 commits into from Feb 12, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 2 additions & 1 deletion .eslintrc.json
Expand Up @@ -68,7 +68,8 @@
"prefer-rest-params": "off",
"react/jsx-pascal-case": "off",
"react/no-children-prop": "off",
"react/prop-types": "off"
"react/prop-types": "off",
"valid-jsdoc": "off"
},
"settings": {
"import/core-modules": ["meteor/aldeed:simple-schema", "meteor/check"],
Expand Down
31 changes: 28 additions & 3 deletions docs/api-helpers.md
Expand Up @@ -104,15 +104,40 @@ filterDOMProps.register('propA', 'propB');

## `joinName`

Safely joins partial field names. When the first param is null, returns an array of strings. Otherwise, returns a string. If you create a custom field with subfields, then it's better to use this helper than manually concatenating them.
Safely joins partial field names.
If you create a custom field with subfields, do use `joinName` instead of manually concatenating them.
It ensures that the name will be correctly escaped if needed.

```tsx
import { joinName } from 'uniforms';

joinName(null, 'a', 'b.c', 'd'); // ['a', 'b', 'c', 'd']
joinName('a', 'b.c', 'd'); // 'a.b.c.d'
joinName('array', 1, 'field'); // 'array.1.field'
joinName('object', 'nested.property'); // 'object.nested.property'
```

If the first argument is `null`, then it returns an array of escaped parts.

```tsx
import { joinName } from 'uniforms';

joinName(null, 'array', 1, 'field'); // ['array', '1', 'field']
joinName(null, 'object', 'nested.property'); // ['object', 'nested', 'property']
```

If the field name contains a dot (`.`) or a bracket (`[` or `]`), it has to be escaped with `["..."]`.
If any of these characters is not escaped, `joinName` will **not** throw an error but its behavior is not specified.
The escape of any other name part will be stripped.

```tsx
joinName(null, 'object["with.dot"].field'); // ['object', '["with.dot"]', 'field']
joinName('object["with.dot"].field'); // 'object["with.dot"].field'

joinName(null, 'this["is"].safe'); // ['this', 'is', 'safe']
joinName('this["is"].safe'); // 'this.is.safe'
```

For more examples check [`joinName` tests](https://github.com/vazco/uniforms/blob/master/packages/uniforms/__tests__/joinName.ts).

## `randomIds`

Generates random ID, based on given prefix. Use it, if you want to have random but deterministic strings. If no prefix is provided, a unique 'uniforms-X' prefix will be used generated.
Expand Down
Expand Up @@ -173,6 +173,39 @@ describe('JSONSchemaBridge', () => {
objectWithoutProperties: { type: 'object' },
withLabel: { type: 'string', uniforms: { label: 'Example' } },
forcedRequired: { type: 'string', uniforms: { required: true } },
'path.with.a.dot': {
type: 'object',
properties: {
'another.with.a.dot': {
type: 'string',
},
another: {
type: 'object',
properties: {
with: {
type: 'object',
properties: {
a: {
type: 'object',
properties: {
dot: {
type: 'number',
},
},
},
},
},
},
},
},
},
path: {
type: 'object',
properties: {
'with.a.dot': { type: 'number' },
'["with.a.dot"]': { type: 'string' },
},
},
},
required: ['dateOfBirth', 'nonObjectAnyOfRequired'],
};
Expand Down Expand Up @@ -477,6 +510,28 @@ describe('JSONSchemaBridge', () => {
});
});

it('returns correct definition (dots in name)', () => {
expect(bridge.getField('["path.with.a.dot"]')).toMatchObject({
type: 'object',
});
expect(
bridge.getField('["path.with.a.dot"]["another.with.a.dot"]'),
).toMatchObject({
type: 'string',
});
expect(
bridge.getField('["path.with.a.dot"].another.with.a.dot'),
).toMatchObject({
type: 'number',
});
expect(bridge.getField('path["with.a.dot"]')).toMatchObject({
type: 'number',
});
expect(bridge.getField('path["[\\"with.a.dot\\"]"]')).toMatchObject({
type: 'string',
});
});

it('returns correct definition ($ref pointing to $ref)', () => {
expect(bridge.getField('personalData.middleName')).toEqual({
type: 'string',
Expand Down Expand Up @@ -830,6 +885,8 @@ describe('JSONSchemaBridge', () => {
'objectWithoutProperties',
'withLabel',
'forcedRequired',
'["path.with.a.dot"]',
'path',
]);
});

Expand Down
Expand Up @@ -150,7 +150,7 @@ export default class JSONSchemaBridge extends Bridge {
fieldInvariant(name, !!definition);
} else if (definition.type === 'object') {
fieldInvariant(name, !!definition.properties);
definition = definition.properties[next];
definition = definition.properties[joinName.unescape(next)];
fieldInvariant(name, !!definition);
} else {
let nextFound = false;
Expand Down Expand Up @@ -296,7 +296,7 @@ export default class JSONSchemaBridge extends Bridge {
this._compiledSchema[name];

if (type === 'object' && properties) {
return Object.keys(properties);
return Object.keys(properties).map(joinName.escape);
}

return [];
Expand Down
130 changes: 103 additions & 27 deletions packages/uniforms/__tests__/joinName.ts
@@ -1,52 +1,128 @@
import { joinName } from 'uniforms';

function test(parts: unknown[], array: string[], string: string) {
// Serialization (join).
expect(joinName(...parts)).toBe(string);

// Deserialization (split).
expect(joinName(null, ...parts)).toEqual(array);

// Re-serialization (split + join).
expect(joinName(joinName(null, ...parts))).toEqual(string);

// Re-deserialization (join + split).
expect(joinName(null, joinName(...parts))).toEqual(array);
}

describe('joinName', () => {
it('is a function', () => {
expect(joinName).toBeInstanceOf(Function);
});

it('have raw mode', () => {
expect(joinName(null)).toEqual([]);
expect(joinName(null, 'a')).toEqual(['a']);
expect(joinName(null, 'a', 'b')).toEqual(['a', 'b']);
expect(joinName(null, 'a', 'b', null)).toEqual(['a', 'b']);
expect(joinName(null, 'a', 'b', null, 0)).toEqual(['a', 'b', '0']);
expect(joinName(null, 'a', 'b', null, 1)).toEqual(['a', 'b', '1']);
it('works with empty name', () => {
test([], [], '');
});

it('works with arrays', () => {
expect(joinName(['a'], 'b')).toBe('a.b');
expect(joinName('a', ['b'])).toBe('a.b');
test([['a']], ['a'], 'a');
test([[['a']]], ['a'], 'a');
test([[[['a']]]], ['a'], 'a');

test([[], 'a'], ['a'], 'a');
test(['a', []], ['a'], 'a');

test([['a'], 'b'], ['a', 'b'], 'a.b');
test(['a', ['b']], ['a', 'b'], 'a.b');

test([['a', 'b'], 'c'], ['a', 'b', 'c'], 'a.b.c');
test(['a', ['b', 'c']], ['a', 'b', 'c'], 'a.b.c');

test(['a', ['b', 'c'], 'd'], ['a', 'b', 'c', 'd'], 'a.b.c.d');
});

it('works with empty strings', () => {
expect(joinName('', 'a', 'b')).toBe('a.b');
expect(joinName('a', '', 'b')).toBe('a.b');
expect(joinName('a', 'b', '')).toBe('a.b');
test(['', 'a', 'b'], ['a', 'b'], 'a.b');
test(['a', '', 'b'], ['a', 'b'], 'a.b');
test(['a', 'b', ''], ['a', 'b'], 'a.b');
});

it('works with falsy values', () => {
expect(joinName('a', null, 'b')).toBe('a.b');
expect(joinName('a', false, 'b')).toBe('a.b');
expect(joinName('a', undefined, 'b')).toBe('a.b');
test(['a', null, 'b'], ['a', 'b'], 'a.b');
test(['a', false, 'b'], ['a', 'b'], 'a.b');
test(['a', undefined, 'b'], ['a', 'b'], 'a.b');
});

it('works with numbers', () => {
expect(joinName(0, 'a', 'b')).toBe('0.a.b');
expect(joinName('a', 0, 'b')).toBe('a.0.b');
expect(joinName('a', 'b', 0)).toBe('a.b.0');
expect(joinName(1, 'a', 'b')).toBe('1.a.b');
expect(joinName('a', 1, 'b')).toBe('a.1.b');
expect(joinName('a', 'b', 1)).toBe('a.b.1');
test([0, 'a', 'b'], ['0', 'a', 'b'], '0.a.b');
test(['a', 0, 'b'], ['a', '0', 'b'], 'a.0.b');
test(['a', 'b', 0], ['a', 'b', '0'], 'a.b.0');
test([1, 'a', 'b'], ['1', 'a', 'b'], '1.a.b');
test(['a', 1, 'b'], ['a', '1', 'b'], 'a.1.b');
test(['a', 'b', 1], ['a', 'b', '1'], 'a.b.1');
});

it('works with partials', () => {
expect(joinName('a', 'b.c.d')).toBe('a.b.c.d');
expect(joinName('a.b', 'c.d')).toBe('a.b.c.d');
expect(joinName('a.b.c', 'd')).toBe('a.b.c.d');
test(['a', 'b.c.d'], ['a', 'b', 'c', 'd'], 'a.b.c.d');
test(['a.b', 'c.d'], ['a', 'b', 'c', 'd'], 'a.b.c.d');
test(['a.b.c', 'd'], ['a', 'b', 'c', 'd'], 'a.b.c.d');
});

it('works with subscripts', () => {
test(['a["b"]'], ['a', 'b'], 'a.b');
test(['a["b"].c'], ['a', 'b', 'c'], 'a.b.c');
test(['a["b"].c["d"]'], ['a', 'b', 'c', 'd'], 'a.b.c.d');
test(['a["b"]["c.d"]'], ['a', 'b', '["c.d"]'], 'a.b["c.d"]');
test(['a["b"]["c.d"].e'], ['a', 'b', '["c.d"]', 'e'], 'a.b["c.d"].e');
test(['a["b"]["c.d"]["e"]'], ['a', 'b', '["c.d"]', 'e'], 'a.b["c.d"].e');
test(['a["b"].["c.d"]'], ['a', 'b', '["c.d"]'], 'a.b["c.d"]');
test(['a["b"].["c.d"].e'], ['a', 'b', '["c.d"]', 'e'], 'a.b["c.d"].e');
test(['a["b"].["c.d"]["e"]'], ['a', 'b', '["c.d"]', 'e'], 'a.b["c.d"].e');

test(['["a"]'], ['a'], 'a');
test(['["a"].b'], ['a', 'b'], 'a.b');
test(['["a"]["b.c"]'], ['a', '["b.c"]'], 'a["b.c"]');
test(['["a"]["b.c"].d'], ['a', '["b.c"]', 'd'], 'a["b.c"].d');
test(['["a"]["b.c"]["d"]'], ['a', '["b.c"]', 'd'], 'a["b.c"].d');
test(['["a"].["b.c"]'], ['a', '["b.c"]'], 'a["b.c"]');
test(['["a"].["b.c"].d'], ['a', '["b.c"]', 'd'], 'a["b.c"].d');
test(['["a"].["b.c"]["d"]'], ['a', '["b.c"]', 'd'], 'a["b.c"].d');

test(['[""]'], ['[""]'], '[""]');
test(['["."]'], ['["."]'], '["."]');
test(['[".."]'], ['[".."]'], '[".."]');
test(['["..."]'], ['["..."]'], '["..."]');
test(['["[\'\']"]'], ['["[\'\']"]'], '["[\'\']"]');
test(['["[\\"\\"]"]'], ['["[\\"\\"]"]'], '["[\\"\\"]"]');
});

it('handles incorrect cases _somehow_', () => {
// Boolean `true`.
test([true], ['true'], 'true');
test([true, 'a'], ['true', 'a'], 'true.a');
test(['a', true], ['a', 'true'], 'a.true');

// Dots before subscripts.
test(['a["b"].c.["d"]'], ['a', 'b', 'c', 'd'], 'a.b.c.d');
test(['a.["b"].c["d"]'], ['a', 'b', 'c', 'd'], 'a.b.c.d');
test(['a.["b"].c.["d"]'], ['a', 'b', 'c', 'd'], 'a.b.c.d');

// Only dots.
test(['.'], ['["."]'], '["."]');
test(['..'], ['[".."]'], '[".."]');
test(['...'], ['["..."]'], '["..."]');

// Leading and trailing dots.
test(['a.'], ['["a."]'], '["a."]');
test(['.a'], ['[""]', 'a'], '[""].a');
test(['["a"].'], ['a'], 'a');
test(['.["a"]'], ['a'], 'a');

expect(joinName(null, 'a', 'b.c.d')).toEqual(['a', 'b', 'c', 'd']);
expect(joinName(null, 'a.b', 'c.d')).toEqual(['a', 'b', 'c', 'd']);
expect(joinName(null, 'a.b.c', 'd')).toEqual(['a', 'b', 'c', 'd']);
// Unescaped brackets.
test(['['], ['["["]'], '["["]');
test(["['"], ['["[\'"]'], '["[\'"]');
test(["[''"], ['["[\'\'"]'], '["[\'\'"]');
test(["['']"], ['["[\'\']"]'], '["[\'\']"]');
test(['["'], ['["[\\""]'], '["[\\""]');
test(['[""'], ['["[\\"\\""]'], '["[\\"\\""]');
});
});