Skip to content

Commit d08ecdb

Browse files
committed
Validate param ranges with union and intersection types
1 parent 2be90cc commit d08ecdb

File tree

3 files changed

+217
-23
lines changed

3 files changed

+217
-23
lines changed

components/context.jsonld

+16
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,22 @@
6161
"undefined": {
6262
"@id": "oo:isUndefined"
6363
},
64+
"ParameterRange": {
65+
"@id": "oo:ParameterRange"
66+
},
67+
"ParameterRangeComposed": {
68+
"@id": "oo:ParameterRangeComposed"
69+
},
70+
"ParameterRangeComposedUnion": {
71+
"@id": "oo:ParameterRangeComposedUnion"
72+
},
73+
"ParameterRangeComposedIntersection": {
74+
"@id": "oo:ParameterRangeComposedIntersection"
75+
},
76+
"parameterRangeComposedChildren": {
77+
"@id": "oo:parameterRangeComposedChildren",
78+
"@type": "@id"
79+
},
6480

6581
"rdfs": "http://www.w3.org/2000/01/rdf-schema#",
6682
"comment": {

lib/preprocess/parameterproperty/ParameterPropertyHandlerRange.ts

+66-23
Original file line numberDiff line numberDiff line change
@@ -33,69 +33,112 @@ export class ParameterPropertyHandlerRange implements IParameterPropertyHandler
3333
* @param param The parameter.
3434
*/
3535
public captureType(value: Resource, param: Resource): Resource {
36+
if (this.hasParamValueValidType(value, param, param.property.range)) {
37+
return value;
38+
}
39+
this.throwIncorrectTypeError(value, param);
40+
}
41+
42+
/**
43+
* Apply the given datatype to the given literal.
44+
* Checks if the datatype is correct and casts to the correct js type.
45+
* Will throw an error if the type has an invalid value.
46+
* Will be ignored if the value is not a literal or the type is not recognized.
47+
* @param value The value.
48+
* @param param The parameter.
49+
* @param paramRange The parameter's range.
50+
*/
51+
public hasParamValueValidType(value: Resource, param: Resource, paramRange: Resource): boolean {
3652
if (value.type === 'Literal') {
3753
let parsed;
38-
switch (param.property.range.value) {
54+
switch (paramRange.value) {
55+
case IRIS_XSD.string:
56+
return true;
3957
case IRIS_XSD.boolean:
4058
if (value.value === 'true') {
4159
(<any>value.term).valueRaw = true;
4260
} else if (value.value === 'false') {
4361
(<any>value.term).valueRaw = false;
4462
} else {
45-
this.throwIncorrectTypeError(value, param);
63+
return false;
4664
}
47-
break;
65+
return true;
4866
case IRIS_XSD.integer:
4967
case IRIS_XSD.number:
5068
case IRIS_XSD.int:
5169
case IRIS_XSD.byte:
5270
case IRIS_XSD.long:
5371
parsed = Number.parseInt(value.value, 10);
5472
if (Number.isNaN(parsed)) {
55-
this.throwIncorrectTypeError(value, param);
56-
} else {
57-
// ParseInt also parses floats to ints!
58-
if (String(parsed) !== value.value) {
59-
this.throwIncorrectTypeError(value, param);
60-
}
61-
(<any>value.term).valueRaw = parsed;
73+
return false;
74+
}
75+
// ParseInt also parses floats to ints!
76+
if (String(parsed) !== value.value) {
77+
return false;
6278
}
63-
break;
79+
(<any>value.term).valueRaw = parsed;
80+
return true;
6481
case IRIS_XSD.float:
6582
case IRIS_XSD.decimal:
6683
case IRIS_XSD.double:
6784
parsed = Number.parseFloat(value.value);
6885
if (Number.isNaN(parsed)) {
69-
this.throwIncorrectTypeError(value, param);
70-
} else {
71-
(<any>value.term).valueRaw = parsed;
86+
return false;
7287
}
73-
break;
88+
(<any>value.term).valueRaw = parsed;
89+
return true;
7490
case IRIS_RDF.JSON:
7591
try {
7692
parsed = JSON.parse(value.value);
7793
(<any>value.term).valueRaw = parsed;
7894
} catch {
79-
this.throwIncorrectTypeError(value, param);
95+
return false;
8096
}
81-
break;
97+
return true;
8298
}
83-
} else if (!value.isA('Variable') && param.property.range && !value.isA(param.property.range.term)) {
99+
}
100+
101+
if (!value.isA('Variable') && paramRange && !value.isA(paramRange.term)) {
102+
// Check if the param type is a composed type
103+
if (paramRange.isA('ParameterRangeComposedUnion')) {
104+
return paramRange.properties.parameterRangeComposedChildren
105+
.some(child => this.hasParamValueValidType(value, param, child));
106+
}
107+
if (paramRange.isA('ParameterRangeComposedIntersection')) {
108+
return paramRange.properties.parameterRangeComposedChildren
109+
.every(child => this.hasParamValueValidType(value, param, child));
110+
}
111+
84112
// Check if this param defines a field with sub-params
85-
if (param.property.range.properties.parameters.length > 0) {
113+
if (paramRange.properties.parameters.length > 0) {
86114
// TODO: Add support for type-checking nested fields with collectEntries
87115
} else {
88-
this.throwIncorrectTypeError(value, param);
116+
return false;
89117
}
90118
}
91-
return value;
119+
120+
return true;
92121
}
93122

94-
protected throwIncorrectTypeError(value: Resource, parameter: Resource): void {
123+
protected throwIncorrectTypeError(value: Resource, parameter: Resource): never {
95124
const withTypes = value.properties.types.length > 0 ? ` with types "${value.properties.types.map(resource => resource.value)}"` : '';
96-
throw new ErrorResourcesContext(`The value "${value.value}"${withTypes} for parameter "${parameter.value}" is not of required range type "${parameter.property.range.value}"`, {
125+
throw new ErrorResourcesContext(`The value "${value.value}"${withTypes} for parameter "${parameter.value}" is not of required range type "${this.rangeToDisplayString(parameter.property.range)}"`, {
97126
value,
98127
parameter,
99128
});
100129
}
130+
131+
protected rangeToDisplayString(paramRange: Resource): string {
132+
if (paramRange.isA('ParameterRangeComposedUnion')) {
133+
return paramRange.properties.parameterRangeComposedChildren
134+
.map(child => this.rangeToDisplayString(child))
135+
.join(' | ');
136+
}
137+
if (paramRange.isA('ParameterRangeComposedIntersection')) {
138+
return paramRange.properties.parameterRangeComposedChildren
139+
.map(child => this.rangeToDisplayString(child))
140+
.join(' & ');
141+
}
142+
return paramRange.value;
143+
}
101144
}

test/unit/preprocess/parameterproperty/ParameterPropertyHandlerRange-test.ts

+135
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,141 @@ describe('ParameterPropertyHandlerRange', () => {
296296
}),
297297
)).toBeTruthy();
298298
});
299+
300+
it('should handle union types with all valid types', () => {
301+
expect(handler.captureType(
302+
objectLoader.createCompactedResource({
303+
'@id': 'ex:abc',
304+
'@type': [ 'ex:SomeType1', 'ex:SomeType2' ],
305+
}),
306+
objectLoader.createCompactedResource({
307+
range: {
308+
'@type': 'ParameterRangeComposedUnion',
309+
parameterRangeComposedChildren: [
310+
{
311+
'@id': 'ex:SomeType1',
312+
},
313+
{
314+
'@id': 'ex:SomeType2',
315+
},
316+
],
317+
},
318+
}),
319+
)).toBeTruthy();
320+
});
321+
322+
it('should handle union types with one valid type', () => {
323+
expect(handler.captureType(
324+
objectLoader.createCompactedResource({
325+
'@id': 'ex:abc',
326+
'@type': 'ex:SomeType',
327+
}),
328+
objectLoader.createCompactedResource({
329+
range: {
330+
'@type': 'ParameterRangeComposedUnion',
331+
parameterRangeComposedChildren: [
332+
{
333+
'@id': 'ex:SomeTypeInvalid',
334+
},
335+
{
336+
'@id': 'ex:SomeType',
337+
},
338+
],
339+
},
340+
}),
341+
)).toBeTruthy();
342+
});
343+
344+
it('should throw on union types with no valid type', () => {
345+
expect(() => handler.captureType(
346+
objectLoader.createCompactedResource({
347+
'@id': 'ex:abc',
348+
'@type': 'ex:SomeType',
349+
}),
350+
objectLoader.createCompactedResource({
351+
range: {
352+
'@type': 'ParameterRangeComposedUnion',
353+
parameterRangeComposedChildren: [
354+
{
355+
'@id': 'ex:SomeTypeInvalid1',
356+
},
357+
{
358+
'@id': 'ex:SomeTypeInvalid2',
359+
},
360+
],
361+
},
362+
}),
363+
// eslint-disable-next-line max-len
364+
)).toThrow(/^The value "ex:abc" with types "ex:SomeType" for parameter ".*" is not of required range type "ex:SomeTypeInvalid1 \| ex:SomeTypeInvalid2"/u);
365+
});
366+
367+
it('should handle intersection types with all valid types', () => {
368+
expect(handler.captureType(
369+
objectLoader.createCompactedResource({
370+
'@id': 'ex:abc',
371+
'@type': [ 'ex:SomeType1', 'ex:SomeType2' ],
372+
}),
373+
objectLoader.createCompactedResource({
374+
range: {
375+
'@type': 'ParameterRangeComposedIntersection',
376+
parameterRangeComposedChildren: [
377+
{
378+
'@id': 'ex:SomeType1',
379+
},
380+
{
381+
'@id': 'ex:SomeType2',
382+
},
383+
],
384+
},
385+
}),
386+
)).toBeTruthy();
387+
});
388+
389+
it('should throw on intersection types with one valid type', () => {
390+
expect(() => handler.captureType(
391+
objectLoader.createCompactedResource({
392+
'@id': 'ex:abc',
393+
'@type': 'ex:SomeType',
394+
}),
395+
objectLoader.createCompactedResource({
396+
range: {
397+
'@type': 'ParameterRangeComposedIntersection',
398+
parameterRangeComposedChildren: [
399+
{
400+
'@id': 'ex:SomeType1',
401+
},
402+
{
403+
'@id': 'ex:SomeType2',
404+
},
405+
],
406+
},
407+
}),
408+
// eslint-disable-next-line max-len
409+
)).toThrow(/^The value "ex:abc" with types "ex:SomeType" for parameter ".*" is not of required range type "ex:SomeType1 & ex:SomeType2"/u);
410+
});
411+
412+
it('should throw on intersection types with no valid type', () => {
413+
expect(() => handler.captureType(
414+
objectLoader.createCompactedResource({
415+
'@id': 'ex:abc',
416+
'@type': 'ex:SomeType',
417+
}),
418+
objectLoader.createCompactedResource({
419+
range: {
420+
'@type': 'ParameterRangeComposedIntersection',
421+
parameterRangeComposedChildren: [
422+
{
423+
'@id': 'ex:SomeTypeInvalid1',
424+
},
425+
{
426+
'@id': 'ex:SomeTypeInvalid2',
427+
},
428+
],
429+
},
430+
}),
431+
// eslint-disable-next-line max-len
432+
)).toThrow(/^The value "ex:abc" with types "ex:SomeType" for parameter ".*" is not of required range type "ex:SomeTypeInvalid1 & ex:SomeTypeInvalid2"/u);
433+
});
299434
});
300435
});
301436
});

0 commit comments

Comments
 (0)