diff --git a/README.md b/README.md index bcf2be5..5b4dc17 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,8 @@ +# Disclamer + +this repository is based on https://github.com/aspecto-io/genson-js +contains some minor tweaks that allow use of `description` tag and make `required` collection more flexible + # genson-js ![Build](https://github.com/aspecto-io/genson-js/workflows/Build/badge.svg) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com) [![TypeScript](https://badgen.net/npm/types/env-var)](http://www.typescriptlang.org/) [![NPM version](https://img.shields.io/npm/v/genson-js.svg)](https://www.npmjs.com/package/genson-js) diff --git a/src/schema-builder.ts b/src/schema-builder.ts index 6510d3b..e7a8b70 100644 --- a/src/schema-builder.ts +++ b/src/schema-builder.ts @@ -1,52 +1,83 @@ import { ValueType, Schema, SchemaGenOptions } from './types'; -function createSchemaFor(value: any, options?: SchemaGenOptions): Schema { +function createSchemaFor(value: any, options?: SchemaGenOptions, description? : string, enums? : string[]): Schema { + let additions = {}; + if(enums){ + additions["enum"] = enums; + } + if (description) { + additions["description"] = description; + } switch (typeof value) { case 'number': if (Number.isInteger(value)) { - return { type: ValueType.Integer }; + return { type: ValueType.Integer, ...additions }; } - return { type: ValueType.Number }; + return { type: ValueType.Number, ...additions }; case 'boolean': - return { type: ValueType.Boolean }; + return { type: ValueType.Boolean, ...additions }; case 'string': - return { type: ValueType.String }; + return { type: ValueType.String, ...additions }; case 'object': if (value === null) { - return { type: ValueType.Null }; + return { type: ValueType.Null, ...additions }; } if (Array.isArray(value)) { - return createSchemaForArray(value, options); + return createSchemaForArray(value, options, description); } - return createSchemaForObject(value, options); + return createSchemaForObject(value, options, description); } } -function createSchemaForArray(arr: Array, options?: SchemaGenOptions): Schema { +function createSchemaForArray(arr: Array, options?: SchemaGenOptions, description? : string): Schema { if (arr.length === 0) { - return { type: ValueType.Array }; + return description ? { type: ValueType.Array, description } : { type: ValueType.Array }; } const elementSchemas = arr.map((value) => createSchemaFor(value, options)); const items = combineSchemas(elementSchemas); - return { type: ValueType.Array, items }; + return description ? { type: ValueType.Array, items, description } : { type: ValueType.Array, items }; } -function createSchemaForObject(obj: Object, options?: SchemaGenOptions): Schema { +function createSchemaForObject(obj: Object, options?: SchemaGenOptions, description? : string): Schema { const keys = Object.keys(obj); + const nonFunctionKeys = keys.filter((key) => typeof obj[key] !== 'function'); if (keys.length === 0) { - return { + + return description ? { type: ValueType.Object, - }; + description + } : { type: ValueType.Object }; } const properties = Object.entries(obj).reduce((props, [key, val]) => { - props[key] = createSchemaFor(val, options); + let description : string | undefined = undefined; + let enums : string[] | undefined = undefined; + + if(obj["addDescriptionOfProperty"]){ + description = obj["addDescriptionOfProperty"](key); + } + + if(obj["addEnumForProperty"]){ + enums = obj["addEnumForProperty"](key); + } + + if(typeof val !== 'function'){ + props[key] = createSchemaFor(val, options, description, enums); + } return props; }, {}); - const schema: Schema = { type: ValueType.Object, properties }; + const schema: Schema = description ? { type: ValueType.Object, properties, description } : { type: ValueType.Object, properties }; + + let requiredKeys = nonFunctionKeys; + if (!options?.noRequired) { - schema.required = keys; + if(obj["excludeFromRequired"]){ + const excluded : string[] = obj["excludeFromRequired"](); + requiredKeys = requiredKeys.filter(x => !excluded.includes(x)) + } + schema.required = requiredKeys; } + return schema; } @@ -231,9 +262,9 @@ function isContainerSchema(schema: Schema): boolean { // FACADE export function createSchema(value: any, options?: SchemaGenOptions): Schema { + JSON.stringify(value);//just to catch circular dependencies if (typeof value === 'undefined') value = null; - const clone = JSON.parse(JSON.stringify(value)); - return createSchemaFor(clone, options); + return createSchemaFor(value, options); } export function mergeSchemas(schemas: Schema[], options?: SchemaGenOptions): Schema { diff --git a/src/types.ts b/src/types.ts index 0e82179..e289a8c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -14,6 +14,7 @@ export type Schema = { properties?: Record; required?: string[]; anyOf?: Array; + description?: string; }; export type SchemaGenOptions = { diff --git a/tests/__snapshots__/schema-builder.spec.ts.snap b/tests/__snapshots__/schema-builder.spec.ts.snap index cd2103d..a377764 100644 --- a/tests/__snapshots__/schema-builder.spec.ts.snap +++ b/tests/__snapshots__/schema-builder.spec.ts.snap @@ -29,6 +29,7 @@ Object { "lvl1PropObj2": Object { "properties": Object { "lvl2PropArr1": Object { + "description": "some description", "items": Object { "type": "integer", }, diff --git a/tests/schema-builder.spec.ts b/tests/schema-builder.spec.ts index bfd2425..bfe3299 100644 --- a/tests/schema-builder.spec.ts +++ b/tests/schema-builder.spec.ts @@ -1,3 +1,4 @@ +import { type } from 'os'; import { createSchema, mergeSchemas, ValueType, extendSchema, createCompoundSchema } from '../src'; import { pp } from './test-utils'; @@ -180,13 +181,40 @@ describe('SchemaBuilder', () => { }); describe('all cases combined', () => { + it('should exclude from required all properties that are returned by excludeFromRequired function', () => { + type TestType = { + notRequired?: string; + required: string; + excludeFromRequired? : () => string[] + }; + + const value: TestType = { + notRequired: 'sometimes undefined', + required: 'never undefined', + excludeFromRequired: () =>['notRequired'] + } + + const schema = createSchema(value); + expect(schema).toEqual({ + type: 'object', + properties: { + notRequired: { type: 'string' }, + required: { type: 'string' }, + }, + required: ['required'], + }); + }); + it('should generate valid schemas for complex objects', () => { const schema = createSchema([ { lvl1PropNum: 1, lvl1PropStr: 'second', + addEnumForProperty: (propName: string) => propName === 'lvl1PropStr'? ['first', 'second', 'third'] : undefined, + addDescriptionOfProperty: (propName: string) => propName === 'lvl1PropStr'? 'enum prop' : undefined, lvl1PropObj1: { lvl2PropArr: [1, 2] }, lvl1PropObj2: { + addDescriptionOfProperty: (propName: string) => propName === 'lvl2PropArr1'? 'some description' : undefined, lvl2PropNum1: 5, lvl2PropArr1: [5], six: null, @@ -273,6 +301,8 @@ describe('SchemaBuilder', () => { }, }); }); + + }); describe('prototype methods', () => {