@jsonforms/core 3.6.0 → 3.7.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -20,6 +20,6 @@ export declare const findAllRefs: (schema: JsonSchema, result?: ReferenceSchemaM
20
20
  * @param {JsonSchema} schema the root schema from which to start
21
21
  * @param {string} schemaPath the schema path to be resolved
22
22
  * @param {JsonSchema} rootSchema the actual root schema
23
- * @returns {JsonSchema} the resolved sub-schema
23
+ * @returns {JsonSchema} the resolved sub-schema or undefined
24
24
  */
25
- export declare const resolveSchema: (schema: JsonSchema, schemaPath: string, rootSchema: JsonSchema) => JsonSchema;
25
+ export declare const resolveSchema: (schema: JsonSchema, schemaPath: string, rootSchema: JsonSchema) => JsonSchema | undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsonforms/core",
3
- "version": "3.6.0",
3
+ "version": "3.7.0-alpha.0",
4
4
  "description": "Core module of JSON Forms",
5
5
  "repository": "https://github.com/eclipsesource/jsonforms",
6
6
  "bugs": "https://github.com/eclipsesource/jsonforms/issues",
@@ -44,8 +44,7 @@
44
44
  "ts"
45
45
  ],
46
46
  "require": [
47
- "./test-config/ts-node.config.js",
48
- "source-map-support/register.js"
47
+ "./test-config/ts-node.config.js"
49
48
  ]
50
49
  },
