Skip to content

Commit

Permalink
Merge pull request #1 from Exabyte-io/feature/SOF-3680
Browse files Browse the repository at this point in the history
Feature/SOF-3680: JSONSchema to SimpleSchema converter
  • Loading branch information
timurbazhirov authored Jul 9, 2022
2 parents 0ec87c1 + c46e242 commit 96a1df0
Show file tree
Hide file tree
Showing 12 changed files with 6,735 additions and 5 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@ node_modules/
.husky/
.nyc_output/
.idea/

**.DS_Store
5,441 changes: 5,441 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

11 changes: 8 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"type": "git",
"url": "https://github.com/Exabyte-io/json-to-simpl-schema.git"
},
"main": "dist/index.js",
"main": "dist/main.js",
"files": [
"/dist",
"/src",
Expand Down Expand Up @@ -44,11 +44,17 @@
"eslint-plugin-jsdoc": "37.1.0",
"eslint-plugin-jsx-a11y": "6.5.1",
"eslint-plugin-prettier": "4.0.0",
"eslint-plugin-react": "^7.30.1",
"eslint-plugin-simple-import-sort": "7.0.0",
"husky": "^7.0.4",
"lint-staged": "^12.1.2",
"mocha": "^9.1.3",
"nyc": "^15.1.0"
"nyc": "^15.1.0",
"prettier": "^2.7.1",
"simpl-schema": "git+https://github.com/Exabyte-io/simpl-schema.git#f79a487e365a492ad63336f769f8a356126a8745"
},
"peerDependencies": {
"simpl-schema": "git+https://github.com/Exabyte-io/simpl-schema.git#f79a487e365a492ad63336f769f8a356126a8745"
},
"engines": {
"node": ">=12.0.0"
Expand All @@ -58,4 +64,3 @@
"*.{js,css}": "prettier --write"
}
}

95 changes: 95 additions & 0 deletions src/JsonToSimpleSchema.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import SimpleSchema from "simpl-schema";

// import SimpleSchema from "./simpl-schema/package/dist/main";
import {
convertAnyOfToOneOf,
getAllowedValuesOption,
getBlackboxOption,
getJsonSchemaProperties,
getOptionalOption,
getPrimitivePropertyType,
getRegExOption,
translateOptions,
} from "./utils";

export default class JsonToSimpleSchema {
constructor(jsonSchema) {
this.jsonSchema = jsonSchema;
}

toSimpleSchema() {
const properties = getJsonSchemaProperties(this.jsonSchema);

const simpleSchemaEntries = Object.entries(properties).reduce(
(accumulatedEntries, [propertyName, jsonProperty]) => {
const simpleSchemaProperty = {
...JsonToSimpleSchema.getSimpleSchemaTypeOption(jsonProperty),
...getOptionalOption(propertyName, this.jsonSchema),
...getBlackboxOption(jsonProperty),
...getAllowedValuesOption(jsonProperty),
...getRegExOption(jsonProperty),
...translateOptions(jsonProperty),
};

const propertyEntry = [propertyName, simpleSchemaProperty];
const arrayEntry =
simpleSchemaProperty.type === Array
? [JsonToSimpleSchema.getArrayEntry(propertyName, jsonProperty)]
: [];

return accumulatedEntries.concat([propertyEntry, ...arrayEntry]);
},
[],
);

return new SimpleSchema(Object.fromEntries(simpleSchemaEntries));
}

static getSimpleSchemaTypeOption(jsonProperty) {
const oneOfSchemas = jsonProperty.anyOf
? convertAnyOfToOneOf(jsonProperty.anyOf)
: jsonProperty.oneOf;

if (oneOfSchemas) {
const schemas = oneOfSchemas.map((schema) => {
const primitiveType = getPrimitivePropertyType(schema);

if (primitiveType === Object) {
const { oneOf, anyOf, ...restJsonProperty } = jsonProperty;
const baseSchema = new JsonToSimpleSchema(restJsonProperty).toSimpleSchema();
const oneOfSchema = new JsonToSimpleSchema(schema).toSimpleSchema();

return oneOfSchema.extend(baseSchema);
}

return primitiveType;
});

return { type: SimpleSchema.oneOf(...schemas) };
}

const primitiveType = getPrimitivePropertyType(jsonProperty);

const typeOption =
primitiveType === Object &&
jsonProperty.properties &&
!jsonProperty.additionalProperties
? new JsonToSimpleSchema(jsonProperty).toSimpleSchema()
: primitiveType;

return { type: typeOption };
}

static getArrayEntry(propertyName, jsonProperty) {
return [
`${propertyName}.$`,
{
...JsonToSimpleSchema.getSimpleSchemaTypeOption(jsonProperty.items),
...getBlackboxOption(jsonProperty.items),
...getAllowedValuesOption(jsonProperty.items),
...getRegExOption(jsonProperty.items),
...translateOptions(jsonProperty.items),
},
];
}
}
1 change: 0 additions & 1 deletion src/index.js

This file was deleted.

