From 7ca16c3e4ae5253827bda6326da3a231b73272a7 Mon Sep 17 00:00:00 2001 From: sverhoeven Date: Fri, 12 Jul 2024 10:00:02 +0200 Subject: [PATCH 01/10] Handle incompatible keyword in haddock3 yaml files Manually edited catalog to test incompatible keyword Refs https://github.com/haddocking/haddock3/pull/935 TODO - [ ] generate JSON schema - [ ] handle incompatible fields being in a group - [ ] handle multiple incompatible fields in same object, note allOf did not work --- .../generate_haddock3_catalog.py | 2 ++ .../public/catalog/haddock3.easy.yaml | 31 ++++++++++++------- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/packages/haddock3_catalog/generate_haddock3_catalog.py b/packages/haddock3_catalog/generate_haddock3_catalog.py index f1ea0ab0..df2195e3 100755 --- a/packages/haddock3_catalog/generate_haddock3_catalog.py +++ b/packages/haddock3_catalog/generate_haddock3_catalog.py @@ -124,6 +124,8 @@ def config2schema(config): prop['description'] = v['short'] if 'long' in v and v['long'] != 'No long description yet': prop['$comment'] = v['long'] + if 'incompatible' in v: + # TODO handle incompatible parameters if 'type' not in v: # if not type field treat value as dict of dicts # TODO instead of removing group and explevel from mol1.prot_segid dict do proper filtering and support group recursivly diff --git a/packages/haddock3_catalog/public/catalog/haddock3.easy.yaml b/packages/haddock3_catalog/public/catalog/haddock3.easy.yaml index 708a6d1b..9a75d182 100644 --- a/packages/haddock3_catalog/public/catalog/haddock3.easy.yaml +++ b/packages/haddock3_catalog/public/catalog/haddock3.easy.yaml @@ -163,17 +163,24 @@ global: this parameter to `true` will add the javascript library in generated files, therefore completely isolating haddock3 from any web call. type: boolean - less_io: - default: false - title: Reduce the amount of I/O operations. - description: Reduce the amount of I/O operations. - $comment: This option will reduce the amount of I/O operations by writing - less files to disk. This can be useful for example when running on a network - file system where I/O operations are slow. - type: boolean required: - run_dir additionalProperties: false + if: + properties: + mode: + const: batch + then: {} + else: + properties: + less_io: + default: false + title: Reduce the amount of I/O operations. + description: Reduce the amount of I/O operations. + $comment: This option will reduce the amount of I/O operations by writing + less files to disk. This can be useful for example when running on a network + file system where I/O operations are slow. + type: boolean uiSchema: molecules: items: @@ -186,8 +193,8 @@ global: ui:group: execution ncores: ui:group: execution - mode: - ui:group: execution + # mode: + # ui:group: execution batch_type: ui:group: execution queue: @@ -200,8 +207,8 @@ global: ui:group: clean offline: ui:group: execution - less_io: - ui:group: execution + # less_io: + # ui:group: execution tomlSchema: {} nodes: - id: alascan From 76b60a0fc7e54a69212f4c63a19de449caeaaf7a Mon Sep 17 00:00:00 2001 From: Stefan Verhoeven Date: Thu, 22 Aug 2024 10:13:21 +0200 Subject: [PATCH 02/10] Create if/then/else for incompatible props --- .../generate_haddock3_catalog.py | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/packages/haddock3_catalog/generate_haddock3_catalog.py b/packages/haddock3_catalog/generate_haddock3_catalog.py index df2195e3..d06f05eb 100755 --- a/packages/haddock3_catalog/generate_haddock3_catalog.py +++ b/packages/haddock3_catalog/generate_haddock3_catalog.py @@ -109,6 +109,7 @@ def config2schema(config): required = [] collapsed_config = collapse_expandable(config) + ifthenelses = {} for k, v in collapsed_config.items(): prop = {} prop_ui = {} @@ -125,7 +126,22 @@ def config2schema(config): if 'long' in v and v['long'] != 'No long description yet': prop['$comment'] = v['long'] if 'incompatible' in v: - # TODO handle incompatible parameters + ifthenelse = { + "if": { + "properties": {} + }, + "then": {}, + "else": { + "properties": { + k: prop + } + } + } + for ik, iv in v['incompatible'].items(): + ifthenelse['if']['properties'][ik] = { + "const": iv + } + ifthenelses[k] = ifthenelse if 'type' not in v: # if not type field treat value as dict of dicts # TODO instead of removing group and explevel from mol1.prot_segid dict do proper filtering and support group recursivly @@ -338,7 +354,8 @@ def config2schema(config): raise ValueError(f"Don't know how to determine type of items of {v}") else: raise ValueError(f"Don't know what to do with {k}:{v}") - properties[k] = prop + if k not in ifthenelses: + properties[k] = prop if 'group' in v and v['group'] != '' and v['group'] is not None: prop_ui['ui:group'] = v['group'] if prop_ui: @@ -351,6 +368,10 @@ def config2schema(config): "required": required, "additionalProperties": False } + if ifthenelses: + if len(ifthenelses) > 1: + raise ValueError(f"Only one ifthenelse is supported, but got {len(ifthenelses)}") + schema.update(list(ifthenelses.values())[0]) return { "schema": schema, "uiSchema": uiSchema, From f50406174636440a83c10e98e5bd51117499c5c8 Mon Sep 17 00:00:00 2001 From: Stefan Verhoeven Date: Thu, 22 Aug 2024 10:46:50 +0200 Subject: [PATCH 03/10] Added test --- packages/core/src/grouper.test.ts | 153 ++++++++++++++++++++++++------ 1 file changed, 125 insertions(+), 28 deletions(-) diff --git a/packages/core/src/grouper.test.ts b/packages/core/src/grouper.test.ts index 145e4758..4359ec32 100644 --- a/packages/core/src/grouper.test.ts +++ b/packages/core/src/grouper.test.ts @@ -1,7 +1,13 @@ import { expect, describe, it, beforeEach } from 'vitest' import { JSONSchema7 } from 'json-schema' import { UiSchema } from '@rjsf/core' -import { groupCatalog, groupParameters, groupSchema, groupUiSchema, unGroupParameters } from './grouper' +import { + groupCatalog, + groupParameters, + groupSchema, + groupUiSchema, + unGroupParameters +} from './grouper' import { ICatalog, IParameters } from './types' function deepCopy (value: T): T { @@ -288,18 +294,22 @@ describe('given a schema with a 2 props with ui:group in uiSchema and 1 without' schema, uiSchema }, - categories: [{ - name: 'category1', - description: 'Category 1' - }], - nodes: [{ - schema, - uiSchema, - id: 'node1', - label: 'Node 1', - description: 'Description 1', - category: 'category1' - }], + categories: [ + { + name: 'category1', + description: 'Category 1' + } + ], + nodes: [ + { + schema, + uiSchema, + id: 'node1', + label: 'Node 1', + description: 'Description 1', + category: 'category1' + } + ], examples: {} } @@ -342,20 +352,24 @@ describe('given a schema with a 2 props with ui:group in uiSchema and 1 without' formSchema: expectedSchema, formUiSchema: expecteduiSchema }, - categories: [{ - name: 'category1', - description: 'Category 1' - }], - nodes: [{ - schema, - uiSchema, - formSchema: expectedSchema, - formUiSchema: expecteduiSchema, - id: 'node1', - label: 'Node 1', - description: 'Description 1', - category: 'category1' - }], + categories: [ + { + name: 'category1', + description: 'Category 1' + } + ], + nodes: [ + { + schema, + uiSchema, + formSchema: expectedSchema, + formUiSchema: expecteduiSchema, + id: 'node1', + label: 'Node 1', + description: 'Description 1', + category: 'category1' + } + ], examples: {} } expect(actual).toEqual(expected) @@ -499,7 +513,9 @@ describe('given a un-grouped prop with same name as group', () => { } } - expect(() => groupSchema(schema, uiSchema)).toThrow('Can not have group and un-grouped parameter with same name prop2') + expect(() => groupSchema(schema, uiSchema)).toThrow( + 'Can not have group and un-grouped parameter with same name prop2' + ) }) }) }) @@ -633,3 +649,84 @@ describe('given a prop with same name as group and another prop in same group', }) }) }) + +describe('given a schema with if/then/else', () => { + let schema: JSONSchema7 + let uiSchema: UiSchema + + beforeEach(() => { + schema = { + type: 'object', + properties: { + prop1: { + type: 'string', + enum: ['val1', 'val2'] + } + }, + additionalProperties: false, + if: { + properties: { + prop1: { + const: 'val1' + } + } + }, + then: {}, + else: { + properties: { + prop2: { + type: 'string' + } + } + } + } + uiSchema = { + prop1: { + 'ui:group': 'group1' + }, + prop2: { + 'ui:group': 'group1' + } + } + }) + + describe('groupSchema()', () => { + it('should move properties inside an object with group name as key', () => { + const groupedSchema = groupSchema(schema, uiSchema) + console.log(JSON.stringify(groupedSchema, null, 2)) + + const expectedSchema: JSONSchema7 = { + type: 'object', + properties: { + group1: { + type: 'object', + properties: { + prop1: { + type: 'string', + enum: ['val1', 'val2'] + } + }, + additionalProperties: false, + if: { + properties: { + prop1: { + const: 'val1' + } + } + }, + then: {}, + else: { + properties: { + prop2: { + type: 'string' + } + } + } + } + }, + additionalProperties: false + } + expect(groupedSchema).toEqual(expectedSchema) + }) + }) +}) From 881bca42f6df407ff923c94170e6dd0936b6b642 Mon Sep 17 00:00:00 2001 From: Stefan Verhoeven Date: Thu, 22 Aug 2024 12:55:00 +0200 Subject: [PATCH 04/10] Show all errors in tooltip Helps when prop with erro is conditional and is not being rendered --- packages/form/src/CollapsibleField.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/form/src/CollapsibleField.tsx b/packages/form/src/CollapsibleField.tsx index c8fdf458..f4cf7b9f 100644 --- a/packages/form/src/CollapsibleField.tsx +++ b/packages/form/src/CollapsibleField.tsx @@ -97,7 +97,7 @@ export const CollapsibleField = (props: FieldProps): JSX.Element => { {title} {/* error icon */} - {hasError ? : null} + {hasError ?
: null} {/* show content when not collapsed */} {/* TitleField inside the ObjectField is not rendered when there is no title or name */} From f8d1b237ff6e981c8278abbcb1da360772348ca1 Mon Sep 17 00:00:00 2001 From: Stefan Verhoeven Date: Thu, 22 Aug 2024 12:57:06 +0200 Subject: [PATCH 05/10] Only show less_io when mode!=batch --- .../public/catalog/haddock3.easy.yaml | 8 +++---- .../public/catalog/haddock3.expert.yaml | 23 ++++++++++++------- .../public/catalog/haddock3.guru.yaml | 23 ++++++++++++------- 3 files changed, 34 insertions(+), 20 deletions(-) diff --git a/packages/haddock3_catalog/public/catalog/haddock3.easy.yaml b/packages/haddock3_catalog/public/catalog/haddock3.easy.yaml index 9855c859..fd9c5e5c 100644 --- a/packages/haddock3_catalog/public/catalog/haddock3.easy.yaml +++ b/packages/haddock3_catalog/public/catalog/haddock3.easy.yaml @@ -193,8 +193,8 @@ global: ui:group: execution ncores: ui:group: execution - # mode: - # ui:group: execution + mode: + ui:group: execution batch_type: ui:group: execution queue: @@ -207,8 +207,8 @@ global: ui:group: clean offline: ui:group: execution - # less_io: - # ui:group: execution + less_io: + ui:group: execution tomlSchema: {} nodes: - id: alascan diff --git a/packages/haddock3_catalog/public/catalog/haddock3.expert.yaml b/packages/haddock3_catalog/public/catalog/haddock3.expert.yaml index 3d4af049..dc28ad72 100644 --- a/packages/haddock3_catalog/public/catalog/haddock3.expert.yaml +++ b/packages/haddock3_catalog/public/catalog/haddock3.expert.yaml @@ -173,17 +173,24 @@ global: this parameter to `true` will add the javascript library in generated files, therefore completely isolating haddock3 from any web call. type: boolean - less_io: - default: false - title: Reduce the amount of I/O operations. - description: Reduce the amount of I/O operations. - $comment: This option will reduce the amount of I/O operations by writing - less files to disk. This can be useful for example when running on a network - file system where I/O operations are slow. - type: boolean required: - run_dir additionalProperties: false + if: + properties: + mode: + const: batch + then: {} + else: + properties: + less_io: + default: false + title: Reduce the amount of I/O operations. + description: Reduce the amount of I/O operations. + $comment: This option will reduce the amount of I/O operations by writing + less files to disk. This can be useful for example when running on a network + file system where I/O operations are slow. + type: boolean uiSchema: molecules: items: diff --git a/packages/haddock3_catalog/public/catalog/haddock3.guru.yaml b/packages/haddock3_catalog/public/catalog/haddock3.guru.yaml index 7676cf14..8d94561a 100644 --- a/packages/haddock3_catalog/public/catalog/haddock3.guru.yaml +++ b/packages/haddock3_catalog/public/catalog/haddock3.guru.yaml @@ -183,17 +183,24 @@ global: this parameter to `true` will add the javascript library in generated files, therefore completely isolating haddock3 from any web call. type: boolean - less_io: - default: false - title: Reduce the amount of I/O operations. - description: Reduce the amount of I/O operations. - $comment: This option will reduce the amount of I/O operations by writing - less files to disk. This can be useful for example when running on a network - file system where I/O operations are slow. - type: boolean required: - run_dir additionalProperties: false + if: + properties: + mode: + const: batch + then: {} + else: + properties: + less_io: + default: false + title: Reduce the amount of I/O operations. + description: Reduce the amount of I/O operations. + $comment: This option will reduce the amount of I/O operations by writing + less files to disk. This can be useful for example when running on a network + file system where I/O operations are slow. + type: boolean uiSchema: molecules: items: From 03f5e1327236587b7cceb255cf5f34058e8f146f Mon Sep 17 00:00:00 2001 From: Stefan Verhoeven Date: Thu, 22 Aug 2024 13:00:06 +0200 Subject: [PATCH 06/10] Move prop in if/then/else condtion to group Only move if the props in the condition are in the same group --- packages/core/src/grouper.test.ts | 303 ++++++++++++++++++++++++------ packages/core/src/grouper.ts | 94 ++++++++- 2 files changed, 338 insertions(+), 59 deletions(-) diff --git a/packages/core/src/grouper.test.ts b/packages/core/src/grouper.test.ts index 4359ec32..99ea6650 100644 --- a/packages/core/src/grouper.test.ts +++ b/packages/core/src/grouper.test.ts @@ -1,4 +1,4 @@ -import { expect, describe, it, beforeEach } from 'vitest' +import { expect, describe, it, beforeEach, assert } from 'vitest' import { JSONSchema7 } from 'json-schema' import { UiSchema } from '@rjsf/core' import { @@ -654,79 +654,272 @@ describe('given a schema with if/then/else', () => { let schema: JSONSchema7 let uiSchema: UiSchema - beforeEach(() => { - schema = { - type: 'object', - properties: { - prop1: { - type: 'string', - enum: ['val1', 'val2'] - } - }, - additionalProperties: false, - if: { + describe('else in same group', () => { + beforeEach(() => { + schema = { + type: 'object', properties: { prop1: { - const: 'val1' + type: 'string', + enum: ['val1', 'val2'] } - } - }, - then: {}, - else: { - properties: { - prop2: { - type: 'string' + }, + additionalProperties: false, + if: { + properties: { + prop1: { + const: 'val1' + } + } + }, + then: {}, + else: { + properties: { + prop2: { + type: 'string' + } } } } - } - uiSchema = { - prop1: { - 'ui:group': 'group1' - }, - prop2: { - 'ui:group': 'group1' + uiSchema = { + prop1: { + 'ui:group': 'group1' + }, + prop2: { + 'ui:group': 'group1' + } } - } - }) + }) - describe('groupSchema()', () => { - it('should move properties inside an object with group name as key', () => { - const groupedSchema = groupSchema(schema, uiSchema) - console.log(JSON.stringify(groupedSchema, null, 2)) + describe('groupSchema()', () => { + it('should move properties inside an object with group name as key', () => { + const groupedSchema = groupSchema(schema, uiSchema) - const expectedSchema: JSONSchema7 = { - type: 'object', - properties: { - group1: { - type: 'object', - properties: { - prop1: { - type: 'string', - enum: ['val1', 'val2'] - } - }, - additionalProperties: false, - if: { + const expectedSchema: JSONSchema7 = { + type: 'object', + properties: { + group1: { + type: 'object', properties: { prop1: { - const: 'val1' + type: 'string', + enum: ['val1', 'val2'] + } + }, + additionalProperties: false, + if: { + properties: { + prop1: { + const: 'val1' + } + } + }, + then: {}, + else: { + properties: { + prop2: { + type: 'string' + } } } - }, - then: {}, - else: { + } + }, + additionalProperties: false + } + expect(groupedSchema).toEqual(expectedSchema) + }) + }) + }) + + describe('else in same group', () => { + beforeEach(() => { + schema = { + type: 'object', + properties: { + prop1: { + type: 'string', + enum: ['val1', 'val2'] + } + }, + additionalProperties: false, + if: { + properties: { + prop1: { + const: 'val1' + } + } + }, + then: { + properties: { + prop2: { + type: 'string' + } + } + }, + else: {} + } + uiSchema = { + prop1: { + 'ui:group': 'group1' + }, + prop2: { + 'ui:group': 'group1' + } + } + }) + + describe('groupSchema()', () => { + it('should move properties inside an object with group name as key', () => { + const groupedSchema = groupSchema(schema, uiSchema) + + const expectedSchema: JSONSchema7 = { + type: 'object', + properties: { + group1: { + type: 'object', properties: { - prop2: { - type: 'string' + prop1: { + type: 'string', + enum: ['val1', 'val2'] } - } + }, + additionalProperties: false, + if: { + properties: { + prop1: { + const: 'val1' + } + } + }, + then: { + properties: { + prop2: { + type: 'string' + } + } + }, + else: {} } + }, + additionalProperties: false + } + expect(groupedSchema).toEqual(expectedSchema) + }) + }) + }) + + describe.each<{ label: string, schema: JSONSchema7, uiSchema: UiSchema }>([ + { + label: 'if and else in different group', + schema: { + type: 'object', + properties: { + prop1: { + type: 'string', + enum: ['val1', 'val2'] } }, - additionalProperties: false + additionalProperties: false, + if: { + properties: { + prop1: { + const: 'val1' + } + } + }, + then: { + }, + else: { + properties: { + prop2: { + type: 'string' + } + } + } + }, + uiSchema: { + prop1: { + 'ui:group': 'group1' + }, + prop2: { + 'ui:group': 'group2' + } } - expect(groupedSchema).toEqual(expectedSchema) + }, + { + label: 'if and then in different group', + schema: { + type: 'object', + properties: { + prop1: { + type: 'string', + enum: ['val1', 'val2'] + } + }, + additionalProperties: false, + if: { + properties: { + prop1: { + const: 'val1' + } + } + }, + then: { + properties: { + prop2: { + type: 'string' + } + } + }, + else: {} + }, + uiSchema: { + prop1: { + 'ui:group': 'group1' + }, + prop2: { + 'ui:group': 'group2' + } + } + }, + { + label: 'if not in group', + schema: { + type: 'object', + properties: { + prop1: { + type: 'string', + enum: ['val1', 'val2'] + } + }, + additionalProperties: false, + if: { + properties: { + prop1: { + const: 'val1' + } + } + }, + then: { + properties: { + prop2: { + type: 'string' + } + } + }, + else: {} + }, + uiSchema: { + prop2: { + 'ui:group': 'group2' + } + } + } + ])('$label', ({ schema, uiSchema }) => { + it('groupSchema() should throw error', () => { + assert.throws(() => { + groupSchema(schema, uiSchema) + }, 'Cannot have an if in one group and a then/else in another group') }) }) }) diff --git a/packages/core/src/grouper.ts b/packages/core/src/grouper.ts index 7c1e2e5a..32d70f3e 100644 --- a/packages/core/src/grouper.ts +++ b/packages/core/src/grouper.ts @@ -18,6 +18,67 @@ import { ICatalog, IParameters } from './types' import { JSONSchema7 } from 'json-schema' import { isObject } from './utils/isObject' +function groupOfIfProp (schema: JSONSchema7, uiSchema: UiSchema): string | undefined { + if (!(schema.if !== undefined && + typeof schema.if !== 'boolean' && + (schema.if.properties != null))) { + return undefined + } + // TODO handle multiple props in schema.if + const ifPropName = Object.keys(schema.if.properties)[0] + if (ifPropName in uiSchema && 'ui:group' in uiSchema[ifPropName]) { + return uiSchema[ifPropName]['ui:group'] + } + return undefined +} + +function groupSchemaCondition ({ + k, + v, + group, + newSchema, + uiSchema, + newGroup +}: { + k: string + v: UiSchema + group: string + newSchema: JSONSchema7 + uiSchema: UiSchema + newGroup: JSONSchema7 +}): boolean { + // TODO handle when if is in group but then/else is not + // now treated as normal prop instead of conditional + const elseInGroup = + newSchema.else !== undefined && + typeof newSchema.else !== 'boolean' && + (newSchema.else.properties != null) && + k in newSchema.else.properties + const thenInGroup = + newSchema.then !== undefined && + typeof newSchema.then !== 'boolean' && + (newSchema.then.properties != null) && + k in newSchema.then.properties + if (thenInGroup ?? elseInGroup) { + const ifGroup = groupOfIfProp(newSchema, uiSchema) + if (ifGroup !== group) { + throw new Error( + 'Cannot have an if in one group and a then/else in another group' + ) + } + newGroup.if = newSchema.if + newGroup.then = newSchema.then + newGroup.else = newSchema.else + /* eslint-disable @typescript-eslint/no-dynamic-delete */ + delete newSchema.if + delete newSchema.then + delete newSchema.else + /* eslint-enable @typescript-eslint/no-dynamic-delete */ + return true + } + return false +} + /** * Create new JSON schema where properties marked with same `ui:group` are grouped together into an object. * @@ -41,7 +102,9 @@ export function groupSchema ( Object.keys(uiSchema).filter((k) => 'ui:group' in uiSchema[k]) ) const allProps = new Set(Object.keys(schema.properties ?? {})) - const grouplessProps = new Set([...allProps].filter((x) => !propsWithGroup.has(x))) + const grouplessProps = new Set( + [...allProps].filter((x) => !propsWithGroup.has(x)) + ) const grouplessPropsWithSameNameAsGroup = new Set( [...grouplessProps].filter((x) => definedGroups.has(x)) ) @@ -53,12 +116,18 @@ export function groupSchema ( ) } - if (!('properties' in newSchema && typeof newSchema.properties === 'object')) { + if ( + !('properties' in newSchema && typeof newSchema.properties === 'object') + ) { return newSchema } // prop with group and same name as any group should be nested first - const propsWithSameNameAsAnyGroup = new Set(Object.entries(uiSchema).filter(([k, v]) => 'ui:group' in v && definedGroups.has(k)).map((d) => d[0])) + const propsWithSameNameAsAnyGroup = new Set( + Object.entries(uiSchema) + .filter(([k, v]) => 'ui:group' in v && definedGroups.has(k)) + .map((d) => d[0]) + ) for (const k of propsWithSameNameAsAnyGroup) { const prop = newSchema.properties[k] /* eslint-disable @typescript-eslint/no-dynamic-delete */ @@ -78,7 +147,9 @@ export function groupSchema ( // TODO recursivly, now only loops over first direct props if ('ui:group' in v && !propsWithSameNameAsAnyGroup.has(k)) { const group = v['ui:group'] - if (!('properties' in newSchema && typeof newSchema.properties === 'object')) { + if ( + !('properties' in newSchema && typeof newSchema.properties === 'object') + ) { throw new Error('Schema must have properties') } if (!(group in newSchema.properties)) { @@ -92,6 +163,21 @@ export function groupSchema ( if (typeof newGroup === 'boolean' || newGroup.properties === undefined) { return } + if ( + groupSchemaCondition({ + k, + v, + group, + newSchema, + uiSchema, + newGroup + }) + ) { + // If k was a then or else prop then it is has been moved to the newGroup as if/then/else + // no need to move it as property + return + } + newGroup.properties[k] = newSchema.properties[k] // Remove k as it now is in the group /* eslint-disable @typescript-eslint/no-dynamic-delete */ From aeb28897af1545a491085dfb1cd66067fd5eb623 Mon Sep 17 00:00:00 2001 From: sverhoeven Date: Fri, 23 Aug 2024 12:17:55 +0200 Subject: [PATCH 07/10] Prune else parameter value when if is true and the inverse --- packages/core/src/grouper.test.ts | 2 +- packages/core/src/grouper.ts | 2 +- packages/core/src/pruner.test.ts | 132 ++++++++++++++++++++++++++++++ packages/core/src/pruner.ts | 36 +++++++- 4 files changed, 169 insertions(+), 3 deletions(-) diff --git a/packages/core/src/grouper.test.ts b/packages/core/src/grouper.test.ts index 99ea6650..1d27e239 100644 --- a/packages/core/src/grouper.test.ts +++ b/packages/core/src/grouper.test.ts @@ -731,7 +731,7 @@ describe('given a schema with if/then/else', () => { }) }) - describe('else in same group', () => { + describe('then in same group', () => { beforeEach(() => { schema = { type: 'object', diff --git a/packages/core/src/grouper.ts b/packages/core/src/grouper.ts index 32d70f3e..3a54a2b6 100644 --- a/packages/core/src/grouper.ts +++ b/packages/core/src/grouper.ts @@ -59,7 +59,7 @@ function groupSchemaCondition ({ typeof newSchema.then !== 'boolean' && (newSchema.then.properties != null) && k in newSchema.then.properties - if (thenInGroup ?? elseInGroup) { + if (thenInGroup || elseInGroup) { const ifGroup = groupOfIfProp(newSchema, uiSchema) if (ifGroup !== group) { throw new Error( diff --git a/packages/core/src/pruner.test.ts b/packages/core/src/pruner.test.ts index 1e346aa2..e563e6dc 100644 --- a/packages/core/src/pruner.test.ts +++ b/packages/core/src/pruner.test.ts @@ -640,4 +640,136 @@ describe('pruneDefaults()', () => { expect(result).toStrictEqual(expected) }) }) + + describe('if/then/else', () => { + describe('given if is true and has else parameter', () => { + it('should remove else parameter', () => { + const parameters = { + prop1: 'val1', + prop2: 'someval' + } + const schema: JSONSchema7 = { + type: 'object', + properties: { + prop1: { + type: 'string', + enum: ['val1', 'val2'] + } + }, + if: { + properties: { + prop1: { + const: 'val1' + } + } + }, + then: { + }, + else: { + properties: { + prop2: { + type: 'string' + } + } + }, + additionalProperties: false + } + + const result = pruneDefaults(parameters, schema, true) + const expected = { + prop1: 'val1' + } + expect(result).toStrictEqual(expected) + }) + }) + + describe('given if is false and has then parameter', () => { + it('should remove then parameter', () => { + const parameters = { + prop1: 'val2', + prop2: 'someval' + } + const schema: JSONSchema7 = { + type: 'object', + properties: { + prop1: { + type: 'string', + enum: ['val1', 'val2'] + } + }, + if: { + properties: { + prop1: { + const: 'val1' + } + } + }, + then: { + properties: { + prop2: { + type: 'string' + } + } + }, + else: { + }, + additionalProperties: false + } + + const result = pruneDefaults(parameters, schema, true) + const expected = { + prop1: 'val2' + } + expect(result).toStrictEqual(expected) + }) + }) + + describe('given if is true and has then and else parameter', () => { + it('should remove else parameter, but keep then', () => { + const parameters = { + prop1: 'val1', + prop2: 'someval', + prop3: 'someval3' + } + const schema: JSONSchema7 = { + type: 'object', + properties: { + prop1: { + type: 'string', + enum: ['val1', 'val2'] + } + }, + if: { + properties: { + prop1: { + const: 'val1' + } + } + }, + then: { + properties: { + prop3: { + type: 'string' + } + } + }, + else: { + properties: { + prop2: { + type: 'string' + } + } + }, + additionalProperties: false + } + + const result = pruneDefaults(parameters, schema, true) + const expected = { + prop1: 'val1', + prop3: 'someval3' + } + expect(result).toStrictEqual(expected) + }) + }) + }) }) diff --git a/packages/core/src/pruner.ts b/packages/core/src/pruner.ts index 77c408c6..06bf046e 100644 --- a/packages/core/src/pruner.ts +++ b/packages/core/src/pruner.ts @@ -72,5 +72,39 @@ export function pruneDefaults (parameters: IParameters, schema: JSONSchema7, res newParameters[k] = v } }) - return newParameters + return pruneThenElses(newParameters, schema) +} + +function pruneThenElses (parameters: IParameters, schema: JSONSchema7): IParameters { + if (!(schema.if !== undefined && typeof schema.if !== 'boolean' && schema.if !== undefined && schema.if.properties !== undefined)) { + // no if then bail out + return parameters + } + + const condition = Object.entries(schema.if.properties).every( + ([k, sv]) => typeof sv === 'object' && sv.const !== undefined && sv.const === parameters[k] + ) + if (condition) { + // Remove else parameters + if (schema.else !== undefined && typeof schema.else !== 'boolean' && schema.else !== undefined && schema.else.properties !== undefined) { + const elsePropnames = Object.keys(schema.else.properties) + for (const k of elsePropnames) { + /* eslint-disable @typescript-eslint/no-dynamic-delete */ + delete parameters[k] + /* eslint-enable @typescript-eslint/no-dynamic-delete */ + } + } + } else { + // Remove then parameters + if (schema.then !== undefined && typeof schema.then !== 'boolean' && schema.then !== undefined && schema.then.properties !== undefined) { + const thenPropnames = Object.keys(schema.then.properties) + for (const k of thenPropnames) { + /* eslint-disable @typescript-eslint/no-dynamic-delete */ + delete parameters[k] + /* eslint-enable @typescript-eslint/no-dynamic-delete */ + } + } + } + + return parameters } From dcfaea4c8ee2d8c9a2efc6f953fb6301e0aa134b Mon Sep 17 00:00:00 2001 From: sverhoeven Date: Fri, 23 Aug 2024 12:52:16 +0200 Subject: [PATCH 08/10] Remove then/else parameter if it same as default --- packages/core/src/pruner.test.ts | 83 ++++++++++++++++++++++++++++++++ packages/core/src/pruner.ts | 24 +++++++++ 2 files changed, 107 insertions(+) diff --git a/packages/core/src/pruner.test.ts b/packages/core/src/pruner.test.ts index e563e6dc..eaf7e3de 100644 --- a/packages/core/src/pruner.test.ts +++ b/packages/core/src/pruner.test.ts @@ -771,5 +771,88 @@ describe('pruneDefaults()', () => { expect(result).toStrictEqual(expected) }) }) + + describe('given if is false and has else parameter which is same as default', () => { + it('should remove else parameter', () => { + const parameters = { + prop1: 'val2', + prop2: 'someval' + } + const schema: JSONSchema7 = { + type: 'object', + properties: { + prop1: { + type: 'string', + enum: ['val1', 'val2'] + } + }, + if: { + properties: { + prop1: { + const: 'val1' + } + } + }, + then: { + }, + else: { + properties: { + prop2: { + type: 'string', + default: 'someval' + } + } + }, + additionalProperties: false + } + + const result = pruneDefaults(parameters, schema, true) + const expected = { + prop1: 'val2' + } + expect(result).toStrictEqual(expected) + }) + }) + + describe('given if is true and has then parameter which is same as default', () => { + it('should remove then parameter', () => { + const parameters = { + prop1: 'val1', + prop2: 'someval' + } + const schema: JSONSchema7 = { + type: 'object', + properties: { + prop1: { + type: 'string', + enum: ['val1', 'val2'] + } + }, + if: { + properties: { + prop1: { + const: 'val1' + } + } + }, + then: { + properties: { + prop2: { + type: 'string', + default: 'someval' + } + } + }, + else: {}, + additionalProperties: false + } + + const result = pruneDefaults(parameters, schema, true) + const expected = { + prop1: 'val1' + } + expect(result).toStrictEqual(expected) + }) + }) }) }) diff --git a/packages/core/src/pruner.ts b/packages/core/src/pruner.ts index 06bf046e..1d62bc26 100644 --- a/packages/core/src/pruner.ts +++ b/packages/core/src/pruner.ts @@ -94,6 +94,18 @@ function pruneThenElses (parameters: IParameters, schema: JSONSchema7): IParamet /* eslint-enable @typescript-eslint/no-dynamic-delete */ } } + // Remove then parameters that are same as default + if (schema.then !== undefined && typeof schema.then !== 'boolean' && schema.then !== undefined && schema.then.properties !== undefined) { + const thenPropnames = Object.keys(schema.then.properties) + for (const k of thenPropnames) { + const p = schema.then.properties[k] + if (typeof p === 'object' && p.default === parameters[k]) { + /* eslint-disable @typescript-eslint/no-dynamic-delete */ + delete parameters[k] + /* eslint-enable @typescript-eslint/no-dynamic-delete */ + } + } + } } else { // Remove then parameters if (schema.then !== undefined && typeof schema.then !== 'boolean' && schema.then !== undefined && schema.then.properties !== undefined) { @@ -104,6 +116,18 @@ function pruneThenElses (parameters: IParameters, schema: JSONSchema7): IParamet /* eslint-enable @typescript-eslint/no-dynamic-delete */ } } + // Remove else parameters that are same as default + if (schema.else !== undefined && typeof schema.else !== 'boolean' && schema.else !== undefined && schema.else.properties !== undefined) { + const elsePropnames = Object.keys(schema.else.properties) + for (const k of elsePropnames) { + const p = schema.else.properties[k] + if (typeof p === 'object' && p.default === parameters[k]) { + /* eslint-disable @typescript-eslint/no-dynamic-delete */ + delete parameters[k] + /* eslint-enable @typescript-eslint/no-dynamic-delete */ + } + } + } } return parameters From 4cb788fbf14fba66e01b6b17068c9f7213868f6c Mon Sep 17 00:00:00 2001 From: sverhoeven Date: Fri, 23 Aug 2024 13:26:09 +0200 Subject: [PATCH 09/10] Docs --- CHANGELOG.md | 1 + docs/schema.md | 24 ++++++++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index efb44b48..6a8ac739 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added * Hook useSaveWithGlobalRewrite() ([#161](https://github.com/i-VRESSE/workflow-builder/pull/161)) +* Support for if/then/else in JSON schema ([#160](https://github.com/i-VRESSE/workflow-builder/pull/160)) ## @i-vresse/wb-core 3.0.1 - 2024-05-23 diff --git a/docs/schema.md b/docs/schema.md index 4e96fc80..7236904b 100644 --- a/docs/schema.md +++ b/docs/schema.md @@ -131,3 +131,27 @@ and `abcd.pdb` file has chain A and B with residues 1, 2, 3 and 4. Then the form will have restricted the `chain` prop to only allow `A` and `B` and will have restricted the sta and end prop to only allow 1, 2, 3 and 4. + +## If then else + +The `if`, `then` and `else` keywords can be used to [conditionally apply a schema](https://json-schema.org/understanding-json-schema/reference/conditionals#ifthenelse). + +For example to have a the `foo` property only if the `bar` property is false use: + +```yaml + type: object + properties: + bar: + type: boolean + if: + properties: + bar: + const: true + then: {} + else: + properties: + foo: + type: string +``` + +Only supports simple const condition with one or more properties and not complex conditions like patterns. From ad37895f500bc5721950f8af27b4e5ca2156c7f9 Mon Sep 17 00:00:00 2001 From: sverhoeven Date: Fri, 23 Aug 2024 13:34:03 +0200 Subject: [PATCH 10/10] English --- docs/schema.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/schema.md b/docs/schema.md index 7236904b..55d9adf7 100644 --- a/docs/schema.md +++ b/docs/schema.md @@ -136,7 +136,7 @@ and will have restricted the sta and end prop to only allow 1, 2, 3 and 4. The `if`, `then` and `else` keywords can be used to [conditionally apply a schema](https://json-schema.org/understanding-json-schema/reference/conditionals#ifthenelse). -For example to have a the `foo` property only if the `bar` property is false use: +For example to have the `foo` property only if the `bar` property is false use: ```yaml type: object @@ -154,4 +154,4 @@ For example to have a the `foo` property only if the `bar` property is false use type: string ``` -Only supports simple const condition with one or more properties and not complex conditions like patterns. +Only supports simple const condition with one or more properties and not complex conditions like patterns. Also only a single if/tnen/else block per object is supported. It can be combined with [groups](uiSchema.md#uigroup).