diff --git a/packages/core/src/util/path.ts b/packages/core/src/util/path.ts index 401cfd685..b62ed6afc 100644 --- a/packages/core/src/util/path.ts +++ b/packages/core/src/util/path.ts @@ -57,9 +57,8 @@ export { compose as composePaths }; */ export const toDataPathSegments = (schemaPath: string): string[] => { const s = schemaPath - .replace(/anyOf\/[\d]\//g, '') - .replace(/allOf\/[\d]\//g, '') - .replace(/oneOf\/[\d]\//g, ''); + .replace(/(anyOf|allOf|oneOf)\/[\d]\//g, '') + .replace(/(then|else)\//g, ''); const segments = s.split('/'); const decodedSegments = segments.map(decode); @@ -79,7 +78,7 @@ export const toDataPathSegments = (schemaPath: string): string[] => { */ export const toDataPath = (schemaPath: string): string => { return toDataPathSegments(schemaPath).join('.'); -}; + }; export const composeWithUi = (scopableUi: Scopable, path: string): string => { const segments = toDataPathSegments(scopableUi.scope); diff --git a/packages/core/src/util/resolvers.ts b/packages/core/src/util/resolvers.ts index 9c494e4fb..ad3d89c85 100644 --- a/packages/core/src/util/resolvers.ts +++ b/packages/core/src/util/resolvers.ts @@ -25,8 +25,8 @@ import isEmpty from 'lodash/isEmpty'; import get from 'lodash/get'; -import { JsonSchema } from '../models'; -import { decode, encode } from './path'; +import { JsonSchema, JsonSchema7 } from '../models'; +import { decode } from './path'; /** * Map for storing refs and the respective schemas they are pointing to. @@ -112,50 +112,67 @@ export const resolveSchema = ( schema: JsonSchema, schemaPath: string, rootSchema: JsonSchema +): JsonSchema => { + const segments = schemaPath?.split('/').map(decode); + return resolveSchemaWithSegments(schema, segments, rootSchema); +}; + +const resolveSchemaWithSegments = ( + schema: JsonSchema, + pathSegments: string[], + rootSchema: JsonSchema ): JsonSchema => { if (isEmpty(schema)) { return undefined; } - const validPathSegments = schemaPath.split('/').map(decode); - let resultSchema = schema; - for (let i = 0; i < validPathSegments.length; i++) { - let pathSegment = validPathSegments[i]; - resultSchema = - resultSchema === undefined || resultSchema.$ref === undefined - ? resultSchema - // use rootSchema as value for schema, since schema is undefined or a ref - : resolveSchema(rootSchema, resultSchema.$ref, rootSchema); - if (invalidSegment(pathSegment)) { - // skip invalid segments - continue; - } - let curSchema = get(resultSchema, pathSegment); - if (!curSchema) { - // resolving was not successful, check whether the scope omitted an oneOf, allOf or anyOf and resolve anyway - const schemas = [].concat( - resultSchema?.oneOf ?? [], - resultSchema?.allOf ?? [], - resultSchema?.anyOf ?? [] + + if (schema.$ref) { + schema = resolveSchema(rootSchema, schema.$ref, rootSchema); + } + + if (!pathSegments || pathSegments.length === 0) { + return schema; + } + + const [segment, ...remainingSegments] = pathSegments; + + if (invalidSegment(segment)) { + return resolveSchemaWithSegments(schema, remainingSegments, rootSchema); + } + + const singleSegmentResolveSchema = get(schema, segment); + + const resolvedSchema = resolveSchemaWithSegments(singleSegmentResolveSchema, remainingSegments, rootSchema); + if (resolvedSchema) { + return resolvedSchema; + } + + if (segment === 'properties' || segment === 'items') { + // Let's try to resolve the path, assuming oneOf/allOf/anyOf/then/else was omitted. + // We only do this when traversing an object or array as we want to avoid + // following a property which is named oneOf, allOf, anyOf, then or else. + let alternativeResolveResult = undefined; + + const subSchemas = [].concat( + schema.oneOf ?? [], + schema.allOf ?? [], + schema.anyOf ?? [], + (schema as JsonSchema7).then ?? [], + (schema as JsonSchema7).else ?? [] + ); + + for (const subSchema of subSchemas) { + alternativeResolveResult = resolveSchemaWithSegments( + subSchema, + [segment, ...remainingSegments], + rootSchema ); - for (let item of schemas) { - curSchema = resolveSchema(item, validPathSegments.slice(i).map(encode).join('/'), rootSchema); - if (curSchema) { - break; - } - } - if (curSchema) { - // already resolved rest of the path - resultSchema = curSchema; + if (alternativeResolveResult) { break; } } - resultSchema = curSchema; + return alternativeResolveResult; } - if (resultSchema !== undefined && resultSchema.$ref !== undefined) { - return resolveSchema(rootSchema, resultSchema.$ref, rootSchema) - ?? schema; - } - - return resultSchema; -}; + return undefined; +} \ No newline at end of file diff --git a/packages/core/test/util/path.test.ts b/packages/core/test/util/path.test.ts index 74ce2faba..d7299135d 100644 --- a/packages/core/test/util/path.test.ts +++ b/packages/core/test/util/path.test.ts @@ -46,12 +46,39 @@ test('toDataPath ', t => { test('toDataPath replace anyOf', t => { t.is(toDataPath('/anyOf/1/properties/foo/anyOf/1/properties/bar'), 'foo.bar'); }); +test('toDataPath replace anyOf in combination with conditional schema compositions', t => { + t.is(toDataPath('/anyOf/1/then/properties/foo'), 'foo'); +}); +test('toDataPath replace multiple directly nested anyOf in combination with conditional schema compositions', t => { + t.is(toDataPath('/anyOf/1/then/anyOf/0/then/properties/foo'), 'foo'); +}); +test('toDataPath replace multiple nested properties with anyOf in combination with conditional schema compositions', t => { + t.is(toDataPath('/anyOf/1/properties/foo/anyOf/0/then/properties/bar'), 'foo.bar'); +}); test('toDataPath replace allOf', t => { t.is(toDataPath('/allOf/1/properties/foo/allOf/1/properties/bar'), 'foo.bar'); }); +test('toDataPath replace allOf in combination with conditional schema compositions', t => { + t.is(toDataPath('/allOf/1/then/properties/foo'), 'foo'); +}); +test('toDataPath replace multiple directly nested allOf in combination with conditional schema compositions', t => { + t.is(toDataPath('/allOf/1/then/allOf/0/then/properties/foo'), 'foo'); +}); +test('toDataPath replace multiple nested properties with allOf in combination with conditional schema compositions', t => { + t.is(toDataPath('/allOf/1/properties/foo/allOf/0/then/properties/bar'), 'foo.bar'); +}); test('toDataPath replace oneOf', t => { t.is(toDataPath('/oneOf/1/properties/foo/oneOf/1/properties/bar'), 'foo.bar'); }); +test('toDataPath replace oneOf in combination with conditional schema compositions', t => { + t.is(toDataPath('/oneOf/1/then/properties/foo'), 'foo'); +}); +test('toDataPath replace multiple directly nested oneOf in combination with conditional schema compositions', t => { + t.is(toDataPath('/oneOf/1/then/oneOf/0/then/properties/foo'), 'foo'); +}); +test('toDataPath replace multiple nested properties with oneOf in combination with conditional schema compositions', t => { + t.is(toDataPath('/oneOf/1/properties/foo/oneOf/0/then/properties/bar'), 'foo.bar'); +}); test('toDataPath replace all combinators', t => { t.is( toDataPath( diff --git a/packages/core/test/util/resolvers.test.ts b/packages/core/test/util/resolvers.test.ts index db611e333..0efc7dc31 100644 --- a/packages/core/test/util/resolvers.test.ts +++ b/packages/core/test/util/resolvers.test.ts @@ -84,15 +84,58 @@ test('resolveSchema - resolves schema with any ', t => { } }] } - } + }, + anyOf: [ + { + if: { + properties: { + exist: { + const: true + } + } + }, + then: { + properties: { + lastname: { + type: 'string' + } + } + }, + else: { + properties: { + firstname: { + type: 'string' + }, + address: { + type: 'object', + anyOf: [ + { + properties: { + street: { + type: 'string' + } + } + } + ] + } + } + } + } + ] }; // test backward compatibility t.deepEqual(resolveSchema(schema, '#/properties/description/oneOf/0/properties/name', schema), {type: 'string'}); t.deepEqual(resolveSchema(schema, '#/properties/description/oneOf/1/properties/index', schema), {type: 'number'}); + t.deepEqual(resolveSchema(schema, '#/anyOf/0/then/properties/lastname', schema), {type: 'string'}); + t.deepEqual(resolveSchema(schema, '#/anyOf/0/else/properties/firstname', schema), {type: 'string'}); + t.deepEqual(resolveSchema(schema, '#/anyOf/0/else/properties/address/anyOf/0/properties/street', schema), {type: 'string'}); // new simple approach t.deepEqual(resolveSchema(schema, '#/properties/description/properties/name', schema), {type: 'string'}); t.deepEqual(resolveSchema(schema, '#/properties/description/properties/index', schema), {type: 'number'}); t.deepEqual(resolveSchema(schema, '#/properties/description/properties/exist', schema), {type: 'boolean'}); + t.deepEqual(resolveSchema(schema, '#/properties/lastname', schema), {type: 'string'}); + t.deepEqual(resolveSchema(schema, '#/properties/firstname', schema), {type: 'string'}); + t.deepEqual(resolveSchema(schema, '#/properties/address/properties/street', schema), {type: 'string'}); t.is(resolveSchema(schema, '#/properties/description/properties/notfound', schema), undefined); // refs t.deepEqual(resolveSchema(schema, '#/properties/description/properties/element/properties/geometry', schema), {type: 'string'}); diff --git a/packages/examples/src/examples/conditional-schema-compositions.ts b/packages/examples/src/examples/conditional-schema-compositions.ts new file mode 100644 index 000000000..98c7b3b6f --- /dev/null +++ b/packages/examples/src/examples/conditional-schema-compositions.ts @@ -0,0 +1,118 @@ +/* + The MIT License + + Copyright (c) 2017-2019 EclipseSource Munich + https://github.com/eclipsesource/jsonforms + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. +*/ +import { registerExamples } from '../register'; + +export const schema = { + type: 'object', + properties: { + name: { + type: 'string', + minLength: 1, + description: 'The task\'s name', + }, + recurrence: { + type: 'string', + enum: ['Never', 'Daily', 'Weekly', 'Monthly'], + }, + }, + anyOf: [ + { + if: { + properties: { + recurrence: { + const: 'Never' + } + } + }, + then: { + properties: { + lastname: { + type: 'string' + }, + age: { + type: 'number' + } + } + } + }, + ] +}; + +export const uischema = { + type: 'HorizontalLayout', + elements: [ + { + type: 'VerticalLayout', + elements: [ + { + type: 'Control', + scope: '#/properties/name', + }, + { + type: 'Control', + scope: '#/properties/recurrence', + }, + { + type: 'Control', + scope: '#/anyOf/0/then/properties/lastname', + rule: { + effect: 'SHOW', + condition: { + scope: '#/properties/recurrence', + schema: { + const: 'Never' + } + } + } + }, + { + type: 'Control', + scope: '#/properties/age', + rule: { + effect: 'SHOW', + condition: { + scope: '#/properties/recurrence', + schema: { + const: 'Never' + } + } + } + }, + ], + }, + ], +}; + +const data = {}; + +registerExamples([ + { + name: 'conditional-schema-compositions', + label: 'Conditional Schema Compositions', + data, + schema, + uischema + } +]); diff --git a/packages/examples/src/index.ts b/packages/examples/src/index.ts index 320444a1d..bdc5e3fa5 100644 --- a/packages/examples/src/index.ts +++ b/packages/examples/src/index.ts @@ -75,6 +75,7 @@ import * as enumInArray from './examples/enumInArray'; import * as readonly from './examples/readonly'; import * as bug_1779 from './examples/1779'; import * as bug_1645 from './examples/1645'; +import * as conditionalSchemaComposition from './examples/conditional-schema-compositions'; export * from './register'; export * from './example'; @@ -134,5 +135,6 @@ export { enumInArray, readonly, bug_1779, - bug_1645 + bug_1645, + conditionalSchemaComposition };