genson-js
Version:
JSON schema generator with a possibility to merge schemas
247 lines • 8.73 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.createCompoundSchema = exports.extendSchema = exports.mergeSchemas = exports.createSchema = exports.wrapAnyOfSchema = exports.unwrapSchemas = exports.unwrapSchema = void 0;
const types_1 = require("./types");
function createSchemaFor(value, options) {
switch (typeof value) {
case 'number':
if (Number.isInteger(value)) {
return { type: types_1.ValueType.Integer };
}
return { type: types_1.ValueType.Number };
case 'boolean':
return { type: types_1.ValueType.Boolean };
case 'string':
return { type: types_1.ValueType.String };
case 'object':
if (value === null) {
return { type: types_1.ValueType.Null };
}
if (Array.isArray(value)) {
return createSchemaForArray(value, options);
}
return createSchemaForObject(value, options);
}
}
function createSchemaForArray(arr, options) {
if (arr.length === 0) {
return { type: types_1.ValueType.Array };
}
const elementSchemas = arr.map((value) => createSchemaFor(value, options));
const items = combineSchemas(elementSchemas);
return { type: types_1.ValueType.Array, items };
}
function createSchemaForObject(obj, options) {
const keys = Object.keys(obj);
if (keys.length === 0) {
return {
type: types_1.ValueType.Object,
};
}
const properties = Object.entries(obj).reduce((props, [key, val]) => {
props[key] = createSchemaFor(val, options);
return props;
}, {});
const schema = { type: types_1.ValueType.Object, properties };
if (!(options === null || options === void 0 ? void 0 : options.noRequired)) {
schema.required = keys;
}
return schema;
}
function combineSchemas(schemas, options) {
const schemasByType = {
[types_1.ValueType.Null]: [],
[types_1.ValueType.Boolean]: [],
[types_1.ValueType.Integer]: [],
[types_1.ValueType.Number]: [],
[types_1.ValueType.String]: [],
[types_1.ValueType.Array]: [],
[types_1.ValueType.Object]: [],
};
const unwrappedSchemas = unwrapSchemas(schemas);
for (const unwrappedSchema of unwrappedSchemas) {
const type = unwrappedSchema.type;
if (schemasByType[type].length === 0 || isContainerSchema(unwrappedSchema)) {
schemasByType[type].push(unwrappedSchema);
}
else {
continue;
}
}
const resultSchemasByType = {
[types_1.ValueType.Null]: schemasByType[types_1.ValueType.Null][0],
[types_1.ValueType.Boolean]: schemasByType[types_1.ValueType.Boolean][0],
[types_1.ValueType.Number]: schemasByType[types_1.ValueType.Number][0],
[types_1.ValueType.Integer]: schemasByType[types_1.ValueType.Integer][0],
[types_1.ValueType.String]: schemasByType[types_1.ValueType.String][0],
[types_1.ValueType.Array]: combineArraySchemas(schemasByType[types_1.ValueType.Array]),
[types_1.ValueType.Object]: combineObjectSchemas(schemasByType[types_1.ValueType.Object], options),
};
if (resultSchemasByType[types_1.ValueType.Number]) {
// if at least one value is float, others can be floats too
delete resultSchemasByType[types_1.ValueType.Integer];
}
const schemasFound = Object.values(resultSchemasByType).filter(Boolean);
const multiType = schemasFound.length > 1;
if (multiType) {
const wrapped = wrapAnyOfSchema({ anyOf: schemasFound });
return wrapped;
}
return schemasFound[0];
}
function combineArraySchemas(schemas) {
if (!schemas || schemas.length === 0) {
return undefined;
}
const itemSchemas = [];
for (const schema of schemas) {
if (!schema.items)
continue;
const unwrappedSchemas = unwrapSchema(schema.items);
itemSchemas.push(...unwrappedSchemas);
}
if (itemSchemas.length === 0) {
return {
type: types_1.ValueType.Array,
};
}
const items = combineSchemas(itemSchemas);
return {
type: types_1.ValueType.Array,
items,
};
}
function combineObjectSchemas(schemas, options) {
if (!schemas || schemas.length === 0) {
return undefined;
}
const allPropSchemas = schemas.map((s) => s.properties).filter(Boolean);
const schemasByProp = Object.create(null);
// const schemasByProp: Record<string, Schema[]> = {};
for (const propSchemas of allPropSchemas) {
for (const [prop, schema] of Object.entries(propSchemas)) {
if (!schemasByProp[prop]) {
schemasByProp[prop] = [];
}
const unwrappedSchemas = unwrapSchema(schema);
schemasByProp[prop].push(...unwrappedSchemas);
}
}
const properties = Object.entries(schemasByProp).reduce((props, [prop, schemas]) => {
if (schemas.length === 1) {
props[prop] = schemas[0];
}
else {
props[prop] = combineSchemas(schemas);
}
return props;
}, {});
const combinedSchema = { type: types_1.ValueType.Object };
if (Object.keys(properties).length > 0) {
combinedSchema.properties = properties;
}
if (!(options === null || options === void 0 ? void 0 : options.noRequired)) {
const required = intersection(schemas.map((s) => s.required || []));
if (required.length > 0) {
combinedSchema.required = required;
}
}
return combinedSchema;
}
function unwrapSchema(schema) {
if (!schema)
return [];
if (schema.anyOf) {
return unwrapSchemas(schema.anyOf);
}
if (Array.isArray(schema.type)) {
return schema.type.map((x) => ({ type: x }));
}
return [schema];
}
exports.unwrapSchema = unwrapSchema;
function unwrapSchemas(schemas) {
if (!schemas || schemas.length === 0)
return [];
const unwrappedSchemas = schemas.flatMap((schema) => unwrapSchema(schema));
return unwrappedSchemas;
}
exports.unwrapSchemas = unwrapSchemas;
function wrapAnyOfSchema(schema) {
const simpleSchemas = [];
const complexSchemas = [];
for (const subSchema of schema.anyOf) {
if (Array.isArray(subSchema.type)) {
simpleSchemas.push(...subSchema.type);
}
else if (isSimpleSchema(subSchema)) {
simpleSchemas.push(subSchema.type);
}
else {
complexSchemas.push(subSchema);
}
}
if (complexSchemas.length === 0) {
return { type: simpleSchemas };
}
const anyOf = [];
if (simpleSchemas.length > 0) {
anyOf.push({ type: simpleSchemas.length > 1 ? simpleSchemas : simpleSchemas[0] });
}
anyOf.push(...complexSchemas);
return { anyOf };
}
exports.wrapAnyOfSchema = wrapAnyOfSchema;
function intersection(valuesArr) {
if (valuesArr.length === 0)
return [];
const arrays = valuesArr.filter(Array.isArray);
const counter = {};
for (const arr of arrays) {
for (const val of arr) {
if (!counter[val]) {
counter[val] = 1;
}
else {
counter[val]++;
}
}
}
const result = Object.entries(counter)
.filter(([_, value]) => value === arrays.length)
.map(([key]) => key);
return result;
}
function isSimpleSchema(schema) {
const keys = Object.keys(schema);
return keys.length === 1 && keys[0] === 'type';
}
function isContainerSchema(schema) {
const type = schema.type;
return type === types_1.ValueType.Array || type === types_1.ValueType.Object;
}
// FACADE
function createSchema(value, options) {
if (typeof value === 'undefined')
value = null;
const clone = JSON.parse(JSON.stringify(value));
return createSchemaFor(clone, options);
}
exports.createSchema = createSchema;
function mergeSchemas(schemas, options) {
const mergedSchema = combineSchemas(schemas, options);
return mergedSchema;
}
exports.mergeSchemas = mergeSchemas;
function extendSchema(schema, value, options) {
const valueSchema = createSchema(value, options);
const mergedSchema = combineSchemas([schema, valueSchema], options);
return mergedSchema;
}
exports.extendSchema = extendSchema;
function createCompoundSchema(values, options) {
const schemas = values.map((value) => createSchema(value, options));
return mergeSchemas(schemas, options);
}
exports.createCompoundSchema = createCompoundSchema;
//# sourceMappingURL=schema-builder.js.map