From d62c3193fb4c9562952a173690e32e1dc6addaa6 Mon Sep 17 00:00:00 2001 From: adamskrodzki Date: Tue, 27 Jun 2023 20:32:42 +0200 Subject: [PATCH 1/3] add description and required control --- src/schema-builder.ts | 57 ++++++++++++------- src/types.ts | 1 + .../__snapshots__/schema-builder.spec.ts.snap | 1 + tests/schema-builder.spec.ts | 28 +++++++++ 4 files changed, 68 insertions(+), 19 deletions(-) diff --git a/src/schema-builder.ts b/src/schema-builder.ts index 6510d3b..3b97187 100644 --- a/src/schema-builder.ts +++ b/src/schema-builder.ts @@ -1,52 +1,71 @@ import { ValueType, Schema, SchemaGenOptions } from './types'; -function createSchemaFor(value: any, options?: SchemaGenOptions): Schema { +function createSchemaFor(value: any, options?: SchemaGenOptions, description? : string): Schema { switch (typeof value) { case 'number': if (Number.isInteger(value)) { - return { type: ValueType.Integer }; + return description ? { type: ValueType.Integer, description } : { type: ValueType.Integer }; } - return { type: ValueType.Number }; + return description ? { type: ValueType.Number, description } : { type: ValueType.Number }; case 'boolean': - return { type: ValueType.Boolean }; + return description ? { type: ValueType.Boolean, description } : { type: ValueType.Boolean }; case 'string': - return { type: ValueType.String }; + return description ? { type: ValueType.String, description } : { type: ValueType.String }; case 'object': if (value === null) { - return { type: ValueType.Null }; + return description ? { type: ValueType.Null, description } : { type: ValueType.Null }; } 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; + + if(obj["addDescriptionOfProperty"]){ + description= obj["addDescriptionOfProperty"](key); + } + + if(typeof val !== 'function'){ + props[key] = createSchemaFor(val, options, description); + } 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 +250,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..303b73a 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,6 +181,30 @@ 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([ { @@ -187,6 +212,7 @@ describe('SchemaBuilder', () => { lvl1PropStr: 'second', lvl1PropObj1: { lvl2PropArr: [1, 2] }, lvl1PropObj2: { + addDescriptionOfProperty: (propName: string) => propName === 'lvl2PropArr1'? 'some description' : undefined, lvl2PropNum1: 5, lvl2PropArr1: [5], six: null, @@ -273,6 +299,8 @@ describe('SchemaBuilder', () => { }, }); }); + + }); describe('prototype methods', () => { From fdf30076045a5076b39445e5cee75813eb89ec2b Mon Sep 17 00:00:00 2001 From: adamskrodzki Date: Tue, 27 Jun 2023 22:32:02 +0200 Subject: [PATCH 2/3] update readme --- README.md | 5 +++++ 1 file changed, 5 insertions(+) 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) From ebeb68a07afb52a252bc5cfb4973ee789ceac9e9 Mon Sep 17 00:00:00 2001 From: adamskrodzki Date: Tue, 27 Jun 2023 23:05:46 +0200 Subject: [PATCH 3/3] add enums --- src/schema-builder.ts | 28 ++++++++++++++++++++-------- tests/schema-builder.spec.ts | 2 ++ 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/src/schema-builder.ts b/src/schema-builder.ts index 3b97187..e7a8b70 100644 --- a/src/schema-builder.ts +++ b/src/schema-builder.ts @@ -1,19 +1,26 @@ import { ValueType, Schema, SchemaGenOptions } from './types'; -function createSchemaFor(value: any, options?: SchemaGenOptions, description? : string): 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 description ? { type: ValueType.Integer, description } : { type: ValueType.Integer }; + return { type: ValueType.Integer, ...additions }; } - return description ? { type: ValueType.Number, description } : { type: ValueType.Number }; + return { type: ValueType.Number, ...additions }; case 'boolean': - return description ? { type: ValueType.Boolean, description } : { type: ValueType.Boolean }; + return { type: ValueType.Boolean, ...additions }; case 'string': - return description ? { type: ValueType.String, description } : { type: ValueType.String }; + return { type: ValueType.String, ...additions }; case 'object': if (value === null) { - return description ? { type: ValueType.Null, description } : { type: ValueType.Null }; + return { type: ValueType.Null, ...additions }; } if (Array.isArray(value)) { return createSchemaForArray(value, options, description); @@ -43,13 +50,18 @@ function createSchemaForObject(obj: Object, options?: SchemaGenOptions, descript } const properties = Object.entries(obj).reduce((props, [key, val]) => { let description : string | undefined = undefined; + let enums : string[] | undefined = undefined; if(obj["addDescriptionOfProperty"]){ - description= obj["addDescriptionOfProperty"](key); + description = obj["addDescriptionOfProperty"](key); + } + + if(obj["addEnumForProperty"]){ + enums = obj["addEnumForProperty"](key); } if(typeof val !== 'function'){ - props[key] = createSchemaFor(val, options, description); + props[key] = createSchemaFor(val, options, description, enums); } return props; }, {}); diff --git a/tests/schema-builder.spec.ts b/tests/schema-builder.spec.ts index 303b73a..bfe3299 100644 --- a/tests/schema-builder.spec.ts +++ b/tests/schema-builder.spec.ts @@ -210,6 +210,8 @@ describe('SchemaBuilder', () => { { 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,