Skip to content

Commit a36b570

Browse files
committed
Close #58: Support anyOf/oneOf/allOf at top level
1 parent 845a883 commit a36b570

File tree

8 files changed

+562
-130
lines changed

8 files changed

+562
-130
lines changed

src/data.js

Lines changed: 235 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import {normalizeKeyword, getKeyword} from './util';
1+
import {normalizeKeyword, getKeyword, getSchemaType, actualType,
2+
isEqualset, isSubset} from './util';
23
import {FILLER} from './constants';
34

45

@@ -115,16 +116,48 @@ export function getBlankArray(schema, getRef) {
115116
}
116117

117118

119+
export function getBlankAllOf(schema, getRef) {
120+
// currently, we support allOf only inside an object
121+
return getBlankObject(schema, getRef);
122+
}
123+
124+
125+
export function getBlankOneOf(schema, getRef) {
126+
// for blank data, we always return the first option
127+
let nextSchema = schema.oneOf[0];
128+
129+
let type = getSchemaType(nextSchema);
130+
131+
return getBlankData(nextSchema, getRef);
132+
}
133+
134+
135+
export function getBlankAnyOf(schema, getRef) {
136+
// for blank data, we always return the first option
137+
let nextSchema = schema.anyOf[0];
138+
139+
let type = getSchemaType(nextSchema);
140+
141+
return getBlankData(nextSchema, getRef);
142+
}
143+
144+
118145
export function getBlankData(schema, getRef) {
119146
if (schema.hasOwnProperty('$ref'))
120147
schema = getRef(schema['$ref']);
121148

122-
let type = normalizeKeyword(schema.type);
149+
let type = getSchemaType(schema);
123150

124151
if (type === 'array')
125152
return getBlankArray(schema, getRef);
126153
else if (type === 'object')
127154
return getBlankObject(schema, getRef);
155+
else if (type === 'allOf')
156+
return getBlankAllOf(schema, getRef);
157+
else if (type === 'oneOf')
158+
return getBlankOneOf(schema, getRef);
159+
else if (type === 'anyOf')
160+
return getBlankAnyOf(schema, getRef);
128161
else if (type === 'boolean')
129162
return schema.default === false ? false : (schema.default || null);
130163
else if (type === 'integer' || type === 'number')
@@ -135,6 +168,9 @@ export function getBlankData(schema, getRef) {
135168

136169

137170
function getSyncedArray(data, schema, getRef) {
171+
if (actualType(data) !== 'array')
172+
throw new Error("Schema expected an 'array' but the data type was '" + actualType(data) + "'");
173+
138174
let newData = JSON.parse(JSON.stringify(data));
139175

140176
if (schema.items.hasOwnProperty('$ref')) {
@@ -177,14 +213,20 @@ function getSyncedArray(data, schema, getRef) {
177213

178214

179215
function getSyncedObject(data, schema, getRef) {
216+
if (actualType(data) !== 'object')
217+
throw new Error("Schema expected an 'object' but the data type was '" + actualType(data) + "'");
218+
180219
let newData = JSON.parse(JSON.stringify(data));
181220

182221
let schema_keys = getKeyword(schema, 'keys', 'properties', {});
183222

184-
185223
if (schema.hasOwnProperty('allOf')) {
186224
for (let i = 0; i < schema.allOf.length; i++) {
187-
schema_keys = {...schema_keys, ...getBlankObject(schema.allOf[i])};
225+
// ignore items in allOf which are not object
226+
if (getSchemaType(schema.allOf[i]) !== 'object')
227+
continue;
228+
229+
schema_keys = {...schema_keys, ...getKeyword(schema.allOf[i], 'properties', 'keys', {})};
188230
}
189231
}
190232

@@ -199,7 +241,7 @@ function getSyncedObject(data, schema, getRef) {
199241
if (isRef)
200242
schemaValue = getRef(schemaValue['$ref']);
201243

202-
let type = normalizeKeyword(schemaValue.type);
244+
let type = getSchemaType(schemaValue, data.key);
203245

204246
if (!data.hasOwnProperty(key)) {
205247
if (type === 'array')
@@ -235,19 +277,200 @@ function getSyncedObject(data, schema, getRef) {
235277
}
236278

237279

280+
export function getSyncedAllOf(data, schema, getRef) {
281+
// currently we only support allOf inside an object
282+
// so, we'll treat the curent schema and data to be an object
283+
284+
return getSyncedObject(data, schema, getRef);
285+
}
286+
287+
288+
export function getSyncedOneOf(data, schema, getRef) {
289+
let index = findMatchingSubschemaIndex(data, schema, getRef, 'oneOf');
290+
let subschema = schema['oneOf'][index];
291+
292+
let syncFunc = getSyncFunc(getSchemaType(subschema));
293+
294+
if (syncFunc)
295+
return syncFunc(data, subschema, getRef);
296+
297+
return data;
298+
}
299+
300+
301+
export function getSyncedAnyOf(data, schema, getRef) {
302+
let index = findMatchingSubschemaIndex(data, schema, getRef, 'anyOf');
303+
let subschema = schema['anyOf'][index];
304+
305+
let syncFunc = getSyncFunc(getSchemaType(subschema));
306+
307+
if (syncFunc)
308+
return syncFunc(data, subschema, getRef);
309+
310+
return data;
311+
}
312+
313+
238314
export function getSyncedData(data, schema, getRef) {
239315
// adds those keys to data which are in schema but not in data
240-
241316
if (schema.hasOwnProperty('$ref'))
242317
schema = getRef(schema['$ref']);
243318

244-
let type = normalizeKeyword(schema.type);
245319

246-
if (type === 'array') {
247-
return getSyncedArray(data, schema, getRef);
248-
} else if (type === 'object') {
249-
return getSyncedObject(data, schema, getRef);
250-
}
320+
let type = getSchemaType(schema);
321+
322+
let syncFunc = getSyncFunc(type);
323+
324+
if (syncFunc)
325+
return syncFunc(data, schema, getRef);
251326

252327
return data;
253328
}
329+
330+
331+
function getSyncFunc(type) {
332+
if (type === 'array')
333+
return getSyncedArray;
334+
else if (type === 'object')
335+
return getSyncedObject;
336+
else if (type === 'allOf')
337+
return getSyncedAllOf;
338+
else if (type === 'oneOf')
339+
return getSyncedOneOf;
340+
else if (type === 'anyOf')
341+
return getSyncedAnyOf;
342+
343+
return null;
344+
}
345+
346+
347+
export function findMatchingSubschemaIndex(data, schema, getRef, schemaName) {
348+
let dataType = actualType(data);
349+
let subschemas = schema[schemaName];
350+
351+
let index = null;
352+
353+
for (let i = 0; i < subschemas.length; i++) {
354+
let subschema = subschemas[i];
355+
356+
if (subschema.hasOwnProperty('$ref'))
357+
subschema = getRef(subschema['$ref']);
358+
359+
let subType = getSchemaType(subschema);
360+
361+
if (dataType === 'object') {
362+
// check if all keys match
363+
if (dataObjectMatchesSchema(data, subschema)) {
364+
index = i;
365+
break;
366+
}
367+
} else if (dataType === 'array') {
368+
// check if item types match
369+
if (dataArrayMatchesSchema(data, subschema)) {
370+
index = i;
371+
break;
372+
}
373+
} else if (dataType === subType) {
374+
index = i;
375+
break;
376+
}
377+
}
378+
379+
if (index === null) {
380+
// no exact match found
381+
// so we'll just return the first schema that matches the data type
382+
for (let i = 0; i < subschemas.length; i++) {
383+
let subschema = subschemas[i];
384+
385+
if (subschema.hasOwnProperty('$ref'))
386+
subschema = getRef(subschema['$ref']);
387+
388+
let subType = getSchemaType(subschema);
389+
390+
if (dataType === subType) {
391+
index = i;
392+
break;
393+
}
394+
}
395+
}
396+
397+
return index;
398+
}
399+
400+
export function dataObjectMatchesSchema(data, subschema) {
401+
let dataType = actualType(data);
402+
let subType = getSchemaType(subschema);
403+
404+
if (subType !== dataType)
405+
return false;
406+
407+
let subSchemaKeys = getKeyword(subschema, 'properties', 'keys', {});
408+
409+
// check if all keys in the schema are present in the data
410+
keyset1 = new Set(Object.keys(data));
411+
keyset2 = new Set(Object.keys(subSchemaKeys));
412+
413+
if (subschema.hasOwnProperty('additionalProperties')) {
414+
// subSchemaKeys must be a subset of data
415+
if (!isSubset(keyset2, keyset1))
416+
return false;
417+
} else {
418+
// subSchemaKeys must be equal to data
419+
if (!isEqualset(keyset2, keyset1))
420+
return false;
421+
}
422+
423+
for (let key in subSchemaKeys) {
424+
if (!subSchemaKeys.hasOwnProperty(key))
425+
continue;
426+
427+
if (!data.hasOwnProperty(key))
428+
return false;
429+
430+
431+
let keyType = normalizeKeyword(subSchemaKeys[key].type);
432+
let dataValueType = actualType(data[key]);
433+
434+
if (keyType === 'number' && ['number', 'integer', 'null'].indexOf(dataValueType) === -1) {
435+
return false;
436+
} else if (keyType === 'integer' && ['number', 'integer', 'null'].indexOf(dataValueType) === -1) {
437+
return false;
438+
} else if (keyType === 'boolean' && ['boolean', 'null'].indexOf(dataValueType) === -1) {
439+
return false;
440+
} else if (keyType === 'string' && dataValueType !== 'string') {
441+
return false;
442+
}
443+
}
444+
445+
// if here, all checks have passed
446+
return true;
447+
}
448+
449+
450+
export function dataArrayMatchesSchema(data, subschema) {
451+
let dataType = actualType(data);
452+
let subType = getSchemaType(subschema);
453+
454+
if (subType !== dataType)
455+
return false;
456+
457+
let itemsType = subschema.items.type; // Temporary. Nested subschemas inside array.items won't work.
458+
459+
// check each item in data conforms to array items.type
460+
for (let i = 0; i < data.length; i++) {
461+
dataValueType = actualType(data[i]);
462+
463+
if (itemsType === 'number' && ['number', 'integer', 'null'].indexOf(dataValueType) === -1) {
464+
return false;
465+
} else if (itemsType === 'integer' && ['number', 'integer', 'null'].indexOf(dataValueType) === -1) {
466+
return false;
467+
} else if (itemsType === 'boolean' && ['boolean', 'null'].indexOf(dataValueType) === -1) {
468+
return false;
469+
} else if (itemsType === 'string' && dataValueType !== 'string') {
470+
return false;
471+
}
472+
}
473+
474+
// if here, all checks have passed
475+
return true;
476+
}

src/dataValidation.js

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {normalizeKeyword, getKeyword, getKey, joinCoords} from './util';
1+
import {normalizeKeyword, getKeyword, getKey, joinCoords, getSchemaType} from './util';
22
import {JOIN_SYMBOL} from './constants';
33
import EditorState from './editorState';
44

@@ -12,7 +12,7 @@ export default function DataValidator(schema) {
1212
// can be reused for same schema
1313
this.errorMap = {};
1414

15-
let validator = this.getValidator(schema.type);
15+
let validator = this.getValidator(getSchemaType(schema));
1616

1717
if (validator)
1818
validator(this.schema, data, '');
@@ -39,6 +39,15 @@ export default function DataValidator(schema) {
3939
case 'object':
4040
func = this.validateObject;
4141
break;
42+
case 'allOf':
43+
func = this.validateAllOf;
44+
break;
45+
case 'oneOf':
46+
func = this.validateOneOf;
47+
break;
48+
case 'anyOf':
49+
func = this.validateAnyOf;
50+
break;
4251
case 'string':
4352
func = this.validateString;
4453
break;
@@ -179,6 +188,40 @@ export default function DataValidator(schema) {
179188
return;
180189
}
181190
}
191+
192+
if (schema.hasOwnProperty('allOf'))
193+
this.validateAllOf(schema, data, coords);
194+
};
195+
196+
this.validateAllOf = function(schema, data, coords) {
197+
/* Currently, we only support allOf inside object
198+
so we assume the given type to be an object.
199+
*/
200+
201+
let newSchema = {type: 'object', properties: {}};
202+
203+
// combine subschemas
204+
for (let i = 0; i < schema.allOf.length; i++) {
205+
let subschema = schema.allOf[i];
206+
207+
if (subschema.hasOwnProperty('$ref'))
208+
subschema = this.getRef(subschema.$ref);
209+
210+
let fields = getKeyword(subschema, 'properties', 'keys', {});
211+
212+
for (let field in fields)
213+
newSchema.properties[field] = fields[field];
214+
}
215+
216+
this.validateObject(newSchema, data, coords);
217+
};
218+
219+
this.validateOneOf = function(schema, data, coords) {
220+
// :TODO:
221+
};
222+
223+
this.validateAnyOf = function(schema, data, coords) {
224+
// :TODO:
182225
};
183226

184227
this.validateString = function(schema, data, coords) {

src/editorState.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export default class EditorState {
3434
data = getSyncedData(data, schema, (ref) => EditorState.getRef(ref, schema));
3535
} catch (error) {
3636
console.error("Error while creating EditorState: Schema and data structure don't match");
37-
console.error(error);
37+
throw error;
3838
}
3939
}
4040

0 commit comments

Comments
 (0)