Skip to content

Commit 0a1d02f

Browse files
authored
Merge pull request #867 from fractal-analytics-platform/conditional-properties-oneOf-main
Added support for oneOf and discriminator
2 parents dea607b + ff6d6b9 commit 0a1d02f

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+1060
-272
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
* Merged user settings into user (\#862);
1515
* Adapted to OAuth2 changes (\#862);
1616
* Handled user without profile as not verified users (\#862);
17+
* Added support for oneOf and discriminator (\#867);
18+
* Added resource filter on admin task and task-group pages (\#867);
1719

1820
# 1.20.0
1921

components/__tests__/jschema/bad_input.test.js

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,15 @@ describe('badInput', () => {
2222
'pydantic_v1'
2323
);
2424
expect(manager.getFormData()).deep.eq({ foo: { bar: null } });
25-
manager.root.children[0].children[0].badInput = true;
25+
const objElement =
26+
/** @type {import('../../src/lib/jschema/form_element').ObjectFormElement} */ (
27+
manager.root.children[0]
28+
);
29+
const numElement =
30+
/** @type {import('../../src/lib/jschema/form_element').NumberFormElement} */ (
31+
objElement.children[0]
32+
);
33+
numElement.badInput = true;
2634
expect(manager.getFormData()).deep.eq({ foo: { bar: 'invalid' } });
2735
});
2836

@@ -45,7 +53,16 @@ describe('badInput', () => {
4553
'pydantic_v1'
4654
);
4755
expect(manager.getFormData()).deep.eq({ foo: [0] });
48-
manager.root.children[0].children[0].badInput = true;
56+
57+
const objElement =
58+
/** @type {import('../../src/lib/jschema/form_element').ObjectFormElement} */ (
59+
manager.root.children[0]
60+
);
61+
const numElement =
62+
/** @type {import('../../src/lib/jschema/form_element').NumberFormElement} */ (
63+
objElement.children[0]
64+
);
65+
numElement.badInput = true;
4966
expect(manager.getFormData()).deep.eq({ foo: ['invalid'] });
5067
});
5168

@@ -72,7 +89,15 @@ describe('badInput', () => {
7289
'pydantic_v1'
7390
);
7491
expect(manager.getFormData()).deep.eq({ foo: [0] });
75-
manager.root.children[0].children[0].badInput = true;
92+
const objElement =
93+
/** @type {import('../../src/lib/jschema/form_element').ObjectFormElement} */ (
94+
manager.root.children[0]
95+
);
96+
const numElement =
97+
/** @type {import('../../src/lib/jschema/form_element').NumberFormElement} */ (
98+
objElement.children[0]
99+
);
100+
numElement.badInput = true;
76101
expect(manager.getFormData()).deep.eq({ foo: ['invalid'] });
77102
});
78103
});

components/__tests__/jschema/jschema_adapter.test.js

Lines changed: 253 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,89 @@
11
import { describe, it, expect } from 'vitest';
2-
import { adaptJsonSchema } from '../../src/lib/jschema/jschema_adapter';
2+
import { adaptJsonSchema, stripDiscriminator } from '../../src/lib/jschema/jschema_adapter';
33