51
50
  "nyc": {
@@ -102,32 +102,89 @@ export const findAllRefs = (
102
102
  const invalidSegment = (pathSegment: string) =>
103
103
  pathSegment === '#' || pathSegment === undefined || pathSegment === '';
104
104
 
105
+ /**
106
+ * Map for tracking schema resolution to prevent infinite recursion.
107
+ * Key: schema object reference, Value: Set of paths being resolved from that schema
108
+ */
109
+ type ResolutionTrackingMap = Map<JsonSchema, Set<string>>;
110
+ interface ResolveContext {
111
+ rootSchema: JsonSchema;
112
+ resolutionMap: ResolutionTrackingMap;
113
+ }
114
+
105
115
  /**
106
116
  * Resolve the given schema path in order to obtain a subschema.
107
117
  * @param {JsonSchema} schema the root schema from which to start
108
118
  * @param {string} schemaPath the schema path to be resolved
109
119
  * @param {JsonSchema} rootSchema the actual root schema
110
- * @returns {JsonSchema} the resolved sub-schema
120
+ * @returns {JsonSchema} the resolved sub-schema or undefined
111
121
  */
112
122
  export const resolveSchema = (
113
123
  schema: JsonSchema,
114
124
  schemaPath: string,
115
125
  rootSchema: JsonSchema
116
- ): JsonSchema => {
117
- const segments = schemaPath?.split('/').map(decode);
118
- return resolveSchemaWithSegments(schema, segments, rootSchema);
126
+ ): JsonSchema | undefined => {
127
+ const result = doResolveSchema(schema, schemaPath, {
128
+ rootSchema,
129
+ resolutionMap: new Map(),
130
+ });
131
+ return result;
119
132
  };
120
133
 
121
- const resolveSchemaWithSegments = (
134
+ const doResolveSchema = (
122
135
  schema: JsonSchema,
123
- pathSegments: string[],
124
- rootSchema: JsonSchema
136
+ schemaPath: string,
137
+ ctx: ResolveContext
138
+ ): JsonSchema | undefined => {
139
+ let resolvedSchema: JsonSchema | undefined = undefined;
140
+ // If the schema has a $ref, we resolve it first before continuing.
141
+ if (schema && typeof schema.$ref === 'string') {
142
+ const baseSchema = resolvePath(ctx.rootSchema, schema.$ref, ctx);
143
+ if (baseSchema !== undefined) {
144
+ resolvedSchema = resolvePath(baseSchema, schemaPath, ctx);
145
+ }
146
+ }
147
+ // With later versions of JSON Schema, the $ref can also be used next to other properties,
148
+ // therefore we also try to resolve the path from the schema itself, even if it has a $ref.
149
+ if (resolvedSchema === undefined) {
150
+ resolvedSchema = resolvePath(schema, schemaPath, ctx);
151
+ }
152
+ return resolvedSchema;
153
+ };
154
+
155
+ const resolvePath = (
156
+ schema: JsonSchema,
157
+ schemaPath: string | undefined,
158
+ ctx: ResolveContext
125
159
  ): JsonSchema => {
126
- // use typeof because schema can by of any type - check singleSegmentResolveSchema below
127
- if (typeof schema?.$ref === 'string') {
128
- schema = resolveSchema(rootSchema, schema.$ref, rootSchema);
160
+ let visitedPaths: Set<string> | undefined = ctx.resolutionMap.get(schema);
161
+ if (!visitedPaths) {
162
+ visitedPaths = new Set();
163
+ ctx.resolutionMap.set(schema, visitedPaths);
164
+ }
165
+ if (visitedPaths.has(schemaPath)) {
166
+ // We were already asked to resolve this path from this schema, we must be stuck in a circular reference.
167
+ return undefined;
129
168
  }
130
169
 
170
+ visitedPaths.add(schemaPath);
171
+
172
+ const resolvedSchema = resolvePathSegmentsWithCombinatorFallback(
173
+ schema,
174
+ schemaPath?.split('/').map(decode),
175
+ ctx
176
+ );
177
+
178
+ visitedPaths.delete(schemaPath);
179
+
180
+ return resolvedSchema;
181
+ };
182
+
183
+ const resolvePathSegmentsWithCombinatorFallback = (
184
+ schema: JsonSchema,
185
+ pathSegments: string[],
186
+ ctx: ResolveContext
187
+ ): JsonSchema | undefined => {
131
188
  if (!pathSegments || pathSegments.length === 0) {
132
189
  return schema;
133
190
  }
@@ -136,49 +193,109 @@ const resolveSchemaWithSegments = (
136
193
  return undefined;
137
194
  }
138
195
 
139
- const [segment, ...remainingSegments] = pathSegments;
196
+ const resolvedSchema = resolvePathSegments(schema, pathSegments, ctx);
197
+ if (resolvedSchema !== undefined) {
198
+ return resolvedSchema;
199
+ }
140
200
 
141
- if (invalidSegment(segment)) {
142
- return resolveSchemaWithSegments(schema, remainingSegments, rootSchema);
201
+ // If the schema is not found, try combinators
202
+ const subSchemas = [].concat(
203
+ schema.oneOf ?? [],
204
+ schema.allOf ?? [],
205
+ schema.anyOf ?? [],
206
+ (schema as JsonSchema7).then ?? [],
207
+ (schema as JsonSchema7).else ?? []
208
+ );
209
+
210
+ for (const subSchema of subSchemas) {
211
+ let resolvedSubSchema = subSchema;
212
+ // check whether the subSchema is a $ref. If it is, resolve it first.
213
+ if (subSchema && typeof subSchema.$ref === 'string') {
214
+ resolvedSubSchema = doResolveSchema(ctx.rootSchema, subSchema.$ref, ctx);
215
+ }
216
+ const alternativeResolveResult = resolvePathSegmentsWithCombinatorFallback(
217
+ resolvedSubSchema,
218
+ pathSegments,
219
+ ctx
220
+ );
221
+ if (alternativeResolveResult) {
222
+ return alternativeResolveResult;
223
+ }
143
224
  }
144
225
 
145
- const singleSegmentResolveSchema = get(schema, segment);
226
+ return undefined;
227
+ };
146
228
 
147
- const resolvedSchema = resolveSchemaWithSegments(
148
- singleSegmentResolveSchema,
149
- remainingSegments,
150
- rootSchema
151
- );
152
- if (resolvedSchema) {
153
- return resolvedSchema;
229
+ const resolvePathSegments = (
230
+ schema: JsonSchema,
231
+ pathSegments: string[],
232
+ ctx: ResolveContext
233
+ ): JsonSchema | undefined => {
234
+ if (!pathSegments || pathSegments.length === 0) {
235
+ return schema;
154
236
  }
155
237
 
156
- if (segment === 'properties' || segment === 'items') {
157
- // Let's try to resolve the path, assuming oneOf/allOf/anyOf/then/else was omitted.
158
- // We only do this when traversing an object or array as we want to avoid
159
- // following a property which is named oneOf, allOf, anyOf, then or else.
160
- let alternativeResolveResult = undefined;
238
+ if (isEmpty(schema)) {
239
+ return undefined;
240
+ }
241
+
242
+ // perform a single step
243
+ const singleStepResult = resolveSingleStep(schema, pathSegments);
161
244
 
162
- const subSchemas = [].concat(
163
- schema.oneOf ?? [],
164
- schema.allOf ?? [],
165
- schema.anyOf ?? [],
166
- (schema as JsonSchema7).then ?? [],
167
- (schema as JsonSchema7).else ?? []
245
+ // Check whether resolving the next step was successful and returned a schema which has a $ref itself.
246
+ // In this case, we need to resolve the $ref first before continuing.
247
+ if (
248
+ singleStepResult.schema &&
249
+ typeof singleStepResult.schema.$ref === 'string'
250
+ ) {
251
+ singleStepResult.schema = doResolveSchema(
252
+ ctx.rootSchema,
253
+ singleStepResult.schema.$ref,
254
+ ctx
168
255
  );
256
+ }
169
257
 
170
- for (const subSchema of subSchemas) {
171
- alternativeResolveResult = resolveSchemaWithSegments(
172
- subSchema,
173
- [segment, ...remainingSegments],
174
- rootSchema
175
- );
176
- if (alternativeResolveResult) {
177
- break;
178
- }
179
- }
180
- return alternativeResolveResult;
258
+ return resolvePathSegmentsWithCombinatorFallback(
259
+ singleStepResult.schema,
260
+ singleStepResult.remainingPathSegments,
261
+ ctx
262
+ );
263
+ };
264
+
265
+ interface ResolveSingleStepResult {
266
+ schema: JsonSchema | undefined;
267
+ remainingPathSegments: string[];
268
+ resolvedSegment?: string;
269
+ }
270
+ /**
271
+ * Tries to resolve the next "step" of the pathSegments.
272
+ * Often this will be a single segment, but it might be multiples ones in case there are invalid segments.
273
+ */
274
+ const resolveSingleStep = (
275
+ schema: JsonSchema,
276
+ pathSegments: string[]
277
+ ): ResolveSingleStepResult => {
278
+ if (!pathSegments || pathSegments.length === 0) {
279
+ return { schema, remainingPathSegments: [] };
181
280
  }
182
281
 
183
- return undefined;
282
+ if (isEmpty(schema)) {
283
+ return {
284
+ schema: undefined,
285
+ remainingPathSegments: pathSegments,
286
+ };
287
+ }
288
+
289
+ const [segment, ...remainingPathSegments] = pathSegments;
290
+
291
+ if (invalidSegment(segment)) {
292
+ return resolveSingleStep(schema, remainingPathSegments);
293
+ }
294
+
295
+ const singleSegmentResolveSchema = get(schema, segment);
296
+ return {
297
+ schema: singleSegmentResolveSchema,
298
+ remainingPathSegments,
299
+ resolvedSegment: segment,
300
+ };
184
301
  };