3 changes: 3 additions & 0 deletions src/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import JsonToSimpleSchema from "./JsonToSimpleSchema";

export default JsonToSimpleSchema;
182 changes: 182 additions & 0 deletions src/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
import SimpleSchema from "simpl-schema";

/**
*
* There's no native implementation of AnyOf in SimpleSchema, but there is an implementation of OneOf
* In general, all AnyOf are a special case OneOf and any AnyOf could be converted to OneOf
* Example:
* {
* anyOf: [
* { field1: { type: string } },
* { field2: { type: string } }
* ]
* }
* is equivalent to
* {
* oneOf: [
* { field1: { type: string } },
* { field2: { type: string } },
* {
* allOf: [
* { field1: { type: string } },
* { field2: { type: string } },
* ]
* }
* ]
* }
* Remove this function in case native support of AnyOf is implemented
*/
export function convertAnyOfToOneOf(anyOf) {
const oneOf = anyOf.reduce((combinations, schema) => {
const result = combinations.reduce(
(tempArr, cur) => [...tempArr, [...cur, schema]],
[[schema]],
);
return [...combinations, ...result];
}, []);

return oneOf.map((schemas) => ({ allOf: schemas }));
}

export function getJsonSchemaProperties(schema) {
if (!schema) {
return {};
}

/**
* SimpleSchema does not support oneOf at the root level at the moment.
* But there are a number of schemas that refers to only one single schema in oneOf/anyOf statement and this case can be handled
*/
const { allOf = [], oneOf = [], anyOf = [] } = schema;

const allOfProps = {
...Object.assign({}, ...allOf.map(getJsonSchemaProperties).flat()),
...getJsonSchemaProperties(oneOf[0]),
...getJsonSchemaProperties(anyOf[0]),
};

const schemaProps = schema.properties || {};

return { ...allOfProps, ...schemaProps };
}

export function getSchemaRequiredProperties(schema) {
if (!schema) {
return [];
}

const { allOf = [], oneOf = [], anyOf = [] } = schema;

const allOfRequiredProperties = [
...allOf.map(getSchemaRequiredProperties).flat(),
...getSchemaRequiredProperties(oneOf[0]),
...getSchemaRequiredProperties(anyOf[0]),
];

const schemaRequiredProperties = schema.required || [];

return allOfRequiredProperties.concat(schemaRequiredProperties);
}

export function getOptionalOption(propertyName, schema) {
const requiredProperties = getSchemaRequiredProperties(schema);

return { optional: !requiredProperties.includes(propertyName) };
}

export function getBlackboxOption(jsonProperty) {
if (jsonProperty.additionalProperties !== undefined) {
return { blackbox: jsonProperty.additionalProperties };
}

return { blackbox: jsonProperty.type === "object" && !jsonProperty.properties };
}

export function getAllowedValuesOption(jsonProperty) {
if (jsonProperty.enum) {
return { allowedValues: jsonProperty.enum.filter((item) => item !== null) };
}

return {};
}

export function getRegExOption(jsonProperty) {
const { pattern, format } = jsonProperty;

if (pattern) {
return { regEx: new RegExp(pattern) };
}

switch (format) {
case "email":
return { regEx: SimpleSchema.RegEx.Email };
case "host-name":
case "hostname":
return { regEx: SimpleSchema.RegEx.Domain };
case "ipv4":
return { regEx: SimpleSchema.RegEx.IPv4 };
case "ipv6":
return { regEx: SimpleSchema.RegEx.IPv6 };
default:
return {};
}
}

export function translateOptions(jsonProperty) {
const translationMap = {
title: { key: "label" },
minimum: { key: "min", type: Number },
maximum: { key: "max", type: Number },
exclusiveMinimum: { key: "exclusiveMin", type: Boolean },
exclusiveMaximum: { key: "exclusiveMax", type: Boolean },
minLength: { key: "min", type: Number },
maxLength: { key: "max", type: Number },
minItems: { key: "minCount", type: Number },
maxItems: { key: "maxCount", type: Number },
default: { key: "defaultValue" },
};

const entries = Object.entries(translationMap)
.map(([optionName, { key: mappedKey, type }]) => {
if (jsonProperty[optionName]) {
const mappedValue = type
? type(jsonProperty[optionName])
: jsonProperty[optionName];

return [mappedKey, mappedValue];
}
return null;
})
.filter((entry) => Array.isArray(entry));

return Object.fromEntries(entries);
}

export function getPrimitivePropertyType(jsonProperty) {
const { properties, allOf, oneOf, anyOf, items, type } = jsonProperty;

if (!type) {
if (properties || allOf || oneOf || anyOf) {
return Object;
}
if (items) {
return Array;
}
return String;
}

switch (type) {
case "integer":
return SimpleSchema.Integer;
case "number":
return Number;
case "boolean":
return Boolean;
case "object":
return Object;
case "array":
return Array;
default:
return ["date-time", "date"].includes(jsonProperty.format) ? Date : String;
}
}
Loading

0 comments on commit 96a1df0

Please sign in to comment.