44
describe('jschema_adapter', () => {
5+
it('Adapt discriminators', () => {
6+
const schema = adaptJsonSchema({
7+
$defs: {
8+
ProcessAModel: {
9+
description: 'A process model with the same parameter name as ProcessBModel.',
10+
properties: {
11+
step: {
12+
const: 'ProcessA',
13+
title: 'Step',
14+
type: 'string',
15+
description: 'A literal to identify the process type.'
16+
},
17+
parameter1: {
18+
title: 'Parameter1',
19+
type: 'number',
20+
description: 'An integer parameter in A.'
21+
}
22+
},
23+
required: ['step', 'parameter1'],
24+
title: 'ProcessAModel',
25+
type: 'object'
26+
},
27+
ProcessBModel: {
28+
description: 'B process model with the same parameter name as ProcessAModel.',
29+
properties: {
30+
step: {
31+
const: 'ProcessB',
32+
title: 'Step',
33+
type: 'string',
34+
description: 'A literal to identify the process type.'
35+
},
36+
parameter1: {
37+
title: 'Parameter1',
38+
type: 'number',
39+
description: 'An integer parameter in B.'
40+
}
41+
},
42+
required: ['step', 'parameter1'],
43+
title: 'ProcessBModel',
44+
type: 'object'
45+
}
46+
},
47+
additionalProperties: false,
48+
properties: {
49+
proc_step: {
50+
discriminator: {
51+
mapping: {
52+
ProcessA: '#/$defs/ProcessAModel',
53+
ProcessB: '#/$defs/ProcessBModel'
54+
},
55+
propertyName: 'step'
56+
},
57+
oneOf: [
58+
{
59+
$ref: '#/$defs/ProcessAModel'
60+
},
61+
{
62+
$ref: '#/$defs/ProcessBModel'
63+
}
64+
],
65+
title: 'Proc Step',
66+
description: 'The processing step to apply.'
67+
}
68+
},
69+
required: ['proc_step'],
70+
type: 'object',
71+
title: 'ProcessTask'
72+
});
73+
74+
const property = /** @type {any} */ (schema.properties.proc_step);
75+
76+
const mapping = property.discriminator.mapping;
77+
expect(mapping.ProcessA).eq(0);
78+
expect(mapping.ProcessB).eq(1);
79+
80+
const properties = property.oneOf;
81+
expect(properties[0].title).eq('ProcessAModel');
82+
expect(properties[1].title).eq('ProcessBModel');
83+
});
84+
585
it('Replace references', () => {
6-
const jschema = {
86+
const schema = adaptJsonSchema({
787
type: 'object',
888
properties: {
989
simple: {
@@ -37,22 +117,40 @@ describe('jschema_adapter', () => {
37117
}
38118
}
39119
}
40-
};
120+
});
121+
122+
const simpleProperty =
123+
/** @type {import('../../src/lib/types/jschema').JSONSchemaStringProperty} */ (
124+
schema.properties.simple
125+
);
126+
const referenced1Property =
127+
/** @type {import('../../src/lib/types/jschema').JSONSchemaObjectProperty} */ (
128+
schema.properties.referenced1
129+
);
130+
const referenced2Property =
131+
/** @type {import('../../src/lib/types/jschema').JSONSchemaArrayProperty} */ (
132+
referenced1Property.properties.referenced2
133+
);
41134

42-
const result = adaptJsonSchema(jschema);
135+
const referenced2PropertyItems =
136+
/** @type {import('../../src/lib/types/jschema').JSONSchemaObjectProperty} */ (
137+
referenced2Property.items
138+
);
139+
const ref2StringProperty =
140+
/** @type {import('../../src/lib/types/jschema').JSONSchemaStringProperty} */ (
141+
referenced2PropertyItems.properties.ref2string
142+
);
43143

44-
expect(result.properties.simple.type).eq('string');
45-
expect(result.properties.referenced1.title).eq('Referenced 1');
46-
expect(result.properties.referenced1.type).eq('object');
47-
expect(result.properties.referenced1.properties.referenced2.type).eq('array');
48-
expect(result.properties.referenced1.properties.referenced2.items.type).eq('object');
49-
expect(
50-
result.properties.referenced1.properties.referenced2.items.properties.ref2string.type
51-
).eq('string');
144+
expect(simpleProperty.type).eq('string');
145+
expect(referenced1Property.title).eq('Referenced 1');
146+
expect(referenced1Property.type).eq('object');
147+
expect(referenced2Property.type).eq('array');
148+
expect(referenced2PropertyItems.type).eq('object');
149+
expect(ref2StringProperty.type).eq('string');
52150
});
53151

54152
it('Merge allOf', () => {
55-
const jschema = {
153+
const schema = adaptJsonSchema({
56154
type: 'object',
57155
properties: {
58156
allOfNumber: {
@@ -80,16 +178,14 @@ describe('jschema_adapter', () => {
80178
]
81179
}
82180
}
83-
};
84-
85-
const result = adaptJsonSchema(jschema);
181+
});
86182

87-
expect(result.properties.allOfNumber.type).eq('number');
88-
expect(result.properties.allOfNumber.title).eq('My Number');
89-
expect(result.properties.allOfNumber.minimum).eq(5);
90-
expect(result.properties.allOfNumber.maximum).eq(10);
91-
expect(result.properties.allOfEnum.enum).deep.eq(['A', 'B']);
92-
expect(result.properties.allOfObject).deep.eq({
183+
expect(schema.properties.allOfNumber.type).eq('number');
184+
expect(schema.properties.allOfNumber.title).eq('My Number');
185+
expect(schema.properties.allOfNumber.minimum).eq(5);
186+
expect(schema.properties.allOfNumber.maximum).eq(10);
187+
expect(schema.properties.allOfEnum.enum).deep.eq(['A', 'B']);
188+
expect(schema.properties.allOfObject).deep.eq({
93189
type: 'object',
94190
default: { k1: 'v1', k2: 'v2' },
95191
properties: {
@@ -109,7 +205,140 @@ describe('jschema_adapter', () => {
109205
}
110206
}
111207
};
112-
const result = adaptJsonSchema(jschema);
113-
expect(result.properties.testProp.default).toEqual(null);
208+
const schema = adaptJsonSchema(jschema);
209+
expect(schema.properties.testProp.default).toEqual(null);
210+
});
211+
212+
it('Strip discriminator', () => {
213+
const cleanJschema = stripDiscriminator({
214+
$defs: {
215+
ProcessAModel: {
216+
description: 'A process model with the same parameter name as ProcessBModel.',
217+
properties: {
218+
step: {
219+
const: 'ProcessA',
220+
title: 'Step',
221+
type: 'string',
222+
description: 'A literal to identify the process type.'
223+
},
224+
parameter1: {
225+
title: 'Parameter1',
226+
type: 'number',
227+
description: 'An integer parameter in A.'
228+
}
229+
},
230+
required: ['step', 'parameter1'],
231+
title: 'ProcessAModel',
232+
type: 'object'
233+
},
234+
ProcessBModel: {
235+
description: 'B process model with the same parameter name as ProcessAModel.',
236+
properties: {
237+
step: {
238+
const: 'ProcessB',
239+
title: 'Step',
240+
type: 'string',
241+
description: 'A literal to identify the process type.'
242+
},
243+
parameter1: {
244+
title: 'Parameter1',
245+
type: 'number',
246+
description: 'An integer parameter in B.'
247+
}
248+
},
249+
required: ['step', 'parameter1'],
250+
title: 'ProcessBModel',
251+
type: 'object'
252+
}
253+
},
254+
additionalProperties: false,
255+
properties: {
256+
proc_step: {
257+
discriminator: {
258+
mapping: {
259+
ProcessA: '#/$defs/ProcessAModel',
260+
ProcessB: '#/$defs/ProcessBModel'
261+
},
262+
propertyName: 'step'
263+
},
264+
oneOf: [
265+
{
266+
$ref: '#/$defs/ProcessAModel'
267+
},
268+
{
269+
$ref: '#/$defs/ProcessBModel'
270+
}
271+
],
272+
title: 'Proc Step',
273+
description: 'The processing step to apply.'
274+
}
275+
},
276+
required: ['proc_step'],
277+
type: 'object',
278+
title: 'ProcessTask'
279+
});
280+
281+
expect(JSON.stringify(cleanJschema)).eq(
282+
JSON.stringify({
283+
$defs: {
284+
ProcessAModel: {
285+
description: 'A process model with the same parameter name as ProcessBModel.',
286+
properties: {
287+
step: {
288+
const: 'ProcessA',
289+
title: 'Step',
290+
type: 'string',
291+
description: 'A literal to identify the process type.'
292+
},
293+
parameter1: {
294+
title: 'Parameter1',
295+
type: 'number',
296+
description: 'An integer parameter in A.'
297+
}
298+
},
299+
required: ['step', 'parameter1'],
300+
title: 'ProcessAModel',
301+
type: 'object'
302+
},
303+
ProcessBModel: {
304+
description: 'B process model with the same parameter name as ProcessAModel.',
305+
properties: {
306+
step: {
307+
const: 'ProcessB',
308+
title: 'Step',
309+
type: 'string',
310+
description: 'A literal to identify the process type.'
311+
},
312+
parameter1: {
313+
title: 'Parameter1',
314+
type: 'number',
315+
description: 'An integer parameter in B.'
316+
}
317+
},
318+
required: ['step', 'parameter1'],
319+
title: 'ProcessBModel',
320+
type: 'object'
321+
}
322+
},
323+
additionalProperties: false,
324+
properties: {
325+
proc_step: {
326+
oneOf: [
327+
{
328+
$ref: '#/$defs/ProcessAModel'
329+
},
330+
{
331+
$ref: '#/$defs/ProcessBModel'
332+
}
333+
],
334+
title: 'Proc Step',
335+
description: 'The processing step to apply.'
336+
}
337+
},
338+
required: ['proc_step'],
339+
type: 'object',
340+
title: 'ProcessTask'
341+
})
342+
);
114343
});
115344
});

components/__tests__/jschema/properties/allOf.test.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ describe('allOf properties', () => {
3939
'pydantic_v1'
4040
);
4141

42-
const dropdown = screen.getByRole('combobox');
42+
const dropdown = /** @type {HTMLSelectElement} */ (screen.getByRole('combobox'));
4343
expect(dropdown.options.length).eq(3);
4444
expect(dropdown.options[0].text).eq('Select...');
4545
expect(dropdown.options[1].text).eq('A');
@@ -97,7 +97,7 @@ describe('allOf properties', () => {
9797

9898
const dropdowns = screen.getAllByRole('combobox');
9999
expect(dropdowns.length).eq(2);
100-
const [dropdown1, dropdown2] = dropdowns;
100+
const [dropdown1, dropdown2] = /** @type {HTMLSelectElement[]} */ (dropdowns);
101101
expect(dropdown1.options.length).eq(3);
102102
expect(dropdown1.options[0].text).eq('Select...');
103103
expect(dropdown1.options[1].text).eq('A');

0 commit comments

Comments
 (0)