Skip to content

Commit d65d7eb

Browse files
max-eliasdirix
andauthored
Escape special chars in JSON Pointers
Escape special characters in JSON Pointers to support forward slashes in attribute names. JSON Schema contains JSON Pointers in "$ref" attributes and UI Schemas in the "scope" attributes. As the forward slash is used a separator in in JSON Pointers, but attributes can contain forward slashes in their name, these pointers need to be encoded and decoded. This commit adds an "encode" utility to escape JSON Pointers. Once we convert to a data path or want to access the raw name, "decode" can be used. Co-authored-by: Stefan Dirix <sdirix@eclipsesource.com>
1 parent 63b40d1 commit d65d7eb

File tree

12 files changed

+77
-23
lines changed

12 files changed

+77
-23
lines changed

packages/angular-material/src/other/master-detail/master.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import {
3333
ArrayControlProps,
3434
ControlElement,
3535
createDefaultValue,
36+
decode,
3637
findUISchema,
3738
getFirstPrimitiveProp,
3839
JsonFormsState,
@@ -47,10 +48,12 @@ import {
4748
const keywords = ['#', 'properties', 'items'];
4849

4950
export const removeSchemaKeywords = (path: string) => {
50-
return path
51-
.split('/')
52-
.filter(s => !some(keywords, key => key === s))
53-
.join('.');
51+
return decode(
52+
path
53+
.split('/')
54+
.filter(s => !some(keywords, key => key === s))
55+
.join('.')
56+
);
5457
};
5558

5659
@Component({

packages/angular-material/src/other/table.renderer.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import {
3232
ArrayControlProps,
3333
ControlElement,
3434
deriveTypes,
35+
encode,
3536
isObjectArrayControl,
3637
isPrimitiveArrayControl,
3738
JsonSchema,
@@ -101,7 +102,8 @@ export class TableRenderer extends JsonFormsArrayControl {
101102
): ColumnDescription[] => {
102103
if (schema.type === 'object') {
103104
return this.getValidColumnProps(schema).map(prop => {
104-
const uischema = controlWithoutLabel(`#/properties/${prop}`);
105+
const encProp = encode(prop);
106+
const uischema = controlWithoutLabel(`#/properties/${encProp}`);
105107
if (!this.isEnabled()) {
106108
setReadonly(uischema);
107109
}

packages/core/src/generators/uischema.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ import {
3535
Layout,
3636
UISchemaElement
3737
} from '../models';
38-
import { deriveTypes, resolveSchema } from '../util';
38+
import { deriveTypes, encode, resolveSchema } from '../util';
3939

4040
/**
4141
* Creates a new ILayout.
@@ -162,7 +162,7 @@ const generateUISchema = (
162162
const nextRef: string = currentRef + '/properties';
163163
Object.keys(jsonSchema.properties).map(propName => {
164164
let value = jsonSchema.properties[propName];
165-
const ref = `${nextRef}/${propName}`;
165+
const ref = `${nextRef}/${encode(propName)}`;
166166
if (value.$ref !== undefined) {
167167
value = resolveSchema(rootSchema, value.$ref);
168168
}

packages/core/src/util/label.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import startCase from 'lodash/startCase';
2727

2828
import { ControlElement, JsonSchema, LabelDescription } from '../models';
29+
import { decode } from './path';
2930

3031
const deriveLabel = (
3132
controlElement: ControlElement,
@@ -36,8 +37,7 @@ const deriveLabel = (
3637
}
3738
if (typeof controlElement.scope === 'string') {
3839
const ref = controlElement.scope;
39-
const label = ref.substr(ref.lastIndexOf('/') + 1);
40-
40+
const label = decode(ref.substr(ref.lastIndexOf('/') + 1));
4141
return startCase(label);
4242
}
4343

packages/core/src/util/path.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,11 @@ export const toDataPathSegments = (schemaPath: string): string[] => {
6262
.replace(/oneOf\/[\d]\//g, '');
6363
const segments = s.split('/');
6464

65-
const startFromRoot = segments[0] === '#' || segments[0] === '';
65+
const decodedSegments = segments.map(decode);
66+
67+
const startFromRoot = decodedSegments[0] === '#' || decodedSegments[0] === '';
6668
const startIndex = startFromRoot ? 2 : 1;
67-
return range(startIndex, segments.length, 2).map(idx => segments[idx]);
69+
return range(startIndex, decodedSegments.length, 2).map(idx => decodedSegments[idx]);
6870
};
6971

7072
/**
@@ -88,3 +90,14 @@ export const composeWithUi = (scopableUi: Scopable, path: string): string => {
8890

8991
return isEmpty(segments) ? path : compose(path, segments.join('.'));
9092
};
93+
94+
/**
95+
* Encodes the given segment to be used as part of a JSON Pointer
96+
*
97+
* JSON Pointer has special meaning for "/" and "~", therefore these must be encoded
98+
*/
99+
export const encode = (segment: string) => segment?.replace(/~/g, '~0').replace(/\//g, '~1');
100+
/**
101+
* Decodes a given JSON Pointer segment to its "normal" representation
102+
*/
103+
export const decode = (pointerSegment: string) => pointerSegment?.replace(/~1/g, '/').replace(/~0/, '~');

packages/core/src/util/renderer.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ const isRequired = (
6565
): boolean => {
6666
const pathSegments = schemaPath.split('/');
6767
const lastSegment = pathSegments[pathSegments.length - 1];
68+
// Skip "properties", "items" etc. to resolve the parent
6869
const nextHigherSchemaSegments = pathSegments.slice(
6970
0,
7071
pathSegments.length - 2

packages/core/src/util/resolvers.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import isEmpty from 'lodash/isEmpty';
2727
import get from 'lodash/get';
2828
import { JsonSchema } from '../models';
29+
import { decode, encode } from './path';
2930

3031
/**
3132
* Map for storing refs and the respective schemas they are pointing to.
@@ -115,7 +116,7 @@ export const resolveSchema = (
115116
if (isEmpty(schema)) {
116117
return undefined;
117118
}
118-
const validPathSegments = schemaPath.split('/');
119+
const validPathSegments = schemaPath.split('/').map(decode);
119120
let resultSchema = schema;
120121
for (let i = 0; i < validPathSegments.length; i++) {
121122
let pathSegment = validPathSegments[i];
@@ -136,7 +137,7 @@ export const resolveSchema = (
136137
resultSchema?.anyOf ?? []
137138
);
138139
for (let item of schemas) {
139-
curSchema = resolveSchema(item, validPathSegments.slice(i).join('/'));
140+
curSchema = resolveSchema(item, validPathSegments.slice(i).map(encode).join('/'));
140141
if (curSchema) {
141142
break;
142143
}

packages/core/test/generators/uischema.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -556,3 +556,21 @@ test('generate control for nested oneOf', t => {
556556
};
557557
t.deepEqual(generateDefaultUISchema(schema), uischema);
558558
});
559+
560+
test('encode "/" in generated ui schema', t => {
561+
const schema: JsonSchema = {
562+
properties: {
563+
'some / initial / value': {
564+
type : 'integer'
565+
}
566+
}
567+
};
568+
const uischema: Layout = {
569+
type: 'VerticalLayout',
570+
elements: [{
571+
type: 'Control',
572+
scope: '#/properties/some ~1 initial ~1 value'
573+
}] as ControlElement[]
574+
};
575+
t.deepEqual(generateDefaultUISchema(schema), uischema);
576+
});

packages/core/test/util/path.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,9 @@ test('toDataPath use of encoded paths relative without /', t => {
8787
const fooBar = encodeURIComponent('foo/bar');
8888
t.is(toDataPath(`properties/${fooBar}`), `${fooBar}`);
8989
});
90+
test('toDataPath use of encoded special character in pathname', t => {
91+
t.is(toDataPath('properties/foo~0bar~1baz'), 'foo~bar/baz');
92+
});
9093
test('resolve instance', t => {
9194
const instance = { foo: 123 };
9295
const result = Resolve.data(instance, toDataPath('#/properties/foo'));

packages/core/test/util/resolvers.test.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,4 +63,17 @@ test('resolveSchema - resolves schema with any ', t => {
6363
t.deepEqual(resolveSchema(schema, '#/properties/description/properties/index'), {type: 'number'});
6464
t.deepEqual(resolveSchema(schema, '#/properties/description/properties/exist'), {type: 'boolean'});
6565
t.is(resolveSchema(schema, '#/properties/description/properties/notfound'), undefined);
66-
});
66+
});
67+
68+
test('resolveSchema - resolves schema with encoded characters', t => {
69+
const schema = {
70+
type: 'object',
71+
properties: {
72+
'foo / ~ bar': {
73+
type: 'integer'
74+
}
75+
}
76+
};
77+
t.deepEqual(resolveSchema(schema, '#/properties/foo ~1 ~0 bar'), {type: 'integer'});
78+
t.is(resolveSchema(schema, '#/properties/foo / bar'), undefined);
79+
});

0 commit comments

Comments
 (0)