From 8402f42f3ad3a87814ab5c62b537ecfacfcf76ce Mon Sep 17 00:00:00 2001 From: Jacob Alford Date: Sat, 24 Dec 2022 15:29:52 -0700 Subject: [PATCH 01/21] [#137] poc json-schema base --- package.json | 3 + src/base/JsonSchemaBase.ts | 346 +++++++++++++++++++++++++++++++++++++ yarn.lock | 2 +- 3 files changed, 350 insertions(+), 1 deletion(-) create mode 100644 src/base/JsonSchemaBase.ts diff --git a/package.json b/package.json index 95b3c60f..12482fb6 100644 --- a/package.json +++ b/package.json @@ -101,5 +101,8 @@ "ts-jest": "^26.5.3", "ts-node": "^10.8.1", "typescript": "^4.2.3" + }, + "dependencies": { + "@types/json-schema": "^7.0.11" } } diff --git a/src/base/JsonSchemaBase.ts b/src/base/JsonSchemaBase.ts new file mode 100644 index 00000000..0b46063e --- /dev/null +++ b/src/base/JsonSchemaBase.ts @@ -0,0 +1,346 @@ +/** + * Models for JsonSchema as subsets of JSON Schema Draft 4, Draft 6, and Draft 7. + * + * @since 1.1.0 + */ +import { identity, pipe } from 'fp-ts/function' +import { Const, make } from 'fp-ts/lib/Const' +import * as RNEA from 'fp-ts/ReadonlyNonEmptyArray' +import * as RR from 'fp-ts/ReadonlyRecord' +import { memoize, Schemable1 } from 'io-ts/Schemable' + +// ------------------------------------------------------------------------------------- +// Model +// ------------------------------------------------------------------------------------- + +/** + * @since 1.1.0 + * @category Model + */ +type JsonLiteral = JsonString | JsonNumber | JsonBoolean | JsonNull + +/** + * @since 1.1.0 + * @category Model + */ +type JsonSchema_ = JsonLiteral | JsonInteger | JsonStruct | JsonRecord | JsonArray + +/** + * @since 1.1.0 + * @category Model + */ +export type JsonSchema = (JsonSchema_ | JsonUnion) & Description + +/** + * @since 1.1.0 + * @category Model + */ +export interface Description { + readonly title: string + readonly description: string + readonly required: boolean +} + +/** + * @since 1.1.0 + * @category Model + */ +class JsonString { + readonly type = 'string' + constructor( + readonly minLength?: number, + readonly maxLength?: number, + readonly pattern?: string, + ) {} +} + +/** + * @since 1.1.0 + * @category Model + */ +class JsonNumber { + readonly type = 'number' + constructor(readonly minimum?: number, readonly maximum?: number) {} +} + +/** + * @since 1.1.0 + * @category Model + */ +class JsonInteger implements Omit { + readonly type = 'integer' + constructor(readonly minimum?: number, readonly maximum?: number) {} +} + +/** + * @since 1.1.0 + * @category Model + */ +class JsonBoolean { + readonly type = 'boolean' +} + +/** + * @since 1.1.0 + * @category Model + */ +class JsonStruct { + readonly type = 'object' + constructor(readonly properties: Readonly>) {} +} + +/** + * @since 1.1.0 + * @category Model + */ +class JsonRecord { + readonly type = 'object' + constructor(readonly additionalProperties: JsonSchema) {} +} + +/** + * @since 1.1.0 + * @category Model + */ +class JsonArray { + readonly type = 'array' + constructor( + readonly items: JsonSchema | ReadonlyArray, + readonly minItems?: number, + readonly maxItems?: number, + ) {} +} + +/** + * @since 1.1.0 + * @category Model + */ +class JsonNull { + readonly type = 'null' +} + +/** + * @since 1.1.0 + * @category Model + */ +class JsonUnion { + constructor(readonly oneOf: RNEA.ReadonlyNonEmptyArray) {} +} + +// ------------------------------------------------------------------------------------- +// constructors +// ------------------------------------------------------------------------------------- + +/** + * @since 1.1.0 + * @category Constructors + */ +export const makeSchema = ( + schema: JsonSchema_ | JsonUnion, + description: Description, +): Const => make({ ...schema, ...description }) + +/** + * @since 1.1.0 + * @category Constructors + */ +export const makeStringSchema = ( + minLength?: number, + maxLength?: number, + pattern?: string, +): Const => + makeSchema(new JsonString(minLength, maxLength, pattern), { + title: 'string', + description: 'A string value', + required: true, + }) + +/** + * @since 1.1.0 + * @category Constructors + */ +export const makeNumberSchema = ( + minimum?: number, + maximum?: number, +): Const => + makeSchema(new JsonNumber(minimum, maximum), { + title: 'number', + description: 'A number value', + required: true, + }) + +/** + * @since 1.1.0 + * @category Constructors + */ +export const makeIntegerSchema = ( + minimum?: number, + maximum?: number, +): Const => + makeSchema(new JsonInteger(minimum, maximum), { + title: 'integer', + description: 'An integer value', + required: true, + }) + +/** + * @since 1.1.0 + * @category Constructors + */ +export const booleanSchema: Const = makeSchema(new JsonBoolean(), { + title: 'boolean', + description: 'A boolean value', + required: true, +}) + +/** + * @since 1.1.0 + * @category Constructors + */ +export const makeStructSchema = ( + properties: Readonly>, +): Const => + makeSchema(new JsonStruct(properties), { + title: 'object', + description: 'An object value', + required: true, + }) + +/** + * @since 1.1.0 + * @category Constructors + */ +export const makeRecordSchema = ( + additionalProperties: JsonSchema, +): Const => + makeSchema(new JsonRecord(additionalProperties), { + title: 'object', + description: 'An object value', + required: true, + }) + +/** + * @since 1.1.0 + * @category Constructors + */ +export const makeArraySchema = ( + items: JsonSchema, + minItems?: number, + maxItems?: number, +): Const => + makeSchema(new JsonArray(items, minItems, maxItems), { + title: 'array', + description: 'An array value', + required: true, + }) + +/** + * @since 1.1.0 + * @category Constructors + */ +export const makeTupleSchema = ( + items: ReadonlyArray, +): Const => + makeSchema(new JsonArray(items, items.length, items.length), { + title: 'tuple', + description: 'A product of multiple schemata', + required: true, + }) + +/** + * @since 1.1.0 + * @category Constructors + */ +export const nullSchema = makeSchema(new JsonNull(), { + title: 'null', + description: 'An empty value', + required: true, +}) + +// ------------------------------------------------------------------------------------- +// instances +// ------------------------------------------------------------------------------------- + +/** + * @since 1.1.0 + * @category Instances + */ +export const URI = 'JsonSchema' + +/** + * @since 1.1.0 + * @category Instances + */ +export type URI = typeof URI + +declare module 'fp-ts/lib/HKT' { + interface URItoKind { + readonly JsonSchema: Const + } +} + +/** + * @since 1.1.0 + * @category Instances + */ +export const Schemable: Schemable1 = { + URI, + literal: (...values) => + pipe( + values, + RNEA.map(literal => { + if (literal === null) return nullSchema + switch (typeof literal) { + case 'string': + return makeStringSchema() + case 'number': + return makeNumberSchema() + case 'boolean': + return booleanSchema + } + }), + schemata => + makeSchema(new JsonUnion(schemata), { + title: 'literal', + description: 'A union of literal values', + required: true, + }), + ), + string: makeStringSchema(), + number: makeNumberSchema(), + boolean: booleanSchema, + nullable: schema => + makeSchema(new JsonUnion([schema, nullSchema]), { + title: 'nullable', + description: 'A nullable value', + required: true, + }), + struct: makeStructSchema, + partial: properties => + makeStructSchema( + pipe( + properties as Readonly>, + RR.map(({ title, description, ...rest }) => + makeSchema(rest, { title, description, required: false }), + ), + ), + ), + record: makeRecordSchema, + array: makeArraySchema, + tuple: (...schemata) => makeTupleSchema(schemata), + intersect: right => left => { + // Oh boy + }, + sum: tag => members => { + // Todo + }, + lazy: (id, f) => { + const get = memoize(f) + return makeSchema(get(), { + title: id, + description: `Lazy(${id})`, + required: true, + }) + }, + readonly: identity, +} diff --git a/yarn.lock b/yarn.lock index b8103f7d..cb2d928c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1741,7 +1741,7 @@ jest-diff "^26.0.0" pretty-format "^26.0.0" -"@types/json-schema@^7.0.7": +"@types/json-schema@^7.0.11", "@types/json-schema@^7.0.7": version "7.0.11" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3" integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ== From a7fbd16e362d532f767e9ee5460a9f90df86ca41 Mon Sep 17 00:00:00 2001 From: Jacob Alford Date: Sat, 24 Dec 2022 17:54:51 -0700 Subject: [PATCH 02/21] [#137] clean up json-schema base --- src/base/JsonSchemaBase.ts | 229 ++++++++++++++----------------------- 1 file changed, 84 insertions(+), 145 deletions(-) diff --git a/src/base/JsonSchemaBase.ts b/src/base/JsonSchemaBase.ts index 0b46063e..8b5e5c07 100644 --- a/src/base/JsonSchemaBase.ts +++ b/src/base/JsonSchemaBase.ts @@ -3,12 +3,15 @@ * * @since 1.1.0 */ -import { identity, pipe } from 'fp-ts/function' +import { identity, pipe, unsafeCoerce } from 'fp-ts/function' import { Const, make } from 'fp-ts/lib/Const' import * as RNEA from 'fp-ts/ReadonlyNonEmptyArray' import * as RR from 'fp-ts/ReadonlyRecord' +import * as Str from 'fp-ts/string' import { memoize, Schemable1 } from 'io-ts/Schemable' +import { Int } from '../schemables/WithInt/definition' + // ------------------------------------------------------------------------------------- // Model // ------------------------------------------------------------------------------------- @@ -17,34 +20,28 @@ import { memoize, Schemable1 } from 'io-ts/Schemable' * @since 1.1.0 * @category Model */ -type JsonLiteral = JsonString | JsonNumber | JsonBoolean | JsonNull - -/** - * @since 1.1.0 - * @category Model - */ -type JsonSchema_ = JsonLiteral | JsonInteger | JsonStruct | JsonRecord | JsonArray +export type JsonSchema = ( + | JsonString + | JsonNumber + | JsonBoolean + | JsonNull + | JsonInteger + | JsonStruct + | JsonRecord + | JsonArray + | JsonUnion + | JsonIntersection +) & + Description -/** - * @since 1.1.0 - * @category Model - */ -export type JsonSchema = (JsonSchema_ | JsonUnion) & Description - -/** - * @since 1.1.0 - * @category Model - */ -export interface Description { - readonly title: string - readonly description: string - readonly required: boolean +/** @internal */ +interface Description { + readonly title?: string + readonly description?: string + readonly required?: boolean } -/** - * @since 1.1.0 - * @category Model - */ +/** @internal */ class JsonString { readonly type = 'string' constructor( @@ -54,54 +51,36 @@ class JsonString { ) {} } -/** - * @since 1.1.0 - * @category Model - */ +/** @internal */ class JsonNumber { readonly type = 'number' constructor(readonly minimum?: number, readonly maximum?: number) {} } -/** - * @since 1.1.0 - * @category Model - */ +/** @internal */ class JsonInteger implements Omit { readonly type = 'integer' constructor(readonly minimum?: number, readonly maximum?: number) {} } -/** - * @since 1.1.0 - * @category Model - */ +/** @internal */ class JsonBoolean { readonly type = 'boolean' } -/** - * @since 1.1.0 - * @category Model - */ +/** @internal */ class JsonStruct { readonly type = 'object' constructor(readonly properties: Readonly>) {} } -/** - * @since 1.1.0 - * @category Model - */ +/** @internal */ class JsonRecord { readonly type = 'object' constructor(readonly additionalProperties: JsonSchema) {} } -/** - * @since 1.1.0 - * @category Model - */ +/** @internal */ class JsonArray { readonly type = 'array' constructor( @@ -111,20 +90,19 @@ class JsonArray { ) {} } -/** - * @since 1.1.0 - * @category Model - */ +/** @internal */ class JsonNull { readonly type = 'null' } -/** - * @since 1.1.0 - * @category Model - */ +/** @internal */ class JsonUnion { - constructor(readonly oneOf: RNEA.ReadonlyNonEmptyArray) {} + constructor(readonly oneOf: ReadonlyArray) {} +} + +/** @internal */ +class JsonIntersection { + constructor(readonly allOf: RNEA.ReadonlyNonEmptyArray) {} } // ------------------------------------------------------------------------------------- @@ -135,10 +113,10 @@ class JsonUnion { * @since 1.1.0 * @category Constructors */ -export const makeSchema = ( - schema: JsonSchema_ | JsonUnion, - description: Description, -): Const => make({ ...schema, ...description }) +export const makeSchema = ( + schema: JsonSchema, + description: Description = {}, +): Const => make({ ...schema, ...description }) /** * @since 1.1.0 @@ -148,12 +126,7 @@ export const makeStringSchema = ( minLength?: number, maxLength?: number, pattern?: string, -): Const => - makeSchema(new JsonString(minLength, maxLength, pattern), { - title: 'string', - description: 'A string value', - required: true, - }) +): Const => makeSchema(new JsonString(minLength, maxLength, pattern)) /** * @since 1.1.0 @@ -162,12 +135,7 @@ export const makeStringSchema = ( export const makeNumberSchema = ( minimum?: number, maximum?: number, -): Const => - makeSchema(new JsonNumber(minimum, maximum), { - title: 'number', - description: 'A number value', - required: true, - }) +): Const => makeSchema(new JsonNumber(minimum, maximum)) /** * @since 1.1.0 @@ -176,86 +144,56 @@ export const makeNumberSchema = ( export const makeIntegerSchema = ( minimum?: number, maximum?: number, -): Const => - makeSchema(new JsonInteger(minimum, maximum), { - title: 'integer', - description: 'An integer value', - required: true, - }) +): Const => makeSchema(new JsonInteger(minimum, maximum)) /** * @since 1.1.0 * @category Constructors */ -export const booleanSchema: Const = makeSchema(new JsonBoolean(), { - title: 'boolean', - description: 'A boolean value', - required: true, -}) +export const booleanSchema: Const = makeSchema(new JsonBoolean()) /** * @since 1.1.0 * @category Constructors */ -export const makeStructSchema = ( - properties: Readonly>, -): Const => - makeSchema(new JsonStruct(properties), { - title: 'object', - description: 'An object value', - required: true, - }) +export const makeStructSchema = (properties: { + [K in keyof A]: Const +}): Const => makeSchema(new JsonStruct(properties)) /** * @since 1.1.0 * @category Constructors */ -export const makeRecordSchema = ( - additionalProperties: JsonSchema, -): Const => - makeSchema(new JsonRecord(additionalProperties), { - title: 'object', - description: 'An object value', - required: true, - }) +export const makeRecordSchema = ( + additionalProperties: Const, +): Const> => + makeSchema(new JsonRecord(additionalProperties)) /** * @since 1.1.0 * @category Constructors */ -export const makeArraySchema = ( - items: JsonSchema, +export const makeArraySchema = ( + items: Const, minItems?: number, maxItems?: number, -): Const => - makeSchema(new JsonArray(items, minItems, maxItems), { - title: 'array', - description: 'An array value', - required: true, - }) +): Const> => makeSchema(new JsonArray(items, minItems, maxItems)) /** * @since 1.1.0 * @category Constructors */ -export const makeTupleSchema = ( - items: ReadonlyArray, -): Const => - makeSchema(new JsonArray(items, items.length, items.length), { - title: 'tuple', - description: 'A product of multiple schemata', - required: true, - }) +export const makeTupleSchema = >( + ...items: { + [K in keyof A]: Const + } +): Const => makeSchema(new JsonArray(items, items.length, items.length)) /** * @since 1.1.0 * @category Constructors */ -export const nullSchema = makeSchema(new JsonNull(), { - title: 'null', - description: 'An empty value', - required: true, -}) +export const nullSchema = makeSchema(new JsonNull()) // ------------------------------------------------------------------------------------- // instances @@ -295,45 +233,46 @@ export const Schemable: Schemable1 = { return makeStringSchema() case 'number': return makeNumberSchema() - case 'boolean': + default: return booleanSchema } }), - schemata => - makeSchema(new JsonUnion(schemata), { - title: 'literal', - description: 'A union of literal values', - required: true, - }), + schemata => makeSchema(new JsonUnion(schemata)), ), string: makeStringSchema(), number: makeNumberSchema(), boolean: booleanSchema, - nullable: schema => - makeSchema(new JsonUnion([schema, nullSchema]), { - title: 'nullable', - description: 'A nullable value', - required: true, - }), + nullable: schema => makeSchema(new JsonUnion([schema, nullSchema])), struct: makeStructSchema, - partial: properties => - makeStructSchema( + partial: (properties: { [K in keyof A]: Const }): Const< + JsonSchema, + Partial<{ [K in keyof A]: A[K] }> + > => + makeStructSchema>( pipe( properties as Readonly>, RR.map(({ title, description, ...rest }) => makeSchema(rest, { title, description, required: false }), ), + a => unsafeCoerce(a), ), ), record: makeRecordSchema, array: makeArraySchema, - tuple: (...schemata) => makeTupleSchema(schemata), - intersect: right => left => { - // Oh boy - }, - sum: tag => members => { - // Todo - }, + // @ts-expect-error -- typelevel difference + tuple: >( + ...items: { [K in keyof A]: Const } + ): Const => makeTupleSchema(...items), + intersect: right => left => makeSchema(new JsonIntersection([left, right])), + sum: () => members => + makeSchema( + new JsonUnion( + pipe( + members as Readonly>>, + RR.collect(Str.Ord)((_, a) => a), + ), + ), + ), lazy: (id, f) => { const get = memoize(f) return makeSchema(get(), { From fd61e1f1d73d5e1f883fa4970e454f856e8dd328 Mon Sep 17 00:00:00 2001 From: Jacob Alford Date: Sat, 24 Dec 2022 17:57:44 -0700 Subject: [PATCH 03/21] [#137] make base json-schema require --- src/base/JsonSchemaBase.ts | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/src/base/JsonSchemaBase.ts b/src/base/JsonSchemaBase.ts index 8b5e5c07..47a54850 100644 --- a/src/base/JsonSchemaBase.ts +++ b/src/base/JsonSchemaBase.ts @@ -126,7 +126,8 @@ export const makeStringSchema = ( minLength?: number, maxLength?: number, pattern?: string, -): Const => makeSchema(new JsonString(minLength, maxLength, pattern)) +): Const => + makeSchema(new JsonString(minLength, maxLength, pattern), { required: true }) /** * @since 1.1.0 @@ -135,7 +136,8 @@ export const makeStringSchema = ( export const makeNumberSchema = ( minimum?: number, maximum?: number, -): Const => makeSchema(new JsonNumber(minimum, maximum)) +): Const => + makeSchema(new JsonNumber(minimum, maximum), { required: true }) /** * @since 1.1.0 @@ -144,13 +146,16 @@ export const makeNumberSchema = ( export const makeIntegerSchema = ( minimum?: number, maximum?: number, -): Const => makeSchema(new JsonInteger(minimum, maximum)) +): Const => + makeSchema(new JsonInteger(minimum, maximum), { required: true }) /** * @since 1.1.0 * @category Constructors */ -export const booleanSchema: Const = makeSchema(new JsonBoolean()) +export const booleanSchema: Const = makeSchema(new JsonBoolean(), { + required: true, +}) /** * @since 1.1.0 @@ -158,7 +163,7 @@ export const booleanSchema: Const = makeSchema(new JsonBool */ export const makeStructSchema = (properties: { [K in keyof A]: Const -}): Const => makeSchema(new JsonStruct(properties)) +}): Const => makeSchema(new JsonStruct(properties), { required: true }) /** * @since 1.1.0 @@ -167,7 +172,7 @@ export const makeStructSchema = (properties: { export const makeRecordSchema = ( additionalProperties: Const, ): Const> => - makeSchema(new JsonRecord(additionalProperties)) + makeSchema(new JsonRecord(additionalProperties), { required: true }) /** * @since 1.1.0 @@ -177,7 +182,8 @@ export const makeArraySchema = ( items: Const, minItems?: number, maxItems?: number, -): Const> => makeSchema(new JsonArray(items, minItems, maxItems)) +): Const> => + makeSchema(new JsonArray(items, minItems, maxItems), { required: true }) /** * @since 1.1.0 @@ -187,13 +193,14 @@ export const makeTupleSchema = >( ...items: { [K in keyof A]: Const } -): Const => makeSchema(new JsonArray(items, items.length, items.length)) +): Const => + makeSchema(new JsonArray(items, items.length, items.length), { required: true }) /** * @since 1.1.0 * @category Constructors */ -export const nullSchema = makeSchema(new JsonNull()) +export const nullSchema = makeSchema(new JsonNull(), { required: true }) // ------------------------------------------------------------------------------------- // instances From 54d1b5f9c9ce2832b7ca00d2a744c0577e0ed830 Mon Sep 17 00:00:00 2001 From: Jacob Alford Date: Mon, 2 Jan 2023 12:50:01 -0700 Subject: [PATCH 04/21] [#137] add json-schema to schemables --- src/base/JsonSchemaBase.ts | 218 +++++++++++------- .../WithBrand/instances/json-schema.ts | 17 ++ .../WithCheckDigit/instances/json-schema.ts | 17 ++ .../WithDate/instances/json-schema.ts | 18 ++ .../WithFloat/instances/json-schema.ts | 24 ++ .../WithInt/instances/json-schema.ts | 24 ++ .../WithInvariant/instances/json-schema.ts | 17 ++ .../WithJson/instances/json-schema.ts | 18 ++ .../WithMap/instances/json-schema.ts | 15 ++ .../WithOption/instances/json-schema.ts | 23 ++ .../WithOptional/instances/json-schema.ts | 16 ++ .../WithPadding/instances/json-schema.ts | 45 ++++ .../WithPattern/instances/json-schema.ts | 24 ++ .../WithRefine/instances/json-schema.ts | 13 ++ .../instances/json-schema.ts | 12 + .../instances/printer.ts | 2 +- 16 files changed, 417 insertions(+), 86 deletions(-) create mode 100644 src/schemables/WithBrand/instances/json-schema.ts create mode 100644 src/schemables/WithCheckDigit/instances/json-schema.ts create mode 100644 src/schemables/WithDate/instances/json-schema.ts create mode 100644 src/schemables/WithFloat/instances/json-schema.ts create mode 100644 src/schemables/WithInt/instances/json-schema.ts create mode 100644 src/schemables/WithInvariant/instances/json-schema.ts create mode 100644 src/schemables/WithJson/instances/json-schema.ts create mode 100644 src/schemables/WithMap/instances/json-schema.ts create mode 100644 src/schemables/WithOption/instances/json-schema.ts create mode 100644 src/schemables/WithOptional/instances/json-schema.ts create mode 100644 src/schemables/WithPadding/instances/json-schema.ts create mode 100644 src/schemables/WithPattern/instances/json-schema.ts create mode 100644 src/schemables/WithRefine/instances/json-schema.ts create mode 100644 src/schemables/WithUnknownContainers/instances/json-schema.ts diff --git a/src/base/JsonSchemaBase.ts b/src/base/JsonSchemaBase.ts index 47a54850..8b4e057d 100644 --- a/src/base/JsonSchemaBase.ts +++ b/src/base/JsonSchemaBase.ts @@ -1,46 +1,56 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ /** * Models for JsonSchema as subsets of JSON Schema Draft 4, Draft 6, and Draft 7. * - * @since 1.1.0 + * @since 1.2.0 */ -import { identity, pipe, unsafeCoerce } from 'fp-ts/function' +import { identity, pipe } from 'fp-ts/function' import { Const, make } from 'fp-ts/lib/Const' import * as RNEA from 'fp-ts/ReadonlyNonEmptyArray' import * as RR from 'fp-ts/ReadonlyRecord' import * as Str from 'fp-ts/string' -import { memoize, Schemable1 } from 'io-ts/Schemable' +import { memoize } from 'io-ts/Schemable' import { Int } from '../schemables/WithInt/definition' +import { Schemable2 } from './SchemableBase' // ------------------------------------------------------------------------------------- // Model // ------------------------------------------------------------------------------------- /** - * @since 1.1.0 + * @since 1.2.0 * @category Model */ -export type JsonSchema = ( +export type JsonSchema = + | JsonEmpty | JsonString | JsonNumber | JsonBoolean | JsonNull | JsonInteger + | JsonLiteral | JsonStruct | JsonRecord | JsonArray | JsonUnion | JsonIntersection -) & - Description + +/** + * @since 1.2.0 + * @category Model + */ +export type JsonSchemaWithDescription = JsonSchema & Description /** @internal */ interface Description { readonly title?: string readonly description?: string - readonly required?: boolean } +/** @internal */ +class JsonEmpty {} + /** @internal */ class JsonString { readonly type = 'string' @@ -48,6 +58,10 @@ class JsonString { readonly minLength?: number, readonly maxLength?: number, readonly pattern?: string, + readonly contentEncoding?: string, + readonly contentMediaType?: string, + readonly contentSchema?: JsonSchema, + readonly format?: string, ) {} } @@ -68,10 +82,32 @@ class JsonBoolean { readonly type = 'boolean' } +/** @internal */ +type JsonLiteral = + | { + readonly type: 'string' + readonly const: string + } + | { + readonly type: 'number' + readonly const: number + } + | { + readonly type: 'boolean' + readonly const: boolean + } + | { + readonly type: 'null' + readonly const: null + } + /** @internal */ class JsonStruct { readonly type = 'object' - constructor(readonly properties: Readonly>) {} + constructor( + readonly properties: Readonly>, + readonly required: ReadonlyArray = [], + ) {} } /** @internal */ @@ -110,169 +146,185 @@ class JsonIntersection { // ------------------------------------------------------------------------------------- /** - * @since 1.1.0 + * @since 1.2.0 * @category Constructors */ -export const makeSchema = ( - schema: JsonSchema, - description: Description = {}, -): Const => make({ ...schema, ...description }) +export const emptySchema = make(new JsonEmpty()) /** - * @since 1.1.0 + * @since 1.2.0 * @category Constructors */ export const makeStringSchema = ( minLength?: number, maxLength?: number, pattern?: string, + contentEncoding?: string, + contentMediaType?: string, + contentSchema?: JsonSchema, + format?: string, ): Const => - makeSchema(new JsonString(minLength, maxLength, pattern), { required: true }) + make( + new JsonString( + minLength, + maxLength, + pattern, + contentEncoding, + contentMediaType, + contentSchema, + format, + ), + ) /** - * @since 1.1.0 + * @since 1.2.0 * @category Constructors */ export const makeNumberSchema = ( minimum?: number, maximum?: number, -): Const => - makeSchema(new JsonNumber(minimum, maximum), { required: true }) +): Const => make(new JsonNumber(minimum, maximum)) /** - * @since 1.1.0 + * @since 1.2.0 * @category Constructors */ export const makeIntegerSchema = ( minimum?: number, maximum?: number, -): Const => - makeSchema(new JsonInteger(minimum, maximum), { required: true }) +): Const => make(new JsonInteger(minimum, maximum)) + +/** + * @since 1.2.0 + * @category Constructors + */ +export const booleanSchema: Const = make(new JsonBoolean()) /** - * @since 1.1.0 + * @since 1.2.0 * @category Constructors */ -export const booleanSchema: Const = makeSchema(new JsonBoolean(), { - required: true, -}) +export const makeLiteralSchema = ( + value: A, +): Const => + value === null ? make(new JsonNull()) : make({ type: typeof value, const: value }) /** - * @since 1.1.0 + * @since 1.2.0 * @category Constructors */ -export const makeStructSchema = (properties: { - [K in keyof A]: Const -}): Const => makeSchema(new JsonStruct(properties), { required: true }) +export const makeStructSchema = ( + properties: { + [K in keyof A]: Const + }, + required: ReadonlyArray = [], +): Const => make(new JsonStruct(properties, required)) /** - * @since 1.1.0 + * @since 1.2.0 * @category Constructors */ export const makeRecordSchema = ( additionalProperties: Const, -): Const> => - makeSchema(new JsonRecord(additionalProperties), { required: true }) +): Const> => make(new JsonRecord(additionalProperties)) /** - * @since 1.1.0 + * @since 1.2.0 * @category Constructors */ export const makeArraySchema = ( items: Const, minItems?: number, maxItems?: number, -): Const> => - makeSchema(new JsonArray(items, minItems, maxItems), { required: true }) +): Const> => make(new JsonArray(items, minItems, maxItems)) /** - * @since 1.1.0 + * @since 1.2.0 * @category Constructors */ export const makeTupleSchema = >( ...items: { [K in keyof A]: Const } -): Const => - makeSchema(new JsonArray(items, items.length, items.length), { required: true }) +): Const => make(new JsonArray(items, items.length, items.length)) /** - * @since 1.1.0 + * @since 1.2.0 * @category Constructors */ -export const nullSchema = makeSchema(new JsonNull(), { required: true }) +export const nullSchema = make(new JsonNull()) + +/** + * @since 1.2.0 + * @category Constructors + */ +export const makeUnionSchema = >>( + ...members: U +): Const ? A : never> => + make(new JsonUnion(members)) + +/** + * @since 1.2.0 + * @category Combintators + */ +export const annotate: ( + name?: string, + description?: string, +) => (schema: JsonSchema) => Const = + (name, description) => schema => + make({ + ...schema, + name, + description, + }) // ------------------------------------------------------------------------------------- // instances // ------------------------------------------------------------------------------------- /** - * @since 1.1.0 + * @since 1.2.0 * @category Instances */ export const URI = 'JsonSchema' /** - * @since 1.1.0 + * @since 1.2.0 * @category Instances */ export type URI = typeof URI declare module 'fp-ts/lib/HKT' { - interface URItoKind { - readonly JsonSchema: Const + interface URItoKind2 { + readonly JsonSchema: Const } } /** - * @since 1.1.0 + * @since 1.2.0 * @category Instances */ -export const Schemable: Schemable1 = { +export const Schemable: Schemable2 = { URI, literal: (...values) => - pipe( - values, - RNEA.map(literal => { - if (literal === null) return nullSchema - switch (typeof literal) { - case 'string': - return makeStringSchema() - case 'number': - return makeNumberSchema() - default: - return booleanSchema - } - }), - schemata => makeSchema(new JsonUnion(schemata)), - ), + pipe(values, RNEA.map(makeLiteralSchema), schemata => make(new JsonUnion(schemata))), string: makeStringSchema(), number: makeNumberSchema(), boolean: booleanSchema, - nullable: schema => makeSchema(new JsonUnion([schema, nullSchema])), - struct: makeStructSchema, - partial: (properties: { [K in keyof A]: Const }): Const< - JsonSchema, - Partial<{ [K in keyof A]: A[K] }> - > => - makeStructSchema>( - pipe( - properties as Readonly>, - RR.map(({ title, description, ...rest }) => - makeSchema(rest, { title, description, required: false }), - ), - a => unsafeCoerce(a), - ), - ), + nullable: schema => make(new JsonUnion([schema, nullSchema])), + // @ts-expect-error -- typelevel difference + struct: s => makeStructSchema(s, Object.keys(s)), + // @ts-expect-error -- typelevel difference + partial: makeStructSchema, record: makeRecordSchema, array: makeArraySchema, // @ts-expect-error -- typelevel difference tuple: >( ...items: { [K in keyof A]: Const } ): Const => makeTupleSchema(...items), - intersect: right => left => makeSchema(new JsonIntersection([left, right])), + intersect: right => left => make(new JsonIntersection([left, right])), sum: () => members => - makeSchema( + make( new JsonUnion( pipe( members as Readonly>>, @@ -280,13 +332,9 @@ export const Schemable: Schemable1 = { ), ), ), - lazy: (id, f) => { + lazy: (_, f) => { const get = memoize(f) - return makeSchema(get(), { - title: id, - description: `Lazy(${id})`, - required: true, - }) + return make(get()) }, readonly: identity, } diff --git a/src/schemables/WithBrand/instances/json-schema.ts b/src/schemables/WithBrand/instances/json-schema.ts new file mode 100644 index 00000000..47dd908d --- /dev/null +++ b/src/schemables/WithBrand/instances/json-schema.ts @@ -0,0 +1,17 @@ +/** + * Schemable for constructing a branded newtype + * + * @since 1.2.0 + */ +import { identity } from 'fp-ts/function' + +import * as JS from '../../../base/JsonSchemaBase' +import { WithBrand2 } from '../definition' + +/** + * @since 1.2.0 + * @category Instances + */ +export const JsonSchema: WithBrand2 = { + brand: () => identity, +} diff --git a/src/schemables/WithCheckDigit/instances/json-schema.ts b/src/schemables/WithCheckDigit/instances/json-schema.ts new file mode 100644 index 00000000..fe836aa3 --- /dev/null +++ b/src/schemables/WithCheckDigit/instances/json-schema.ts @@ -0,0 +1,17 @@ +/** + * Schemable for constructing a string with a check digit (e.g. ISBN or Credit Card) + * + * @since 1.2.0 + */ +import { identity } from 'fp-ts/function' + +import * as JS from '../../../base/JsonSchemaBase' +import { WithCheckDigit2 } from '../definition' + +/** + * @since 1.2.0 + * @category Instances + */ +export const JsonSchema: WithCheckDigit2 = { + checkDigit: () => identity, +} diff --git a/src/schemables/WithDate/instances/json-schema.ts b/src/schemables/WithDate/instances/json-schema.ts new file mode 100644 index 00000000..9d3f3e03 --- /dev/null +++ b/src/schemables/WithDate/instances/json-schema.ts @@ -0,0 +1,18 @@ +/** + * Represents valid Date objects, and valid date-strings parsable by `Date.parse` + * + * @since 1.2.0 + */ +import * as JS from '../../../base/JsonSchemaBase' +import { WithDate2 } from '../definition' + +const _ = undefined + +/** + * @since 1.2.0 + * @category Instances + */ +export const JsonSchema: WithDate2 = { + date: JS.emptySchema, + dateFromString: JS.makeStringSchema(_, _, _, _, _, _, 'date'), +} diff --git a/src/schemables/WithFloat/instances/json-schema.ts b/src/schemables/WithFloat/instances/json-schema.ts new file mode 100644 index 00000000..cd2b5212 --- /dev/null +++ b/src/schemables/WithFloat/instances/json-schema.ts @@ -0,0 +1,24 @@ +/** + * Floating point branded newtype. Parameters: min, max are inclusive. + * + * Represents floating point numbers: + * + * ```math + * { f | f ∈ ℝ, f >= -Number.MAX_VALUE, f <= Number.MAX_VALUE } + * ``` + * + * @since 1.2.0 + */ +import * as JS from '../../../base/JsonSchemaBase' +import { WithFloat2 } from '../definition' + +/** + * @since 1.2.0 + * @category Instances + */ +export const JsonSchema: WithFloat2 = { + float(params = {}) { + const { min = -Number.MAX_VALUE, max = Number.MAX_VALUE } = params + return JS.makeNumberSchema(min, max) + }, +} diff --git a/src/schemables/WithInt/instances/json-schema.ts b/src/schemables/WithInt/instances/json-schema.ts new file mode 100644 index 00000000..babbc323 --- /dev/null +++ b/src/schemables/WithInt/instances/json-schema.ts @@ -0,0 +1,24 @@ +/** + * Integer branded newtype. Parameters: min, max are inclusive. + * + * Represents integers: + * + * ```math + * { z | z ∈ ℤ, z >= -2 ** 53 + 1, z <= 2 ** 53 - 1 } + * ``` + * + * @since 1.2.0 + */ +import * as JS from '../../../base/JsonSchemaBase' +import { WithInt2 } from '../definition' + +/** + * @since 1.2.0 + * @category Instances + */ +export const JsonSchema: WithInt2 = { + int: (params = {}) => { + const { min = Number.MIN_SAFE_INTEGER, max = Number.MAX_SAFE_INTEGER } = params + return JS.makeIntegerSchema(min, max) + }, +} diff --git a/src/schemables/WithInvariant/instances/json-schema.ts b/src/schemables/WithInvariant/instances/json-schema.ts new file mode 100644 index 00000000..8f125fa7 --- /dev/null +++ b/src/schemables/WithInvariant/instances/json-schema.ts @@ -0,0 +1,17 @@ +/** + * Invariant mapping for schemable + * + * @since 1.2.0 + */ +import { identity } from 'fp-ts/function' + +import * as JS from '../../../base/JsonSchemaBase' +import { WithInvariant2 } from '../definition' + +/** + * @since 1.2.0 + * @category Instances + */ +export const JsonSchema: WithInvariant2 = { + imap: () => () => identity, +} diff --git a/src/schemables/WithJson/instances/json-schema.ts b/src/schemables/WithJson/instances/json-schema.ts new file mode 100644 index 00000000..7c9affae --- /dev/null +++ b/src/schemables/WithJson/instances/json-schema.ts @@ -0,0 +1,18 @@ +/** + * A basal schemable for Json and JsonString + * + * @since 1.2.0 + */ +import * as JS from '../../../base/JsonSchemaBase' +import { WithJson2 } from '../definition' + +const _ = undefined + +/** + * @since 1.2.0 + * @category Instances + */ +export const JsonSchema: WithJson2 = { + json: JS.emptySchema, + jsonString: JS.makeStringSchema(_, _, _, _, 'application/json'), +} diff --git a/src/schemables/WithMap/instances/json-schema.ts b/src/schemables/WithMap/instances/json-schema.ts new file mode 100644 index 00000000..8df8969e --- /dev/null +++ b/src/schemables/WithMap/instances/json-schema.ts @@ -0,0 +1,15 @@ +/** + * Represents a ReadonlyMap converted from an expected array of entries. + * + * @since 1.2.0 + */ +import * as JS from '../../../base/JsonSchemaBase' +import { WithMap2 } from '../definition' + +/** + * @since 1.2.0 + * @category Instances + */ +export const JsonSchema: WithMap2 = { + mapFromEntries: (_, jsK, jsA) => JS.makeArraySchema(JS.Schemable.tuple(jsK, jsA)), +} diff --git a/src/schemables/WithOption/instances/json-schema.ts b/src/schemables/WithOption/instances/json-schema.ts new file mode 100644 index 00000000..6cb9e9e1 --- /dev/null +++ b/src/schemables/WithOption/instances/json-schema.ts @@ -0,0 +1,23 @@ +/** + * Represents an exclusion of a supplied value where the exclusion is mapped to `None`. + * Requires an inner schemable, and an Eq instance which defaults to strict equality. + * + * @since 1.2.0 + */ +import * as JS from '../../../base/JsonSchemaBase' +import { WithOption2 } from '../definition' + +/** + * @since 1.2.0 + * @category Instances + */ +export const JsonSchema: WithOption2 = { + optionFromExclude: (exclude, jsA) => + /* + * TODO @jacob-alford: Exclude can technically _not_ be a literal value, and that + * might cause problems with non-literal types turning into `boolean`s with the way + * `literal` is constructed + */ + // @ts-expect-error -- typelevel difference + JS.makeUnionSchema(JS.makeLiteralSchema(exclude), jsA), +} diff --git a/src/schemables/WithOptional/instances/json-schema.ts b/src/schemables/WithOptional/instances/json-schema.ts new file mode 100644 index 00000000..f2216792 --- /dev/null +++ b/src/schemables/WithOptional/instances/json-schema.ts @@ -0,0 +1,16 @@ +/** + * Schemable for widening a type to include undefined. Similar to nullable but for undefined. + * + * @since 1.2.0 + */ +import * as JS from '../../../base/JsonSchemaBase' +import { WithOptional2 } from '../definition' + +/** + * @since 1.2.0 + * @category Instances + */ +export const JsonSchema: WithOptional2 = { + // Undefined is not a valid JSON Value + optional: () => JS.emptySchema, +} diff --git a/src/schemables/WithPadding/instances/json-schema.ts b/src/schemables/WithPadding/instances/json-schema.ts new file mode 100644 index 00000000..42ea9aaf --- /dev/null +++ b/src/schemables/WithPadding/instances/json-schema.ts @@ -0,0 +1,45 @@ +/** + * Adds a character to the right or left of a string until it reaches a certain length. + * + * @since 1.2.0 + */ +import { pipe } from 'fp-ts/function' + +import * as JS from '../../../base/JsonSchemaBase' +import { WithPadding2 } from '../definition' +import { match } from '../utils' + +/** + * @since 1.2.0 + * @category Instances + */ +export const JsonSchema: WithPadding2 = { + padLeft: length => () => + pipe( + length, + match({ + MaxLength: ({ maxLength }) => + typeof maxLength === 'number' + ? JS.makeStringSchema(undefined, maxLength) + : JS.makeStringSchema(), + ExactLength: ({ exactLength }) => + typeof exactLength === 'number' + ? JS.makeStringSchema(exactLength, exactLength) + : JS.makeStringSchema(), + }), + ), + padRight: length => () => + pipe( + length, + match({ + MaxLength: ({ maxLength }) => + typeof maxLength === 'number' + ? JS.makeStringSchema(undefined, maxLength) + : JS.makeStringSchema(), + ExactLength: ({ exactLength }) => + typeof exactLength === 'number' + ? JS.makeStringSchema(exactLength, exactLength) + : JS.makeStringSchema(), + }), + ), +} diff --git a/src/schemables/WithPattern/instances/json-schema.ts b/src/schemables/WithPattern/instances/json-schema.ts new file mode 100644 index 00000000..828e108b --- /dev/null +++ b/src/schemables/WithPattern/instances/json-schema.ts @@ -0,0 +1,24 @@ +/** + * Schemable construction based on Regex combinators + * + * @since 1.2.0 + */ +import { pipe } from 'fp-ts/function' + +import * as JS from '../../../base/JsonSchemaBase' +import * as PB from '../../../PatternBuilder' +import { WithPattern2 } from '../definition' + +const _ = undefined + +/** + * @since 1.2.0 + * @category Instances + */ +export const JsonSchema: WithPattern2 = { + pattern: (pattern, description, caseInsensitive) => + pipe( + JS.makeStringSchema(_, _, PB.regexFromPattern(pattern, caseInsensitive).source), + JS.annotate(_, description), + ), +} diff --git a/src/schemables/WithRefine/instances/json-schema.ts b/src/schemables/WithRefine/instances/json-schema.ts new file mode 100644 index 00000000..f547de7a --- /dev/null +++ b/src/schemables/WithRefine/instances/json-schema.ts @@ -0,0 +1,13 @@ +/** @since 1.2.0 */ +import { identity } from 'fp-ts/function' + +import * as JS from '../../../base/JsonSchemaBase' +import { WithRefine2 } from '../definition' + +/** + * @since 1.2.0 + * @category Instances + */ +export const JsonSchema: WithRefine2 = { + refine: () => identity, +} diff --git a/src/schemables/WithUnknownContainers/instances/json-schema.ts b/src/schemables/WithUnknownContainers/instances/json-schema.ts new file mode 100644 index 00000000..df79fe14 --- /dev/null +++ b/src/schemables/WithUnknownContainers/instances/json-schema.ts @@ -0,0 +1,12 @@ +/** @since 1.2.0 */ +import * as JS from '../../../base/JsonSchemaBase' +import { WithUnknownContainers2 } from '../definition' + +/** + * @since 1.2.0 + * @category Instances + */ +export const Printer: WithUnknownContainers2 = { + UnknownArray: JS.makeArraySchema(JS.emptySchema), + UnknownRecord: JS.makeRecordSchema(JS.emptySchema), +} diff --git a/src/schemables/WithUnknownContainers/instances/printer.ts b/src/schemables/WithUnknownContainers/instances/printer.ts index 66eb1f4f..e5fd4200 100644 --- a/src/schemables/WithUnknownContainers/instances/printer.ts +++ b/src/schemables/WithUnknownContainers/instances/printer.ts @@ -1,4 +1,4 @@ -/** @since 1.0.0 */ +/** @since 1.1.0 */ import * as P from '../../../base/PrinterBase' import { WithUnknownContainers2 } from '../definition' From b486447551ad3b641d3fbebb416da239a61f1b0e Mon Sep 17 00:00:00 2001 From: Jacob Alford Date: Tue, 3 Jan 2023 17:34:10 -0700 Subject: [PATCH 05/21] [#137] fully test json-schema --- scripts/generate-schemables.ts | 2 + src/JsonSchema.ts | 59 +++ src/base/JsonSchemaBase.ts | 73 ++-- .../WithOption/instances/json-schema.ts | 5 +- .../WithPadding/instances/json-schema.ts | 32 +- .../instances/json-schema.ts | 2 +- src/schemata.ts | 1 - src/schemata/date/DateFromIsoString.ts | 1 - tests/JsonSchema.test.ts | 352 ++++++++++++++++++ 9 files changed, 489 insertions(+), 38 deletions(-) create mode 100644 src/JsonSchema.ts create mode 100644 tests/JsonSchema.test.ts diff --git a/scripts/generate-schemables.ts b/scripts/generate-schemables.ts index 8087d39b..60698533 100644 --- a/scripts/generate-schemables.ts +++ b/scripts/generate-schemables.ts @@ -152,6 +152,7 @@ export type SchemableTypeclasses = | SchemableTypeclass<'Encoder', 'Enc', 'SchemableExt2', '1.0.0'> | SchemableTypeclass<'Arbitrary', 'Arb', 'SchemableExt1', '1.0.0'> | SchemableTypeclass<'Printer', 'P', 'SchemableExt2', '1.1.0'> + | SchemableTypeclass<'JsonSchema', 'JS', 'SchemableExt2', '1.2.0'> // #region Typeclass modules @@ -359,6 +360,7 @@ const schemableTypeclasses: ReadonlyArray = [ ['Encoder', 'Enc', 'SchemableExt2', '1.0.0', 'encoder'], ['Arbitrary', 'Arb', 'SchemableExt1', '1.0.0', 'arbitrary'], ['Printer', 'P', 'SchemableExt2', '1.1.0', 'printer'], + ['JsonSchema', 'JS', 'SchemableExt2', '1.2.0', 'json-schema'], ] const format: Build = C => C.exec('yarn format') diff --git a/src/JsonSchema.ts b/src/JsonSchema.ts new file mode 100644 index 00000000..dc884e9e --- /dev/null +++ b/src/JsonSchema.ts @@ -0,0 +1,59 @@ +/** + * SchemableExt instances for JsonSchema + * + * **Warning: DO NOT EDIT, this module is autogenerated** + * + * @since 1.2.0 + */ +import * as JS from './base/JsonSchemaBase' +import { SchemableExt2 } from './SchemableExt' +import * as WithBrand from './schemables/WithBrand/instances/json-schema' +import * as WithCheckDigit from './schemables/WithCheckDigit/instances/json-schema' +import * as WithDate from './schemables/WithDate/instances/json-schema' +import * as WithFloat from './schemables/WithFloat/instances/json-schema' +import * as WithInt from './schemables/WithInt/instances/json-schema' +import * as WithInvariant from './schemables/WithInvariant/instances/json-schema' +import * as WithJson from './schemables/WithJson/instances/json-schema' +import * as WithMap from './schemables/WithMap/instances/json-schema' +import * as WithOption from './schemables/WithOption/instances/json-schema' +import * as WithOptional from './schemables/WithOptional/instances/json-schema' +import * as WithPadding from './schemables/WithPadding/instances/json-schema' +import * as WithPattern from './schemables/WithPattern/instances/json-schema' +import * as WithRefine from './schemables/WithRefine/instances/json-schema' +import * as WithUnknownContainers from './schemables/WithUnknownContainers/instances/json-schema' +import { interpret } from './SchemaExt' +export type { + /** + * @since 1.2.0 + * @category Model + */ + JsonSchema, +} from './base/JsonSchemaBase' + +/** + * @since 1.2.0 + * @category Instances + */ +export const Schemable: SchemableExt2 = { + ...JS.Schemable, + ...WithBrand.JsonSchema, + ...WithCheckDigit.JsonSchema, + ...WithDate.JsonSchema, + ...WithFloat.JsonSchema, + ...WithInt.JsonSchema, + ...WithInvariant.JsonSchema, + ...WithJson.JsonSchema, + ...WithMap.JsonSchema, + ...WithOption.JsonSchema, + ...WithOptional.JsonSchema, + ...WithPadding.JsonSchema, + ...WithPattern.JsonSchema, + ...WithRefine.JsonSchema, + ...WithUnknownContainers.JsonSchema, +} + +/** + * @since 1.2.0 + * @category Interpreters + */ +export const getJsonSchema = interpret(Schemable) diff --git a/src/base/JsonSchemaBase.ts b/src/base/JsonSchemaBase.ts index 8b4e057d..a3be1ab5 100644 --- a/src/base/JsonSchemaBase.ts +++ b/src/base/JsonSchemaBase.ts @@ -3,6 +3,7 @@ * Models for JsonSchema as subsets of JSON Schema Draft 4, Draft 6, and Draft 7. * * @since 1.2.0 + * @see https://json-schema.org/draft/2020-12/json-schema-validation.html */ import { identity, pipe } from 'fp-ts/function' import { Const, make } from 'fp-ts/lib/Const' @@ -29,7 +30,9 @@ export type JsonSchema = | JsonBoolean | JsonNull | JsonInteger + | JsonConst | JsonLiteral + | JsonExclude | JsonStruct | JsonRecord | JsonArray @@ -83,30 +86,19 @@ class JsonBoolean { } /** @internal */ -type JsonLiteral = - | { - readonly type: 'string' - readonly const: string - } - | { - readonly type: 'number' - readonly const: number - } - | { - readonly type: 'boolean' - readonly const: boolean - } - | { - readonly type: 'null' - readonly const: null - } +interface JsonConst { + readonly const: unknown +} + +/** @internal */ +type JsonLiteral = (JsonBoolean | JsonNumber | JsonString | JsonNull) & JsonConst /** @internal */ class JsonStruct { readonly type = 'object' constructor( readonly properties: Readonly>, - readonly required: ReadonlyArray = [], + readonly required: ReadonlyArray, ) {} } @@ -129,6 +121,12 @@ class JsonArray { /** @internal */ class JsonNull { readonly type = 'null' + readonly const = null +} + +/** @internal */ +class JsonExclude { + constructor(readonly not: JsonSchema) {} } /** @internal */ @@ -200,6 +198,16 @@ export const makeIntegerSchema = ( */ export const booleanSchema: Const = make(new JsonBoolean()) +/** + * This is internal because it's not technically accurate to say: `forall value. const: + * value` is a valid json schema. However, internally, the only usage is with + * OptionFromExclude which is likely to stick with valid JSON types + * + * @internal + */ +export const makeConstSchema = (value: A): Const => + make({ const: value }) + /** * @since 1.2.0 * @category Constructors @@ -261,7 +269,26 @@ export const nullSchema = make(new JsonNull()) export const makeUnionSchema = >>( ...members: U ): Const ? A : never> => - make(new JsonUnion(members)) + make(members.length > 1 ? new JsonUnion(members) : members[0]) + +/** + * @since 1.2.0 + * @category Constructors + */ +export const makeIntersectionSchema = ( + left: Const, + right: Const, +): Const => make(new JsonIntersection([left, right])) + +/** + * @since 1.2.0 + * @category Constructors + */ +export const makeExclusionSchema = ( + exclude: Z, + schema: Const, +): Const> => + make(new JsonIntersection([schema, new JsonExclude(makeConstSchema(exclude))])) /** * @since 1.2.0 @@ -307,7 +334,9 @@ declare module 'fp-ts/lib/HKT' { export const Schemable: Schemable2 = { URI, literal: (...values) => - pipe(values, RNEA.map(makeLiteralSchema), schemata => make(new JsonUnion(schemata))), + pipe(values, RNEA.map(makeLiteralSchema), schemata => + make(schemata.length === 1 ? RNEA.head(schemata) : new JsonUnion(schemata)), + ), string: makeStringSchema(), number: makeNumberSchema(), boolean: booleanSchema, @@ -325,8 +354,8 @@ export const Schemable: Schemable2 = { intersect: right => left => make(new JsonIntersection([left, right])), sum: () => members => make( - new JsonUnion( - pipe( + makeUnionSchema( + ...pipe( members as Readonly>>, RR.collect(Str.Ord)((_, a) => a), ), diff --git a/src/schemables/WithOption/instances/json-schema.ts b/src/schemables/WithOption/instances/json-schema.ts index 6cb9e9e1..a7850752 100644 --- a/src/schemables/WithOption/instances/json-schema.ts +++ b/src/schemables/WithOption/instances/json-schema.ts @@ -15,9 +15,8 @@ export const JsonSchema: WithOption2 = { optionFromExclude: (exclude, jsA) => /* * TODO @jacob-alford: Exclude can technically _not_ be a literal value, and that - * might cause problems with non-literal types turning into `boolean`s with the way - * `literal` is constructed + * might cause problems if the excluded value is not a valid schema value, such as undefined. */ // @ts-expect-error -- typelevel difference - JS.makeUnionSchema(JS.makeLiteralSchema(exclude), jsA), + JS.makeExclusionSchema(exclude, jsA), } diff --git a/src/schemables/WithPadding/instances/json-schema.ts b/src/schemables/WithPadding/instances/json-schema.ts index 42ea9aaf..f344798a 100644 --- a/src/schemables/WithPadding/instances/json-schema.ts +++ b/src/schemables/WithPadding/instances/json-schema.ts @@ -14,32 +14,44 @@ import { match } from '../utils' * @category Instances */ export const JsonSchema: WithPadding2 = { - padLeft: length => () => + padLeft: length => stringSchema => pipe( length, match({ MaxLength: ({ maxLength }) => typeof maxLength === 'number' - ? JS.makeStringSchema(undefined, maxLength) - : JS.makeStringSchema(), + ? JS.makeIntersectionSchema( + JS.makeStringSchema(undefined, maxLength), + stringSchema, + ) + : JS.makeIntersectionSchema(JS.makeStringSchema(), stringSchema), ExactLength: ({ exactLength }) => typeof exactLength === 'number' - ? JS.makeStringSchema(exactLength, exactLength) - : JS.makeStringSchema(), + ? JS.makeIntersectionSchema( + JS.makeStringSchema(exactLength, exactLength), + stringSchema, + ) + : JS.makeIntersectionSchema(JS.makeStringSchema(), stringSchema), }), ), - padRight: length => () => + padRight: length => stringSchema => pipe( length, match({ MaxLength: ({ maxLength }) => typeof maxLength === 'number' - ? JS.makeStringSchema(undefined, maxLength) - : JS.makeStringSchema(), + ? JS.makeIntersectionSchema( + JS.makeStringSchema(undefined, maxLength), + stringSchema, + ) + : JS.makeIntersectionSchema(JS.makeStringSchema(), stringSchema), ExactLength: ({ exactLength }) => typeof exactLength === 'number' - ? JS.makeStringSchema(exactLength, exactLength) - : JS.makeStringSchema(), + ? JS.makeIntersectionSchema( + JS.makeStringSchema(exactLength, exactLength), + stringSchema, + ) + : JS.makeIntersectionSchema(JS.makeStringSchema(), stringSchema), }), ), } diff --git a/src/schemables/WithUnknownContainers/instances/json-schema.ts b/src/schemables/WithUnknownContainers/instances/json-schema.ts index df79fe14..a76a418f 100644 --- a/src/schemables/WithUnknownContainers/instances/json-schema.ts +++ b/src/schemables/WithUnknownContainers/instances/json-schema.ts @@ -6,7 +6,7 @@ import { WithUnknownContainers2 } from '../definition' * @since 1.2.0 * @category Instances */ -export const Printer: WithUnknownContainers2 = { +export const JsonSchema: WithUnknownContainers2 = { UnknownArray: JS.makeArraySchema(JS.emptySchema), UnknownRecord: JS.makeRecordSchema(JS.emptySchema), } diff --git a/src/schemata.ts b/src/schemata.ts index 061775e9..a7c98fb6 100644 --- a/src/schemata.ts +++ b/src/schemata.ts @@ -177,7 +177,6 @@ export { * * Notable features: * - * - Requires `T` separator between date and time * - Requires padded months, days, hours, minutes, and seconds * - Can be configured to require a time, time and timezone offset (e.g. `Z` or `±05:00`) * or neither (default is require both). diff --git a/src/schemata/date/DateFromIsoString.ts b/src/schemata/date/DateFromIsoString.ts index fdd2326d..6e5470d7 100644 --- a/src/schemata/date/DateFromIsoString.ts +++ b/src/schemata/date/DateFromIsoString.ts @@ -294,7 +294,6 @@ export type DateFromIsoStringS = ( * * Notable features: * - * - Requires `T` separator between date and time * - Requires padded months, days, hours, minutes, and seconds * - Can be configured to require a time, time and timezone offset (e.g. `Z` or `±05:00`) or * neither (default is require both). diff --git a/tests/JsonSchema.test.ts b/tests/JsonSchema.test.ts new file mode 100644 index 00000000..142daad7 --- /dev/null +++ b/tests/JsonSchema.test.ts @@ -0,0 +1,352 @@ +import * as Str from 'fp-ts/string' + +import { getJsonSchema } from '../src/JsonSchema' +import * as S from '../src/schemata' + +describe('JsonSchema', () => { + const testSchema = S.Readonly( + S.Struct({ + literal: S.Literal('string', 5, true, null), + nully: S.Nullable(S.Int({ min: 0, max: 1 })), + partial: S.Partial({ + optNull: S.OptionFromNullable(S.Float({ min: 0, max: 1 })), + optUndefined: S.OptionFromUndefined(S.Float({ min: 0, max: 1 })), + }), + rec: S.Record(S.CreditCard), + arr: S.Array(S.Json.jsonString), + tup: S.Tuple(S.JsonFromString, S.Number), + sum: S.Sum('type')({ + a: S.Struct({ type: S.Literal('a'), a: S.Boolean }), + b: S.Struct({ type: S.Literal('b'), b: S.Lazy('Sum[b].b', () => S.Natural) }), + }), + intersection: S.Intersection(S.Struct({ a: S.NonPositiveInt }))( + S.Struct({ b: S.NonNegativeFloat, c: S.NonPositiveFloat }), + ), + map: S.Map(Str.Ord, S.String, S.Float()), + int: S.Int(), + option: S.Option(69420, S.Number), + singularSum: S.Sum('type')({ + a: S.Struct({ + type: S.Literal('a'), + a: S.NonEmptyArray( + S.Padding.padRight( + { by: 'ExactLength', exactLength: 1 }, + '*', + )( + S.Padding.padRight( + { by: 'MaxLength', maxLength: 2 }, + '=', + )( + S.Padding.padLeft( + { by: 'MaxLength', maxLength: 3 }, + '=', + )( + S.Padding.padLeft( + { by: 'ExactLength', exactLength: 4 }, + '=', + )( + S.Padding.padRight( + { by: 'ExactLength', exactLength: () => 0 }, + '&', + )( + S.Padding.padRight( + { by: 'MaxLength', maxLength: () => 0 }, + '=', + )( + S.Padding.padLeft( + { by: 'MaxLength', maxLength: () => 0 }, + '=', + )( + S.Padding.padLeft( + { by: 'ExactLength', exactLength: () => 0 }, + '=', + )(S.String), + ), + ), + ), + ), + ), + ), + ), + ), + }), + }), + }), + ) + const jsonSchema = getJsonSchema(testSchema) + const testValue = JSON.parse(JSON.stringify(jsonSchema)) as any + test('struct', () => { + expect(testValue.type).toBe('object') + expect(testValue.required).toStrictEqual([ + 'literal', + 'nully', + 'partial', + 'rec', + 'arr', + 'tup', + 'sum', + 'intersection', + 'map', + 'int', + 'option', + 'singularSum', + ]) + }) + test('literal', () => { + expect(testValue.properties.literal).toStrictEqual({ + oneOf: [ + { type: 'string', const: 'string' } as any, + { type: 'number', const: 5 }, + { type: 'boolean', const: true }, + { type: 'null', const: null }, + ], + }) + }) + test('nullable', () => { + expect(testValue.properties.nully).toStrictEqual({ + oneOf: [ + { type: 'integer', minimum: 0, maximum: 1 }, + { type: 'null', const: null }, + ], + }) + }) + test('partial', () => { + expect(testValue.properties.partial).toStrictEqual({ + type: 'object', + properties: { + optNull: { + oneOf: [ + { type: 'number', minimum: 0, maximum: 1 }, + { type: 'null', const: null }, + ], + }, + optUndefined: {}, + }, + required: [], + }) + }) + test('record', () => { + expect(testValue.properties.rec).toStrictEqual({ + type: 'object', + additionalProperties: { + description: 'CreditCard', + type: 'string', + pattern: + '^(4(\\d{12}|\\d{15})|(5[1-5]\\d{4}|(222)[1-9]\\d{2}|(22)[3-9]\\d{3}|(2)[3-6]\\d{4}|(27)[01]\\d{3}|(2720)\\d{2})\\d{10}|3[47]\\d{13}|(3(0([0-5]\\d{5}|(95)\\d{4})|[89]\\d{6})\\d{8,11}|(36)\\d{6}\\d{6,11})|((6011)(0[5-9]\\d{2}|[2-4]\\d{3}|(74)\\d{2}|(7)[7-9]\\d{2}|(8)[6-9]\\d{2}|(9)\\d{3})|(64)[4-9]\\d{5}|(650)[0-5]\\d{4}|(65060)[1-9]\\d{2}|(65061)[1-9]\\d{2}|(6506)[2-9]\\d{3}|(650)[7-9]\\d{4}|(65)[1-9]\\d{5})\\d{8,11}|((352)[89]\\d{4}|(35)[3-8]\\d{5})\\d{8,11}|(((60)|(65)|(81)|(82))\\d{14}|(508)\\d{14})|(62)(2((12)[6-9]\\d{2}|1[3-9]\\d{3}|[2-8]\\d|(9)[01]\\d{3}|(92)[0-5]\\d{2})|[4-6]\\d{5}|(8)[2-8]\\d{4})\\d{8,11})$', + }, + }) + }) + test('array', () => { + expect(testValue.properties.arr).toStrictEqual({ + type: 'array', + items: { + type: 'string', + contentMediaType: 'application/json', + }, + }) + }) + test('tuple', () => { + expect(testValue.properties.tup).toStrictEqual({ + type: 'array', + items: [ + { + type: 'string', + contentMediaType: 'application/json', + }, + { + type: 'number', + }, + ], + minItems: 2, + maxItems: 2, + }) + }) + test('sum', () => { + expect(testValue.properties.sum).toStrictEqual({ + oneOf: [ + { + type: 'object', + properties: { + type: { + type: 'string', + const: 'a', + }, + a: { + type: 'boolean', + }, + }, + required: ['type', 'a'], + }, + { + type: 'object', + properties: { + type: { + type: 'string', + const: 'b', + }, + b: { + type: 'integer', + maximum: 9007199254740991, + minimum: 0, + }, + }, + required: ['type', 'b'], + }, + ], + }) + }) + test('intersection', () => { + expect(testValue.properties.intersection).toStrictEqual({ + allOf: [ + { + type: 'object', + properties: { + a: { + type: 'integer', + maximum: 0, + minimum: -9007199254740991, + }, + }, + required: ['a'], + }, + { + type: 'object', + properties: { + b: { + type: 'number', + maximum: 1.7976931348623157e308, + minimum: 0, + }, + c: { + type: 'number', + maximum: 0, + minimum: -1.7976931348623157e308, + }, + }, + required: ['b', 'c'], + }, + ], + }) + }) + test('map', () => { + expect(testValue.properties.map).toStrictEqual({ + type: 'array', + items: { + type: 'array', + items: [ + { + type: 'string', + }, + { + type: 'number', + maximum: 1.7976931348623157e308, + minimum: -1.7976931348623157e308, + }, + ], + minItems: 2, + maxItems: 2, + }, + }) + }) + test('int', () => { + expect(testValue.properties.int).toStrictEqual({ + type: 'integer', + maximum: 9007199254740991, + minimum: -9007199254740991, + }) + }) + test('option', () => { + expect(testValue.properties.option).toStrictEqual({ + allOf: [ + { + type: 'number', + }, + { + not: { + const: 69420, + }, + }, + ], + }) + }) + test('singularSum', () => { + expect(testValue.properties.singularSum).toStrictEqual({ + type: 'object', + properties: { + type: { + type: 'string', + const: 'a', + }, + a: { + type: 'array', + items: { + allOf: [ + { + type: 'string', + maxLength: 1, + minLength: 1, + }, + { + allOf: [ + { + type: 'string', + maxLength: 2, + }, + { + allOf: [ + { + type: 'string', + maxLength: 3, + }, + { + allOf: [ + { + type: 'string', + maxLength: 4, + minLength: 4, + }, + { + allOf: [ + { + type: 'string', + }, + { + allOf: [ + { + type: 'string', + }, + { + allOf: [ + { + type: 'string', + }, + { + allOf: [ + { + type: 'string', + }, + { + type: 'string', + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + }, + }, + }, + required: ['type', 'a'], + }) + }) +}) From 5ba16c83513a0b64760ee32ff6463729f0a8b66c Mon Sep 17 00:00:00 2001 From: Jacob Alford Date: Tue, 3 Jan 2023 17:39:03 -0700 Subject: [PATCH 06/21] [#137] add tests for date --- tests/JsonSchema.test.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/JsonSchema.test.ts b/tests/JsonSchema.test.ts index 142daad7..10bd8b2a 100644 --- a/tests/JsonSchema.test.ts +++ b/tests/JsonSchema.test.ts @@ -14,6 +14,7 @@ describe('JsonSchema', () => { }), rec: S.Record(S.CreditCard), arr: S.Array(S.Json.jsonString), + tup: S.Tuple(S.JsonFromString, S.Number), sum: S.Sum('type')({ a: S.Struct({ type: S.Literal('a'), a: S.Boolean }), @@ -22,6 +23,8 @@ describe('JsonSchema', () => { intersection: S.Intersection(S.Struct({ a: S.NonPositiveInt }))( S.Struct({ b: S.NonNegativeFloat, c: S.NonPositiveFloat }), ), + date: S.Date.dateFromString, + isoDate: S.DateFromIsoString(), map: S.Map(Str.Ord, S.String, S.Float()), int: S.Int(), option: S.Option(69420, S.Number), @@ -86,6 +89,8 @@ describe('JsonSchema', () => { 'tup', 'sum', 'intersection', + 'date', + 'isoDate', 'map', 'int', 'option', @@ -195,6 +200,20 @@ describe('JsonSchema', () => { ], }) }) + test('date', () => { + expect(testValue.properties.date).toStrictEqual({ + type: 'string', + format: 'date', + }) + }) + test('isoDate', () => { + expect(testValue.properties.isoDate).toStrictEqual({ + type: 'string', + description: 'IsoDateString', + pattern: + '^(((\\d{4}|[+\\x2d]\\d{6})-((0[1-9])|(1[0-2]))-((0[1-9])|(1\\d|[2]\\d|3[0-1]))|((\\d{4}|[+\\x2d]\\d{6})-((0[1-9])|(1[0-2])))|\\d{4}|[+\\x2d]\\d{6})(T| )(((0\\d)|(1\\d|2[0-3])):((0\\d)|(1\\d|[2-4]\\d|5\\d)):((0\\d)|(1\\d|[2-4]\\d|5\\d))\\.(\\d+?)|((0\\d)|(1\\d|2[0-3])):((0\\d)|(1\\d|[2-4]\\d|5\\d)):((0\\d)|(1\\d|[2-4]\\d|5\\d))|((0\\d)|(1\\d|2[0-3])):((0\\d)|(1\\d|[2-4]\\d|5\\d)))(Z|[+\\x2d]((0\\d)|(1\\d|2[0-3])):((0\\d)|(1\\d|[2-4]\\d|5\\d))))$', + }) + }) test('intersection', () => { expect(testValue.properties.intersection).toStrictEqual({ allOf: [ From 8c264dccee56abc45e7b67ce7a40057a34e4941f Mon Sep 17 00:00:00 2001 From: Jacob Alford Date: Tue, 3 Jan 2023 17:39:49 -0700 Subject: [PATCH 07/21] [#137] remove dep for json-schema --- package.json | 3 --- yarn.lock | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/package.json b/package.json index 12482fb6..95b3c60f 100644 --- a/package.json +++ b/package.json @@ -101,8 +101,5 @@ "ts-jest": "^26.5.3", "ts-node": "^10.8.1", "typescript": "^4.2.3" - }, - "dependencies": { - "@types/json-schema": "^7.0.11" } } diff --git a/yarn.lock b/yarn.lock index cb2d928c..b8103f7d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1741,7 +1741,7 @@ jest-diff "^26.0.0" pretty-format "^26.0.0" -"@types/json-schema@^7.0.11", "@types/json-schema@^7.0.7": +"@types/json-schema@^7.0.7": version "7.0.11" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3" integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ== From 4e557467e33ed39db08511016acc24872fe4fa0f Mon Sep 17 00:00:00 2001 From: Jacob Alford Date: Tue, 3 Jan 2023 18:03:58 -0700 Subject: [PATCH 08/21] [#137] add WithAnnotation --- src/Arbitrary.ts | 2 + src/Decoder.ts | 2 + src/Encoder.ts | 2 + src/Eq.ts | 2 + src/Guard.ts | 2 + src/JsonSchema.ts | 2 + src/Printer.ts | 2 + src/SchemableExt.ts | 10 ++++ src/TaskDecoder.ts | 2 + src/Type.ts | 2 + src/schemables/WithAnnotation/definition.ts | 51 +++++++++++++++++++ .../WithAnnotation/instances/arbitrary.ts | 18 +++++++ .../WithAnnotation/instances/decoder.ts | 18 +++++++ .../WithAnnotation/instances/encoder.ts | 18 +++++++ src/schemables/WithAnnotation/instances/eq.ts | 18 +++++++ .../WithAnnotation/instances/guard.ts | 18 +++++++ .../WithAnnotation/instances/json-schema.ts | 16 ++++++ .../WithAnnotation/instances/printer.ts | 18 +++++++ .../WithAnnotation/instances/schema.ts | 16 ++++++ .../WithAnnotation/instances/task-decoder.ts | 18 +++++++ .../WithAnnotation/instances/type.ts | 18 +++++++ src/schemables/WithAnnotation/utils.ts | 11 ++++ src/schemata.ts | 8 +++ 23 files changed, 274 insertions(+) create mode 100644 src/schemables/WithAnnotation/definition.ts create mode 100644 src/schemables/WithAnnotation/instances/arbitrary.ts create mode 100644 src/schemables/WithAnnotation/instances/decoder.ts create mode 100644 src/schemables/WithAnnotation/instances/encoder.ts create mode 100644 src/schemables/WithAnnotation/instances/eq.ts create mode 100644 src/schemables/WithAnnotation/instances/guard.ts create mode 100644 src/schemables/WithAnnotation/instances/json-schema.ts create mode 100644 src/schemables/WithAnnotation/instances/printer.ts create mode 100644 src/schemables/WithAnnotation/instances/schema.ts create mode 100644 src/schemables/WithAnnotation/instances/task-decoder.ts create mode 100644 src/schemables/WithAnnotation/instances/type.ts create mode 100644 src/schemables/WithAnnotation/utils.ts diff --git a/src/Arbitrary.ts b/src/Arbitrary.ts index bd250a14..30ff43e9 100644 --- a/src/Arbitrary.ts +++ b/src/Arbitrary.ts @@ -7,6 +7,7 @@ */ import * as Arb from './base/ArbitraryBase' import { SchemableExt1 } from './SchemableExt' +import * as WithAnnotation from './schemables/WithAnnotation/instances/arbitrary' import * as WithBrand from './schemables/WithBrand/instances/arbitrary' import * as WithCheckDigit from './schemables/WithCheckDigit/instances/arbitrary' import * as WithDate from './schemables/WithDate/instances/arbitrary' @@ -36,6 +37,7 @@ export type { */ export const Schemable: SchemableExt1 = { ...Arb.Schemable, + ...WithAnnotation.Arbitrary, ...WithBrand.Arbitrary, ...WithCheckDigit.Arbitrary, ...WithDate.Arbitrary, diff --git a/src/Decoder.ts b/src/Decoder.ts index c6df4f4d..8eaa7b58 100644 --- a/src/Decoder.ts +++ b/src/Decoder.ts @@ -7,6 +7,7 @@ */ import * as D from './base/DecoderBase' import { SchemableExt2C } from './SchemableExt' +import * as WithAnnotation from './schemables/WithAnnotation/instances/decoder' import * as WithBrand from './schemables/WithBrand/instances/decoder' import * as WithCheckDigit from './schemables/WithCheckDigit/instances/decoder' import * as WithDate from './schemables/WithDate/instances/decoder' @@ -36,6 +37,7 @@ export type { */ export const Schemable: SchemableExt2C = { ...D.Schemable, + ...WithAnnotation.Decoder, ...WithBrand.Decoder, ...WithCheckDigit.Decoder, ...WithDate.Decoder, diff --git a/src/Encoder.ts b/src/Encoder.ts index dfd5a60d..998a406f 100644 --- a/src/Encoder.ts +++ b/src/Encoder.ts @@ -7,6 +7,7 @@ */ import * as Enc from './base/EncoderBase' import { SchemableExt2 } from './SchemableExt' +import * as WithAnnotation from './schemables/WithAnnotation/instances/encoder' import * as WithBrand from './schemables/WithBrand/instances/encoder' import * as WithCheckDigit from './schemables/WithCheckDigit/instances/encoder' import * as WithDate from './schemables/WithDate/instances/encoder' @@ -36,6 +37,7 @@ export type { */ export const Schemable: SchemableExt2 = { ...Enc.Schemable, + ...WithAnnotation.Encoder, ...WithBrand.Encoder, ...WithCheckDigit.Encoder, ...WithDate.Encoder, diff --git a/src/Eq.ts b/src/Eq.ts index 9e67241c..a2933118 100644 --- a/src/Eq.ts +++ b/src/Eq.ts @@ -7,6 +7,7 @@ */ import * as Eq from './base/EqBase' import { SchemableExt1 } from './SchemableExt' +import * as WithAnnotation from './schemables/WithAnnotation/instances/eq' import * as WithBrand from './schemables/WithBrand/instances/eq' import * as WithCheckDigit from './schemables/WithCheckDigit/instances/eq' import * as WithDate from './schemables/WithDate/instances/eq' @@ -36,6 +37,7 @@ export type { */ export const Schemable: SchemableExt1 = { ...Eq.Schemable, + ...WithAnnotation.Eq, ...WithBrand.Eq, ...WithCheckDigit.Eq, ...WithDate.Eq, diff --git a/src/Guard.ts b/src/Guard.ts index 11a7477b..1de168f9 100644 --- a/src/Guard.ts +++ b/src/Guard.ts @@ -7,6 +7,7 @@ */ import * as G from './base/GuardBase' import { SchemableExt1 } from './SchemableExt' +import * as WithAnnotation from './schemables/WithAnnotation/instances/guard' import * as WithBrand from './schemables/WithBrand/instances/guard' import * as WithCheckDigit from './schemables/WithCheckDigit/instances/guard' import * as WithDate from './schemables/WithDate/instances/guard' @@ -36,6 +37,7 @@ export type { */ export const Schemable: SchemableExt1 = { ...G.Schemable, + ...WithAnnotation.Guard, ...WithBrand.Guard, ...WithCheckDigit.Guard, ...WithDate.Guard, diff --git a/src/JsonSchema.ts b/src/JsonSchema.ts index dc884e9e..8eb5f9f5 100644 --- a/src/JsonSchema.ts +++ b/src/JsonSchema.ts @@ -7,6 +7,7 @@ */ import * as JS from './base/JsonSchemaBase' import { SchemableExt2 } from './SchemableExt' +import * as WithAnnotation from './schemables/WithAnnotation/instances/json-schema' import * as WithBrand from './schemables/WithBrand/instances/json-schema' import * as WithCheckDigit from './schemables/WithCheckDigit/instances/json-schema' import * as WithDate from './schemables/WithDate/instances/json-schema' @@ -36,6 +37,7 @@ export type { */ export const Schemable: SchemableExt2 = { ...JS.Schemable, + ...WithAnnotation.JsonSchema, ...WithBrand.JsonSchema, ...WithCheckDigit.JsonSchema, ...WithDate.JsonSchema, diff --git a/src/Printer.ts b/src/Printer.ts index be957088..ed190d3a 100644 --- a/src/Printer.ts +++ b/src/Printer.ts @@ -7,6 +7,7 @@ */ import * as P from './base/PrinterBase' import { SchemableExt2 } from './SchemableExt' +import * as WithAnnotation from './schemables/WithAnnotation/instances/printer' import * as WithBrand from './schemables/WithBrand/instances/printer' import * as WithCheckDigit from './schemables/WithCheckDigit/instances/printer' import * as WithDate from './schemables/WithDate/instances/printer' @@ -36,6 +37,7 @@ export type { */ export const Schemable: SchemableExt2 = { ...P.Schemable, + ...WithAnnotation.Printer, ...WithBrand.Printer, ...WithCheckDigit.Printer, ...WithDate.Printer, diff --git a/src/SchemableExt.ts b/src/SchemableExt.ts index 688bd45a..07666e30 100644 --- a/src/SchemableExt.ts +++ b/src/SchemableExt.ts @@ -9,6 +9,12 @@ import { URIS, URIS2 } from 'fp-ts/HKT' import { Schemable1, Schemable2C } from 'io-ts/Schemable' import { Schemable2, SchemableHKT2 } from './base/SchemableBase' +import { + WithAnnotation1, + WithAnnotation2, + WithAnnotation2C, + WithAnnotationHKT2, +} from './schemables/WithAnnotation/definition' import { WithBrand1, WithBrand2, @@ -100,6 +106,7 @@ import { */ export interface SchemableExt extends SchemableHKT2, + WithAnnotationHKT2, WithBrandHKT2, WithCheckDigitHKT2, WithDateHKT2, @@ -121,6 +128,7 @@ export interface SchemableExt */ export interface SchemableExt1 extends Schemable1, + WithAnnotation1, WithBrand1, WithCheckDigit1, WithDate1, @@ -142,6 +150,7 @@ export interface SchemableExt1 */ export interface SchemableExt2 extends Schemable2, + WithAnnotation2, WithBrand2, WithCheckDigit2, WithDate2, @@ -163,6 +172,7 @@ export interface SchemableExt2 */ export interface SchemableExt2C extends Schemable2C, + WithAnnotation2C, WithBrand2C, WithCheckDigit2C, WithDate2C, diff --git a/src/TaskDecoder.ts b/src/TaskDecoder.ts index 8b11ab96..460af056 100644 --- a/src/TaskDecoder.ts +++ b/src/TaskDecoder.ts @@ -7,6 +7,7 @@ */ import * as TD from './base/TaskDecoderBase' import { SchemableExt2C } from './SchemableExt' +import * as WithAnnotation from './schemables/WithAnnotation/instances/task-decoder' import * as WithBrand from './schemables/WithBrand/instances/task-decoder' import * as WithCheckDigit from './schemables/WithCheckDigit/instances/task-decoder' import * as WithDate from './schemables/WithDate/instances/task-decoder' @@ -36,6 +37,7 @@ export type { */ export const Schemable: SchemableExt2C = { ...TD.Schemable, + ...WithAnnotation.TaskDecoder, ...WithBrand.TaskDecoder, ...WithCheckDigit.TaskDecoder, ...WithDate.TaskDecoder, diff --git a/src/Type.ts b/src/Type.ts index 14aaab18..7377bfd2 100644 --- a/src/Type.ts +++ b/src/Type.ts @@ -7,6 +7,7 @@ */ import * as t from './base/TypeBase' import { SchemableExt1 } from './SchemableExt' +import * as WithAnnotation from './schemables/WithAnnotation/instances/type' import * as WithBrand from './schemables/WithBrand/instances/type' import * as WithCheckDigit from './schemables/WithCheckDigit/instances/type' import * as WithDate from './schemables/WithDate/instances/type' @@ -36,6 +37,7 @@ export type { */ export const Schemable: SchemableExt1 = { ...t.Schemable, + ...WithAnnotation.Type, ...WithBrand.Type, ...WithCheckDigit.Type, ...WithDate.Type, diff --git a/src/schemables/WithAnnotation/definition.ts b/src/schemables/WithAnnotation/definition.ts new file mode 100644 index 00000000..3ed25373 --- /dev/null +++ b/src/schemables/WithAnnotation/definition.ts @@ -0,0 +1,51 @@ +/** + * Schemable for annotating a JSON Schema. Interpretation using interpreters other than + * JsonSchema will not change the derivation. + * + * @since 1.2.0 + */ +import { HKT2, Kind, Kind2, URIS, URIS2 } from 'fp-ts/HKT' + +/** + * @since 1.2.0 + * @category Model + */ +export interface WithAnnotationHKT2 { + readonly annotate: ( + name?: string, + description?: string, + ) => (schema: HKT2) => HKT2 +} + +/** + * @since 1.2.0 + * @category Model + */ +export interface WithAnnotation1 { + readonly annotate: ( + name?: string, + description?: string, + ) => (schema: Kind) => Kind +} + +/** + * @since 1.2.0 + * @category Model + */ +export interface WithAnnotation2 { + readonly annotate: ( + name?: string, + description?: string, + ) => (schema: Kind2) => Kind2 +} + +/** + * @since 1.2.0 + * @category Model + */ +export interface WithAnnotation2C { + readonly annotate: ( + name?: string, + description?: string, + ) => (schema: Kind2) => Kind2 +} diff --git a/src/schemables/WithAnnotation/instances/arbitrary.ts b/src/schemables/WithAnnotation/instances/arbitrary.ts new file mode 100644 index 00000000..1b1a0026 --- /dev/null +++ b/src/schemables/WithAnnotation/instances/arbitrary.ts @@ -0,0 +1,18 @@ +/** + * Schemable for annotating a JSON Schema. Interpretation using interpreters other than + * JsonSchema will not change the derivation. + * + * @since 1.2.0 + */ +import { constant, identity } from 'fp-ts/function' + +import * as Arb from '../../../base/ArbitraryBase' +import { WithAnnotation1 } from '../definition' + +/** + * @since 1.2.0 + * @category Instances + */ +export const Arbitrary: WithAnnotation1 = { + annotate: constant(identity), +} diff --git a/src/schemables/WithAnnotation/instances/decoder.ts b/src/schemables/WithAnnotation/instances/decoder.ts new file mode 100644 index 00000000..7710f1ca --- /dev/null +++ b/src/schemables/WithAnnotation/instances/decoder.ts @@ -0,0 +1,18 @@ +/** + * Schemable for annotating a JSON Schema. Interpretation using interpreters other than + * JsonSchema will not change the derivation. + * + * @since 1.2.0 + */ +import { constant, identity } from 'fp-ts/function' + +import * as D from '../../../base/DecoderBase' +import { WithAnnotation2C } from '../definition' + +/** + * @since 1.2.0 + * @category Instances + */ +export const Decoder: WithAnnotation2C = { + annotate: constant(identity), +} diff --git a/src/schemables/WithAnnotation/instances/encoder.ts b/src/schemables/WithAnnotation/instances/encoder.ts new file mode 100644 index 00000000..cb7ca848 --- /dev/null +++ b/src/schemables/WithAnnotation/instances/encoder.ts @@ -0,0 +1,18 @@ +/** + * Schemable for annotating a JSON Schema. Interpretation using interpreters other than + * JsonSchema will not change the derivation. + * + * @since 1.2.0 + */ +import { constant, identity } from 'fp-ts/function' + +import * as Enc from '../../../base/EncoderBase' +import { WithAnnotation2 } from '../definition' + +/** + * @since 1.2.0 + * @category Instances + */ +export const Encoder: WithAnnotation2 = { + annotate: constant(identity), +} diff --git a/src/schemables/WithAnnotation/instances/eq.ts b/src/schemables/WithAnnotation/instances/eq.ts new file mode 100644 index 00000000..bff7dc7b --- /dev/null +++ b/src/schemables/WithAnnotation/instances/eq.ts @@ -0,0 +1,18 @@ +/** + * Schemable for annotating a JSON Schema. Interpretation using interpreters other than + * JsonSchema will not change the derivation. + * + * @since 1.2.0 + */ +import * as Eq_ from 'fp-ts/Eq' +import { constant, identity } from 'fp-ts/function' + +import { WithAnnotation1 } from '../definition' + +/** + * @since 1.2.0 + * @category Instances + */ +export const Eq: WithAnnotation1 = { + annotate: constant(identity), +} diff --git a/src/schemables/WithAnnotation/instances/guard.ts b/src/schemables/WithAnnotation/instances/guard.ts new file mode 100644 index 00000000..7111fd46 --- /dev/null +++ b/src/schemables/WithAnnotation/instances/guard.ts @@ -0,0 +1,18 @@ +/** + * Schemable for annotating a JSON Schema. Interpretation using interpreters other than + * JsonSchema will not change the derivation. + * + * @since 1.2.0 + */ +import { constant, identity } from 'fp-ts/function' + +import * as G from '../../../base/GuardBase' +import { WithAnnotation1 } from '../definition' + +/** + * @since 1.2.0 + * @category Instances + */ +export const Guard: WithAnnotation1 = { + annotate: constant(identity), +} diff --git a/src/schemables/WithAnnotation/instances/json-schema.ts b/src/schemables/WithAnnotation/instances/json-schema.ts new file mode 100644 index 00000000..2eda603e --- /dev/null +++ b/src/schemables/WithAnnotation/instances/json-schema.ts @@ -0,0 +1,16 @@ +/** + * Schemable for annotating a JSON Schema. Interpretation using interpreters other than + * JsonSchema will not change the derivation. + * + * @since 1.2.0 + */ +import * as JS from '../../../base/JsonSchemaBase' +import { WithAnnotation2 } from '../definition' + +/** + * @since 1.2.0 + * @category Instances + */ +export const JsonSchema: WithAnnotation2 = { + annotate: JS.annotate, +} diff --git a/src/schemables/WithAnnotation/instances/printer.ts b/src/schemables/WithAnnotation/instances/printer.ts new file mode 100644 index 00000000..fa3d89d4 --- /dev/null +++ b/src/schemables/WithAnnotation/instances/printer.ts @@ -0,0 +1,18 @@ +/** + * Schemable for annotating a JSON Schema. Interpretation using interpreters other than + * JsonSchema will not change the derivation. + * + * @since 1.2.0 + */ +import { constant, identity } from 'fp-ts/function' + +import * as P from '../../../base/PrinterBase' +import { WithAnnotation2 } from '../definition' + +/** + * @since 1.2.0 + * @category Instances + */ +export const Printer: WithAnnotation2 = { + annotate: constant(identity), +} diff --git a/src/schemables/WithAnnotation/instances/schema.ts b/src/schemables/WithAnnotation/instances/schema.ts new file mode 100644 index 00000000..5e63f026 --- /dev/null +++ b/src/schemables/WithAnnotation/instances/schema.ts @@ -0,0 +1,16 @@ +/** + * Schemable for annotating a JSON Schema. Interpretation using interpreters other than + * JsonSchema will not change the derivation. + * + * @since 1.2.0 + */ +import * as SC from '../../../SchemaExt' + +/** + * @since 1.2.0 + * @category Combinators + */ +export const Schema = + (name?: string, description?: string) => + (schema: SC.SchemaExt): SC.SchemaExt => + SC.make(s => s.annotate(name, description)(schema(s))) diff --git a/src/schemables/WithAnnotation/instances/task-decoder.ts b/src/schemables/WithAnnotation/instances/task-decoder.ts new file mode 100644 index 00000000..138ce14f --- /dev/null +++ b/src/schemables/WithAnnotation/instances/task-decoder.ts @@ -0,0 +1,18 @@ +/** + * Schemable for annotating a JSON Schema. Interpretation using interpreters other than + * JsonSchema will not change the derivation. + * + * @since 1.2.0 + */ +import { constant, identity } from 'fp-ts/function' + +import * as TD from '../../../base/TaskDecoderBase' +import { WithAnnotation2C } from '../definition' + +/** + * @since 1.2.0 + * @category Instances + */ +export const TaskDecoder: WithAnnotation2C = { + annotate: constant(identity), +} diff --git a/src/schemables/WithAnnotation/instances/type.ts b/src/schemables/WithAnnotation/instances/type.ts new file mode 100644 index 00000000..1b6cc31d --- /dev/null +++ b/src/schemables/WithAnnotation/instances/type.ts @@ -0,0 +1,18 @@ +/** + * Schemable for annotating a JSON Schema. Interpretation using interpreters other than + * JsonSchema will not change the derivation. + * + * @since 1.2.0 + */ +import { constant, identity } from 'fp-ts/function' + +import * as t from '../../../base/TypeBase' +import { WithAnnotation1 } from '../definition' + +/** + * @since 1.2.0 + * @category Instances + */ +export const Type: WithAnnotation1 = { + annotate: constant(identity), +} diff --git a/src/schemables/WithAnnotation/utils.ts b/src/schemables/WithAnnotation/utils.ts new file mode 100644 index 00000000..ec5ece9b --- /dev/null +++ b/src/schemables/WithAnnotation/utils.ts @@ -0,0 +1,11 @@ +/** @since 1.0.0 */ + +/** @internal */ +export const replaceCharAt = (s: string, i: number, c: string): string => + s.substring(0, i) + c + s.substring(i + 1) + +/** @internal */ +export const locationToIndex = ( + s: string, + location: number | ((s: string) => number), +): number => (typeof location === 'number' ? location : location(s)) diff --git a/src/schemata.ts b/src/schemata.ts index a7c98fb6..738d9c55 100644 --- a/src/schemata.ts +++ b/src/schemata.ts @@ -23,6 +23,14 @@ export * from './base/SchemaBase' /** Schemables */ +export { + /** + * Schemable for annotating a JSON Schema. Interpretation using interpreters other than + * JsonSchema will not change the derivation. + * + * @since 1.2.0 + */ Schema as Annotation, +} from './schemables/WithAnnotation/instances/schema' export { /** * Schemable for constructing a branded newtype From 3a4b4999eab1988595fb4287468e43d6a2083171 Mon Sep 17 00:00:00 2001 From: Jacob Alford Date: Tue, 3 Jan 2023 18:12:48 -0700 Subject: [PATCH 09/21] [#137] rename to WithAnnotate, add test --- src/Arbitrary.ts | 4 ++-- src/Decoder.ts | 4 ++-- src/Encoder.ts | 4 ++-- src/Eq.ts | 4 ++-- src/Guard.ts | 4 ++-- src/JsonSchema.ts | 4 ++-- src/Printer.ts | 4 ++-- src/SchemableExt.ts | 18 +++++++++--------- src/TaskDecoder.ts | 4 ++-- src/Type.ts | 4 ++-- .../definition.ts | 8 ++++---- .../instances/arbitrary.ts | 4 ++-- .../instances/decoder.ts | 4 ++-- .../instances/encoder.ts | 4 ++-- .../instances/eq.ts | 4 ++-- .../instances/guard.ts | 4 ++-- .../instances/json-schema.ts | 4 ++-- .../instances/printer.ts | 4 ++-- .../instances/schema.ts | 0 .../instances/task-decoder.ts | 4 ++-- .../instances/type.ts | 4 ++-- src/schemables/WithAnnotation/utils.ts | 11 ----------- src/schemata.ts | 4 ++-- tests/schemables/WithAnnotate.test.ts | 15 +++++++++++++++ 24 files changed, 66 insertions(+), 62 deletions(-) rename src/schemables/{WithAnnotation => WithAnnotate}/definition.ts (81%) rename src/schemables/{WithAnnotation => WithAnnotate}/instances/arbitrary.ts (77%) rename src/schemables/{WithAnnotation => WithAnnotate}/instances/decoder.ts (76%) rename src/schemables/{WithAnnotation => WithAnnotate}/instances/encoder.ts (77%) rename src/schemables/{WithAnnotation => WithAnnotate}/instances/eq.ts (77%) rename src/schemables/{WithAnnotation => WithAnnotate}/instances/guard.ts (78%) rename src/schemables/{WithAnnotation => WithAnnotate}/instances/json-schema.ts (73%) rename src/schemables/{WithAnnotation => WithAnnotate}/instances/printer.ts (77%) rename src/schemables/{WithAnnotation => WithAnnotate}/instances/schema.ts (100%) rename src/schemables/{WithAnnotation => WithAnnotate}/instances/task-decoder.ts (75%) rename src/schemables/{WithAnnotation => WithAnnotate}/instances/type.ts (78%) delete mode 100644 src/schemables/WithAnnotation/utils.ts create mode 100644 tests/schemables/WithAnnotate.test.ts diff --git a/src/Arbitrary.ts b/src/Arbitrary.ts index 30ff43e9..b27ee71e 100644 --- a/src/Arbitrary.ts +++ b/src/Arbitrary.ts @@ -7,7 +7,7 @@ */ import * as Arb from './base/ArbitraryBase' import { SchemableExt1 } from './SchemableExt' -import * as WithAnnotation from './schemables/WithAnnotation/instances/arbitrary' +import * as WithAnnotate from './schemables/WithAnnotate/instances/arbitrary' import * as WithBrand from './schemables/WithBrand/instances/arbitrary' import * as WithCheckDigit from './schemables/WithCheckDigit/instances/arbitrary' import * as WithDate from './schemables/WithDate/instances/arbitrary' @@ -37,7 +37,7 @@ export type { */ export const Schemable: SchemableExt1 = { ...Arb.Schemable, - ...WithAnnotation.Arbitrary, + ...WithAnnotate.Arbitrary, ...WithBrand.Arbitrary, ...WithCheckDigit.Arbitrary, ...WithDate.Arbitrary, diff --git a/src/Decoder.ts b/src/Decoder.ts index 8eaa7b58..e2b2ae0e 100644 --- a/src/Decoder.ts +++ b/src/Decoder.ts @@ -7,7 +7,7 @@ */ import * as D from './base/DecoderBase' import { SchemableExt2C } from './SchemableExt' -import * as WithAnnotation from './schemables/WithAnnotation/instances/decoder' +import * as WithAnnotate from './schemables/WithAnnotate/instances/decoder' import * as WithBrand from './schemables/WithBrand/instances/decoder' import * as WithCheckDigit from './schemables/WithCheckDigit/instances/decoder' import * as WithDate from './schemables/WithDate/instances/decoder' @@ -37,7 +37,7 @@ export type { */ export const Schemable: SchemableExt2C = { ...D.Schemable, - ...WithAnnotation.Decoder, + ...WithAnnotate.Decoder, ...WithBrand.Decoder, ...WithCheckDigit.Decoder, ...WithDate.Decoder, diff --git a/src/Encoder.ts b/src/Encoder.ts index 998a406f..ae2956ec 100644 --- a/src/Encoder.ts +++ b/src/Encoder.ts @@ -7,7 +7,7 @@ */ import * as Enc from './base/EncoderBase' import { SchemableExt2 } from './SchemableExt' -import * as WithAnnotation from './schemables/WithAnnotation/instances/encoder' +import * as WithAnnotate from './schemables/WithAnnotate/instances/encoder' import * as WithBrand from './schemables/WithBrand/instances/encoder' import * as WithCheckDigit from './schemables/WithCheckDigit/instances/encoder' import * as WithDate from './schemables/WithDate/instances/encoder' @@ -37,7 +37,7 @@ export type { */ export const Schemable: SchemableExt2 = { ...Enc.Schemable, - ...WithAnnotation.Encoder, + ...WithAnnotate.Encoder, ...WithBrand.Encoder, ...WithCheckDigit.Encoder, ...WithDate.Encoder, diff --git a/src/Eq.ts b/src/Eq.ts index a2933118..3aea1118 100644 --- a/src/Eq.ts +++ b/src/Eq.ts @@ -7,7 +7,7 @@ */ import * as Eq from './base/EqBase' import { SchemableExt1 } from './SchemableExt' -import * as WithAnnotation from './schemables/WithAnnotation/instances/eq' +import * as WithAnnotate from './schemables/WithAnnotate/instances/eq' import * as WithBrand from './schemables/WithBrand/instances/eq' import * as WithCheckDigit from './schemables/WithCheckDigit/instances/eq' import * as WithDate from './schemables/WithDate/instances/eq' @@ -37,7 +37,7 @@ export type { */ export const Schemable: SchemableExt1 = { ...Eq.Schemable, - ...WithAnnotation.Eq, + ...WithAnnotate.Eq, ...WithBrand.Eq, ...WithCheckDigit.Eq, ...WithDate.Eq, diff --git a/src/Guard.ts b/src/Guard.ts index 1de168f9..b3a4e576 100644 --- a/src/Guard.ts +++ b/src/Guard.ts @@ -7,7 +7,7 @@ */ import * as G from './base/GuardBase' import { SchemableExt1 } from './SchemableExt' -import * as WithAnnotation from './schemables/WithAnnotation/instances/guard' +import * as WithAnnotate from './schemables/WithAnnotate/instances/guard' import * as WithBrand from './schemables/WithBrand/instances/guard' import * as WithCheckDigit from './schemables/WithCheckDigit/instances/guard' import * as WithDate from './schemables/WithDate/instances/guard' @@ -37,7 +37,7 @@ export type { */ export const Schemable: SchemableExt1 = { ...G.Schemable, - ...WithAnnotation.Guard, + ...WithAnnotate.Guard, ...WithBrand.Guard, ...WithCheckDigit.Guard, ...WithDate.Guard, diff --git a/src/JsonSchema.ts b/src/JsonSchema.ts index 8eb5f9f5..d48cce7f 100644 --- a/src/JsonSchema.ts +++ b/src/JsonSchema.ts @@ -7,7 +7,7 @@ */ import * as JS from './base/JsonSchemaBase' import { SchemableExt2 } from './SchemableExt' -import * as WithAnnotation from './schemables/WithAnnotation/instances/json-schema' +import * as WithAnnotate from './schemables/WithAnnotate/instances/json-schema' import * as WithBrand from './schemables/WithBrand/instances/json-schema' import * as WithCheckDigit from './schemables/WithCheckDigit/instances/json-schema' import * as WithDate from './schemables/WithDate/instances/json-schema' @@ -37,7 +37,7 @@ export type { */ export const Schemable: SchemableExt2 = { ...JS.Schemable, - ...WithAnnotation.JsonSchema, + ...WithAnnotate.JsonSchema, ...WithBrand.JsonSchema, ...WithCheckDigit.JsonSchema, ...WithDate.JsonSchema, diff --git a/src/Printer.ts b/src/Printer.ts index ed190d3a..8edb8367 100644 --- a/src/Printer.ts +++ b/src/Printer.ts @@ -7,7 +7,7 @@ */ import * as P from './base/PrinterBase' import { SchemableExt2 } from './SchemableExt' -import * as WithAnnotation from './schemables/WithAnnotation/instances/printer' +import * as WithAnnotate from './schemables/WithAnnotate/instances/printer' import * as WithBrand from './schemables/WithBrand/instances/printer' import * as WithCheckDigit from './schemables/WithCheckDigit/instances/printer' import * as WithDate from './schemables/WithDate/instances/printer' @@ -37,7 +37,7 @@ export type { */ export const Schemable: SchemableExt2 = { ...P.Schemable, - ...WithAnnotation.Printer, + ...WithAnnotate.Printer, ...WithBrand.Printer, ...WithCheckDigit.Printer, ...WithDate.Printer, diff --git a/src/SchemableExt.ts b/src/SchemableExt.ts index 07666e30..923309ec 100644 --- a/src/SchemableExt.ts +++ b/src/SchemableExt.ts @@ -10,11 +10,11 @@ import { Schemable1, Schemable2C } from 'io-ts/Schemable' import { Schemable2, SchemableHKT2 } from './base/SchemableBase' import { - WithAnnotation1, - WithAnnotation2, - WithAnnotation2C, - WithAnnotationHKT2, -} from './schemables/WithAnnotation/definition' + WithAnnotate1, + WithAnnotate2, + WithAnnotate2C, + WithAnnotateHKT2, +} from './schemables/WithAnnotate/definition' import { WithBrand1, WithBrand2, @@ -106,7 +106,7 @@ import { */ export interface SchemableExt extends SchemableHKT2, - WithAnnotationHKT2, + WithAnnotateHKT2, WithBrandHKT2, WithCheckDigitHKT2, WithDateHKT2, @@ -128,7 +128,7 @@ export interface SchemableExt */ export interface SchemableExt1 extends Schemable1, - WithAnnotation1, + WithAnnotate1, WithBrand1, WithCheckDigit1, WithDate1, @@ -150,7 +150,7 @@ export interface SchemableExt1 */ export interface SchemableExt2 extends Schemable2, - WithAnnotation2, + WithAnnotate2, WithBrand2, WithCheckDigit2, WithDate2, @@ -172,7 +172,7 @@ export interface SchemableExt2 */ export interface SchemableExt2C extends Schemable2C, - WithAnnotation2C, + WithAnnotate2C, WithBrand2C, WithCheckDigit2C, WithDate2C, diff --git a/src/TaskDecoder.ts b/src/TaskDecoder.ts index 460af056..e779ec4b 100644 --- a/src/TaskDecoder.ts +++ b/src/TaskDecoder.ts @@ -7,7 +7,7 @@ */ import * as TD from './base/TaskDecoderBase' import { SchemableExt2C } from './SchemableExt' -import * as WithAnnotation from './schemables/WithAnnotation/instances/task-decoder' +import * as WithAnnotate from './schemables/WithAnnotate/instances/task-decoder' import * as WithBrand from './schemables/WithBrand/instances/task-decoder' import * as WithCheckDigit from './schemables/WithCheckDigit/instances/task-decoder' import * as WithDate from './schemables/WithDate/instances/task-decoder' @@ -37,7 +37,7 @@ export type { */ export const Schemable: SchemableExt2C = { ...TD.Schemable, - ...WithAnnotation.TaskDecoder, + ...WithAnnotate.TaskDecoder, ...WithBrand.TaskDecoder, ...WithCheckDigit.TaskDecoder, ...WithDate.TaskDecoder, diff --git a/src/Type.ts b/src/Type.ts index 7377bfd2..31ef8237 100644 --- a/src/Type.ts +++ b/src/Type.ts @@ -7,7 +7,7 @@ */ import * as t from './base/TypeBase' import { SchemableExt1 } from './SchemableExt' -import * as WithAnnotation from './schemables/WithAnnotation/instances/type' +import * as WithAnnotate from './schemables/WithAnnotate/instances/type' import * as WithBrand from './schemables/WithBrand/instances/type' import * as WithCheckDigit from './schemables/WithCheckDigit/instances/type' import * as WithDate from './schemables/WithDate/instances/type' @@ -37,7 +37,7 @@ export type { */ export const Schemable: SchemableExt1 = { ...t.Schemable, - ...WithAnnotation.Type, + ...WithAnnotate.Type, ...WithBrand.Type, ...WithCheckDigit.Type, ...WithDate.Type, diff --git a/src/schemables/WithAnnotation/definition.ts b/src/schemables/WithAnnotate/definition.ts similarity index 81% rename from src/schemables/WithAnnotation/definition.ts rename to src/schemables/WithAnnotate/definition.ts index 3ed25373..93e6e49f 100644 --- a/src/schemables/WithAnnotation/definition.ts +++ b/src/schemables/WithAnnotate/definition.ts @@ -10,7 +10,7 @@ import { HKT2, Kind, Kind2, URIS, URIS2 } from 'fp-ts/HKT' * @since 1.2.0 * @category Model */ -export interface WithAnnotationHKT2 { +export interface WithAnnotateHKT2 { readonly annotate: ( name?: string, description?: string, @@ -21,7 +21,7 @@ export interface WithAnnotationHKT2 { * @since 1.2.0 * @category Model */ -export interface WithAnnotation1 { +export interface WithAnnotate1 { readonly annotate: ( name?: string, description?: string, @@ -32,7 +32,7 @@ export interface WithAnnotation1 { * @since 1.2.0 * @category Model */ -export interface WithAnnotation2 { +export interface WithAnnotate2 { readonly annotate: ( name?: string, description?: string, @@ -43,7 +43,7 @@ export interface WithAnnotation2 { * @since 1.2.0 * @category Model */ -export interface WithAnnotation2C { +export interface WithAnnotate2C { readonly annotate: ( name?: string, description?: string, diff --git a/src/schemables/WithAnnotation/instances/arbitrary.ts b/src/schemables/WithAnnotate/instances/arbitrary.ts similarity index 77% rename from src/schemables/WithAnnotation/instances/arbitrary.ts rename to src/schemables/WithAnnotate/instances/arbitrary.ts index 1b1a0026..3bec8098 100644 --- a/src/schemables/WithAnnotation/instances/arbitrary.ts +++ b/src/schemables/WithAnnotate/instances/arbitrary.ts @@ -7,12 +7,12 @@ import { constant, identity } from 'fp-ts/function' import * as Arb from '../../../base/ArbitraryBase' -import { WithAnnotation1 } from '../definition' +import { WithAnnotate1 } from '../definition' /** * @since 1.2.0 * @category Instances */ -export const Arbitrary: WithAnnotation1 = { +export const Arbitrary: WithAnnotate1 = { annotate: constant(identity), } diff --git a/src/schemables/WithAnnotation/instances/decoder.ts b/src/schemables/WithAnnotate/instances/decoder.ts similarity index 76% rename from src/schemables/WithAnnotation/instances/decoder.ts rename to src/schemables/WithAnnotate/instances/decoder.ts index 7710f1ca..a684a8b6 100644 --- a/src/schemables/WithAnnotation/instances/decoder.ts +++ b/src/schemables/WithAnnotate/instances/decoder.ts @@ -7,12 +7,12 @@ import { constant, identity } from 'fp-ts/function' import * as D from '../../../base/DecoderBase' -import { WithAnnotation2C } from '../definition' +import { WithAnnotate2C } from '../definition' /** * @since 1.2.0 * @category Instances */ -export const Decoder: WithAnnotation2C = { +export const Decoder: WithAnnotate2C = { annotate: constant(identity), } diff --git a/src/schemables/WithAnnotation/instances/encoder.ts b/src/schemables/WithAnnotate/instances/encoder.ts similarity index 77% rename from src/schemables/WithAnnotation/instances/encoder.ts rename to src/schemables/WithAnnotate/instances/encoder.ts index cb7ca848..10ac5a4c 100644 --- a/src/schemables/WithAnnotation/instances/encoder.ts +++ b/src/schemables/WithAnnotate/instances/encoder.ts @@ -7,12 +7,12 @@ import { constant, identity } from 'fp-ts/function' import * as Enc from '../../../base/EncoderBase' -import { WithAnnotation2 } from '../definition' +import { WithAnnotate2 } from '../definition' /** * @since 1.2.0 * @category Instances */ -export const Encoder: WithAnnotation2 = { +export const Encoder: WithAnnotate2 = { annotate: constant(identity), } diff --git a/src/schemables/WithAnnotation/instances/eq.ts b/src/schemables/WithAnnotate/instances/eq.ts similarity index 77% rename from src/schemables/WithAnnotation/instances/eq.ts rename to src/schemables/WithAnnotate/instances/eq.ts index bff7dc7b..72e08f3b 100644 --- a/src/schemables/WithAnnotation/instances/eq.ts +++ b/src/schemables/WithAnnotate/instances/eq.ts @@ -7,12 +7,12 @@ import * as Eq_ from 'fp-ts/Eq' import { constant, identity } from 'fp-ts/function' -import { WithAnnotation1 } from '../definition' +import { WithAnnotate1 } from '../definition' /** * @since 1.2.0 * @category Instances */ -export const Eq: WithAnnotation1 = { +export const Eq: WithAnnotate1 = { annotate: constant(identity), } diff --git a/src/schemables/WithAnnotation/instances/guard.ts b/src/schemables/WithAnnotate/instances/guard.ts similarity index 78% rename from src/schemables/WithAnnotation/instances/guard.ts rename to src/schemables/WithAnnotate/instances/guard.ts index 7111fd46..1a102aa0 100644 --- a/src/schemables/WithAnnotation/instances/guard.ts +++ b/src/schemables/WithAnnotate/instances/guard.ts @@ -7,12 +7,12 @@ import { constant, identity } from 'fp-ts/function' import * as G from '../../../base/GuardBase' -import { WithAnnotation1 } from '../definition' +import { WithAnnotate1 } from '../definition' /** * @since 1.2.0 * @category Instances */ -export const Guard: WithAnnotation1 = { +export const Guard: WithAnnotate1 = { annotate: constant(identity), } diff --git a/src/schemables/WithAnnotation/instances/json-schema.ts b/src/schemables/WithAnnotate/instances/json-schema.ts similarity index 73% rename from src/schemables/WithAnnotation/instances/json-schema.ts rename to src/schemables/WithAnnotate/instances/json-schema.ts index 2eda603e..44557593 100644 --- a/src/schemables/WithAnnotation/instances/json-schema.ts +++ b/src/schemables/WithAnnotate/instances/json-schema.ts @@ -5,12 +5,12 @@ * @since 1.2.0 */ import * as JS from '../../../base/JsonSchemaBase' -import { WithAnnotation2 } from '../definition' +import { WithAnnotate2 } from '../definition' /** * @since 1.2.0 * @category Instances */ -export const JsonSchema: WithAnnotation2 = { +export const JsonSchema: WithAnnotate2 = { annotate: JS.annotate, } diff --git a/src/schemables/WithAnnotation/instances/printer.ts b/src/schemables/WithAnnotate/instances/printer.ts similarity index 77% rename from src/schemables/WithAnnotation/instances/printer.ts rename to src/schemables/WithAnnotate/instances/printer.ts index fa3d89d4..92cdcdf0 100644 --- a/src/schemables/WithAnnotation/instances/printer.ts +++ b/src/schemables/WithAnnotate/instances/printer.ts @@ -7,12 +7,12 @@ import { constant, identity } from 'fp-ts/function' import * as P from '../../../base/PrinterBase' -import { WithAnnotation2 } from '../definition' +import { WithAnnotate2 } from '../definition' /** * @since 1.2.0 * @category Instances */ -export const Printer: WithAnnotation2 = { +export const Printer: WithAnnotate2 = { annotate: constant(identity), } diff --git a/src/schemables/WithAnnotation/instances/schema.ts b/src/schemables/WithAnnotate/instances/schema.ts similarity index 100% rename from src/schemables/WithAnnotation/instances/schema.ts rename to src/schemables/WithAnnotate/instances/schema.ts diff --git a/src/schemables/WithAnnotation/instances/task-decoder.ts b/src/schemables/WithAnnotate/instances/task-decoder.ts similarity index 75% rename from src/schemables/WithAnnotation/instances/task-decoder.ts rename to src/schemables/WithAnnotate/instances/task-decoder.ts index 138ce14f..5e133526 100644 --- a/src/schemables/WithAnnotation/instances/task-decoder.ts +++ b/src/schemables/WithAnnotate/instances/task-decoder.ts @@ -7,12 +7,12 @@ import { constant, identity } from 'fp-ts/function' import * as TD from '../../../base/TaskDecoderBase' -import { WithAnnotation2C } from '../definition' +import { WithAnnotate2C } from '../definition' /** * @since 1.2.0 * @category Instances */ -export const TaskDecoder: WithAnnotation2C = { +export const TaskDecoder: WithAnnotate2C = { annotate: constant(identity), } diff --git a/src/schemables/WithAnnotation/instances/type.ts b/src/schemables/WithAnnotate/instances/type.ts similarity index 78% rename from src/schemables/WithAnnotation/instances/type.ts rename to src/schemables/WithAnnotate/instances/type.ts index 1b6cc31d..e1053f3a 100644 --- a/src/schemables/WithAnnotation/instances/type.ts +++ b/src/schemables/WithAnnotate/instances/type.ts @@ -7,12 +7,12 @@ import { constant, identity } from 'fp-ts/function' import * as t from '../../../base/TypeBase' -import { WithAnnotation1 } from '../definition' +import { WithAnnotate1 } from '../definition' /** * @since 1.2.0 * @category Instances */ -export const Type: WithAnnotation1 = { +export const Type: WithAnnotate1 = { annotate: constant(identity), } diff --git a/src/schemables/WithAnnotation/utils.ts b/src/schemables/WithAnnotation/utils.ts deleted file mode 100644 index ec5ece9b..00000000 --- a/src/schemables/WithAnnotation/utils.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** @since 1.0.0 */ - -/** @internal */ -export const replaceCharAt = (s: string, i: number, c: string): string => - s.substring(0, i) + c + s.substring(i + 1) - -/** @internal */ -export const locationToIndex = ( - s: string, - location: number | ((s: string) => number), -): number => (typeof location === 'number' ? location : location(s)) diff --git a/src/schemata.ts b/src/schemata.ts index 738d9c55..a90eb05c 100644 --- a/src/schemata.ts +++ b/src/schemata.ts @@ -29,8 +29,8 @@ export { * JsonSchema will not change the derivation. * * @since 1.2.0 - */ Schema as Annotation, -} from './schemables/WithAnnotation/instances/schema' + */ Schema as Annotate, +} from './schemables/WithAnnotate/instances/schema' export { /** * Schemable for constructing a branded newtype diff --git a/tests/schemables/WithAnnotate.test.ts b/tests/schemables/WithAnnotate.test.ts new file mode 100644 index 00000000..d4fb8ca9 --- /dev/null +++ b/tests/schemables/WithAnnotate.test.ts @@ -0,0 +1,15 @@ +import { getJsonSchema } from '../../src/JsonSchema' +import * as S from '../../src/schemata' + +describe('annotation', () => { + const schema = S.Annotate('root', 'description')(S.BigIntFromString()) + const jsonSchema = JSON.parse(JSON.stringify(getJsonSchema(schema))) + it('annotates', () => { + expect(jsonSchema).toEqual({ + type: 'string', + pattern: '^((0b)[0-1]+?|(0o)[0-7]+?|-?\\d+?|(0x)[0-9A-Fa-f]+?)$', + name: 'root', + description: 'description', + }) + }) +}) From b138f5eb517fc9f2271a41599b894cae1394f252 Mon Sep 17 00:00:00 2001 From: Jacob Alford Date: Tue, 3 Jan 2023 18:21:51 -0700 Subject: [PATCH 10/21] [#137] dont trample class names with json-schema --- src/base/JsonSchemaBase.ts | 12 +++++++----- tests/schemables/WithAnnotate.test.ts | 12 +++++++++--- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/base/JsonSchemaBase.ts b/src/base/JsonSchemaBase.ts index a3be1ab5..2a8f19b9 100644 --- a/src/base/JsonSchemaBase.ts +++ b/src/base/JsonSchemaBase.ts @@ -299,11 +299,13 @@ export const annotate: ( description?: string, ) => (schema: JsonSchema) => Const = (name, description) => schema => - make({ - ...schema, - name, - description, - }) + name === undefined && description === undefined + ? make(schema) + : make({ + ...schema, + ...(name === undefined ? {} : { name }), + ...(description === undefined ? {} : { description }), + }) // ------------------------------------------------------------------------------------- // instances diff --git a/tests/schemables/WithAnnotate.test.ts b/tests/schemables/WithAnnotate.test.ts index d4fb8ca9..3240bc5f 100644 --- a/tests/schemables/WithAnnotate.test.ts +++ b/tests/schemables/WithAnnotate.test.ts @@ -1,15 +1,21 @@ +import { makeIntegerSchema } from '../../src/base/JsonSchemaBase' import { getJsonSchema } from '../../src/JsonSchema' import * as S from '../../src/schemata' describe('annotation', () => { - const schema = S.Annotate('root', 'description')(S.BigIntFromString()) - const jsonSchema = JSON.parse(JSON.stringify(getJsonSchema(schema))) it('annotates', () => { + const schema = S.Annotate('root')(S.BigIntFromString()) + const jsonSchema = JSON.parse(JSON.stringify(getJsonSchema(schema))) expect(jsonSchema).toEqual({ type: 'string', pattern: '^((0b)[0-1]+?|(0o)[0-7]+?|-?\\d+?|(0x)[0-9A-Fa-f]+?)$', name: 'root', - description: 'description', + description: 'BigIntFromString', }) }) + it('doesnt trample class names', () => { + const schema = S.Annotate()(S.Natural) + const jsonSchema = getJsonSchema(schema) + expect(jsonSchema).toStrictEqual(makeIntegerSchema(0, 9007199254740991)) + }) }) From bb5371cfbe7d474b7856cd19957122caf9d7c2c3 Mon Sep 17 00:00:00 2001 From: Jacob Alford Date: Tue, 3 Jan 2023 18:25:30 -0700 Subject: [PATCH 11/21] chore: fix docs --- src/base/JsonSchemaBase.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/base/JsonSchemaBase.ts b/src/base/JsonSchemaBase.ts index 2a8f19b9..0a1a09e0 100644 --- a/src/base/JsonSchemaBase.ts +++ b/src/base/JsonSchemaBase.ts @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ /** * Models for JsonSchema as subsets of JSON Schema Draft 4, Draft 6, and Draft 7. * @@ -324,6 +323,7 @@ export const URI = 'JsonSchema' export type URI = typeof URI declare module 'fp-ts/lib/HKT' { + // eslint-disable-next-line @typescript-eslint/no-unused-vars interface URItoKind2 { readonly JsonSchema: Const } From 65d31fa19619e39e8dcd2d762df2729f5e6a42c9 Mon Sep 17 00:00:00 2001 From: Jacob Alford Date: Tue, 3 Jan 2023 19:21:53 -0700 Subject: [PATCH 12/21] [#137] add type guards for JsonSchema --- src/base/JsonSchemaBase.ts | 135 +++- src/schemables/WithAnnotate/definition.ts | 8 +- .../WithAnnotate/instances/schema.ts | 4 +- tests/JsonSchema.test.ts | 702 ++++++++++-------- tests/schemables/WithAnnotate.test.ts | 2 +- 5 files changed, 536 insertions(+), 315 deletions(-) diff --git a/src/base/JsonSchemaBase.ts b/src/base/JsonSchemaBase.ts index 0a1a09e0..b39abe8f 100644 --- a/src/base/JsonSchemaBase.ts +++ b/src/base/JsonSchemaBase.ts @@ -138,6 +138,131 @@ class JsonIntersection { constructor(readonly allOf: RNEA.ReadonlyNonEmptyArray) {} } +// ------------------------------------------------------------------------------------- +// guards +// ------------------------------------------------------------------------------------- + +/** @internal */ +const hasType = ( + type: string, + u: JsonSchema, +): u is JsonSchema & { readonly type: string } => 'type' in u && u.type === type + +/** @internal */ +const hasKey = ( + key: K, + u: JsonSchema, +): u is JsonSchema & { readonly [key in K]: unknown } => key in u + +/** + * @since 1.2.0 + * @category Guards + */ +export const isJsonEmpty = (u: JsonSchema): u is JsonEmpty => + u instanceof JsonEmpty || Object.keys(u).length === 0 + +/** + * @since 1.2.0 + * @category Guards + */ +export const isJsonString = (u: JsonSchema): u is JsonString => + u instanceof JsonString || hasType('string', u) + +/** + * @since 1.2.0 + * @category Guards + */ +export const isJsonNumber = (u: JsonSchema): u is JsonNumber => + u instanceof JsonNumber || hasType('number', u) + +/** + * @since 1.2.0 + * @category Guards + */ +export const isJsonNull = (u: JsonSchema): u is JsonNull => + u instanceof JsonNull || hasType('null', u) + +/** + * @since 1.2.0 + * @category Guards + */ +export const isJsonInteger = (u: JsonSchema): u is JsonInteger => + u instanceof JsonInteger || hasType('integer', u) + +/** + * @since 1.2.0 + * @category Guards + */ +export const isJsonBoolean = (u: JsonSchema): u is JsonBoolean => + u instanceof JsonBoolean || hasType('boolean', u) + +/** + * @since 1.2.0 + * @category Guards + */ +export const isJsonConst = (u: JsonSchema): u is JsonConst => 'const' in u + +/** + * @since 1.2.0 + * @category Guards + */ +export const isJsonPrimitive = (u: JsonSchema): u is JsonLiteral | JsonNull => + isJsonNull(u) || isJsonBoolean(u) || isJsonNumber(u) || isJsonString(u) + +/** + * @since 1.2.0 + * @category Guards + */ +export const isJsonLiteral = (u: JsonSchema): u is JsonLiteral => + isJsonConst(u) && isJsonPrimitive(u) + +/** + * @since 1.2.0 + * @category Guards + */ +export const isJsonStruct = (u: JsonSchema): u is JsonStruct => + u instanceof JsonStruct || + (hasType('object', u) && hasKey('properties', u) && hasKey('required', u)) + +/** + * @since 1.2.0 + * @category Guards + */ +export const isJsonRecord = (u: JsonSchema): u is JsonRecord => + u instanceof JsonRecord || (hasType('object', u) && hasKey('additionalProperties', u)) + +/** + * @since 1.2.0 + * @category Guards + */ +export const isJsonArray = (u: JsonSchema): u is JsonArray => + u instanceof JsonArray || hasType('array', u) + +/** + * @since 1.2.0 + * @category Guards + */ +export const isJsonExclude = (u: JsonSchema): u is JsonExclude => + isJsonIntersection(u) && + (RNEA.head(u.allOf) instanceof JsonExclude || hasKey('not', RNEA.head(u.allOf))) && + u.allOf.length === 2 && + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + isJsonConst((RNEA.head(u.allOf) as JsonExclude).not) + +/** + * @since 1.2.0 + * @category Guards + */ +export const isJsonUnion = (u: JsonSchema): u is JsonUnion => + u instanceof JsonUnion || hasKey('oneOf', u) + +/** + * @since 1.2.0 + * @category Guards + */ +export const isJsonIntersection = (u: JsonSchema): u is JsonIntersection => + u instanceof JsonIntersection || hasKey('allOf', u) + // ------------------------------------------------------------------------------------- // constructors // ------------------------------------------------------------------------------------- @@ -287,22 +412,22 @@ export const makeExclusionSchema = ( exclude: Z, schema: Const, ): Const> => - make(new JsonIntersection([schema, new JsonExclude(makeConstSchema(exclude))])) + make(new JsonIntersection([new JsonExclude(makeConstSchema(exclude)), schema])) /** * @since 1.2.0 * @category Combintators */ export const annotate: ( - name?: string, + title?: string, description?: string, ) => (schema: JsonSchema) => Const = - (name, description) => schema => - name === undefined && description === undefined + (title, description) => schema => + title === undefined && description === undefined ? make(schema) : make({ ...schema, - ...(name === undefined ? {} : { name }), + ...(title === undefined ? {} : { title }), ...(description === undefined ? {} : { description }), }) diff --git a/src/schemables/WithAnnotate/definition.ts b/src/schemables/WithAnnotate/definition.ts index 93e6e49f..311a80e0 100644 --- a/src/schemables/WithAnnotate/definition.ts +++ b/src/schemables/WithAnnotate/definition.ts @@ -12,7 +12,7 @@ import { HKT2, Kind, Kind2, URIS, URIS2 } from 'fp-ts/HKT' */ export interface WithAnnotateHKT2 { readonly annotate: ( - name?: string, + title?: string, description?: string, ) => (schema: HKT2) => HKT2 } @@ -23,7 +23,7 @@ export interface WithAnnotateHKT2 { */ export interface WithAnnotate1 { readonly annotate: ( - name?: string, + title?: string, description?: string, ) => (schema: Kind) => Kind } @@ -34,7 +34,7 @@ export interface WithAnnotate1 { */ export interface WithAnnotate2 { readonly annotate: ( - name?: string, + title?: string, description?: string, ) => (schema: Kind2) => Kind2 } @@ -45,7 +45,7 @@ export interface WithAnnotate2 { */ export interface WithAnnotate2C { readonly annotate: ( - name?: string, + title?: string, description?: string, ) => (schema: Kind2) => Kind2 } diff --git a/src/schemables/WithAnnotate/instances/schema.ts b/src/schemables/WithAnnotate/instances/schema.ts index 5e63f026..dbe187b6 100644 --- a/src/schemables/WithAnnotate/instances/schema.ts +++ b/src/schemables/WithAnnotate/instances/schema.ts @@ -11,6 +11,6 @@ import * as SC from '../../../SchemaExt' * @category Combinators */ export const Schema = - (name?: string, description?: string) => + (title?: string, description?: string) => (schema: SC.SchemaExt): SC.SchemaExt => - SC.make(s => s.annotate(name, description)(schema(s))) + SC.make(s => s.annotate(title, description)(schema(s))) diff --git a/tests/JsonSchema.test.ts b/tests/JsonSchema.test.ts index 10bd8b2a..934529de 100644 --- a/tests/JsonSchema.test.ts +++ b/tests/JsonSchema.test.ts @@ -1,69 +1,165 @@ import * as Str from 'fp-ts/string' +import * as base from '../src/base/JsonSchemaBase' import { getJsonSchema } from '../src/JsonSchema' import * as S from '../src/schemata' describe('JsonSchema', () => { - const testSchema = S.Readonly( - S.Struct({ - literal: S.Literal('string', 5, true, null), - nully: S.Nullable(S.Int({ min: 0, max: 1 })), - partial: S.Partial({ - optNull: S.OptionFromNullable(S.Float({ min: 0, max: 1 })), - optUndefined: S.OptionFromUndefined(S.Float({ min: 0, max: 1 })), - }), - rec: S.Record(S.CreditCard), - arr: S.Array(S.Json.jsonString), + describe('guards', () => { + test('isJsonEmpty', () => { + expect(base.isJsonEmpty(base.emptySchema)).toBe(true) + expect(base.isJsonEmpty({})).toBe(true) + }) + test('isJsonString', () => { + expect(base.isJsonString(base.makeStringSchema())).toBe(true) + expect(base.isJsonString({ type: 'string' })).toBe(true) + }) + test('isJsonNumber', () => { + expect(base.isJsonNumber(base.makeNumberSchema())).toBe(true) + expect(base.isJsonNumber({ type: 'number' })).toBe(true) + }) + test('isJsonNull', () => { + expect(base.isJsonNull(base.nullSchema)).toBe(true) + expect(base.isJsonNull({ type: 'null', const: null })).toBe(true) + }) + test('isJsonInteger', () => { + expect(base.isJsonInteger(base.makeIntegerSchema())).toBe(true) + expect(base.isJsonInteger({ type: 'integer' })).toBe(true) + }) + test('isJsonBoolean', () => { + expect(base.isJsonBoolean(base.booleanSchema)).toBe(true) + expect(base.isJsonBoolean({ type: 'boolean' })).toBe(true) + }) + test('isJsonLiteral', () => { + expect(base.isJsonLiteral(base.makeLiteralSchema('string'))).toBe(true) + expect(base.isJsonLiteral({ type: 'string', const: 'string' })).toBe(true) + }) + test('isJsonArray', () => { + expect(base.isJsonArray(base.makeArraySchema(base.emptySchema))).toBe(true) + expect(base.isJsonArray({ type: 'array', items: {} })).toBe(true) + }) + test('isJsonStruct', () => { + expect( + base.isJsonStruct(base.makeStructSchema({ a: base.makeStringSchema() })), + ).toBe(true) + expect( + base.isJsonStruct({ + type: 'object', + properties: { a: { type: 'string' } }, + required: [], + }), + ).toBe(true) + }) + test('isJsonRecord', () => { + expect(base.isJsonRecord(base.makeRecordSchema(base.makeStringSchema()))).toBe(true) + expect( + base.isJsonRecord({ type: 'object', additionalProperties: { type: 'string' } }), + ).toBe(true) + }) + test('isJsonExclude', () => { + expect( + base.isJsonExclude(base.makeExclusionSchema(5, base.makeNumberSchema())), + ).toBe(true) + expect( + base.isJsonExclude({ + allOf: [ + { + not: { + const: 5, + }, + }, + { type: 'number' }, + ], + }), + ).toBe(true) + }) + test('isJsonUnion', () => { + expect( + base.isJsonUnion( + base.makeUnionSchema(base.makeStringSchema(), base.makeNumberSchema()), + ), + ).toBe(true) + expect( + base.isJsonUnion({ + oneOf: [{ type: 'string' }, { type: 'number' }], + }), + ).toBe(true) + }) + test('isJsonIntersection', () => { + expect( + base.isJsonIntersection( + base.makeIntersectionSchema(base.makeStringSchema(), base.makeNumberSchema()), + ), + ).toBe(true) + expect( + base.isJsonIntersection({ + allOf: [{ type: 'string' }, { type: 'number' }], + }), + ).toBe(true) + }) + }) + describe('derivation', () => { + const testSchema = S.Readonly( + S.Struct({ + literal: S.Literal('string', 5, true, null), + nully: S.Nullable(S.Int({ min: 0, max: 1 })), + partial: S.Partial({ + optNull: S.OptionFromNullable(S.Float({ min: 0, max: 1 })), + optUndefined: S.OptionFromUndefined(S.Float({ min: 0, max: 1 })), + }), + rec: S.Record(S.CreditCard), + arr: S.Array(S.Json.jsonString), - tup: S.Tuple(S.JsonFromString, S.Number), - sum: S.Sum('type')({ - a: S.Struct({ type: S.Literal('a'), a: S.Boolean }), - b: S.Struct({ type: S.Literal('b'), b: S.Lazy('Sum[b].b', () => S.Natural) }), - }), - intersection: S.Intersection(S.Struct({ a: S.NonPositiveInt }))( - S.Struct({ b: S.NonNegativeFloat, c: S.NonPositiveFloat }), - ), - date: S.Date.dateFromString, - isoDate: S.DateFromIsoString(), - map: S.Map(Str.Ord, S.String, S.Float()), - int: S.Int(), - option: S.Option(69420, S.Number), - singularSum: S.Sum('type')({ - a: S.Struct({ - type: S.Literal('a'), - a: S.NonEmptyArray( - S.Padding.padRight( - { by: 'ExactLength', exactLength: 1 }, - '*', - )( + tup: S.Tuple(S.JsonFromString, S.Number), + sum: S.Sum('type')({ + a: S.Struct({ type: S.Literal('a'), a: S.Boolean }), + b: S.Struct({ type: S.Literal('b'), b: S.Lazy('Sum[b].b', () => S.Natural) }), + }), + intersection: S.Intersection(S.Struct({ a: S.NonPositiveInt }))( + S.Struct({ b: S.NonNegativeFloat, c: S.NonPositiveFloat }), + ), + date: S.Date.dateFromString, + isoDate: S.DateFromIsoString(), + map: S.Map(Str.Ord, S.String, S.Float()), + int: S.Int(), + option: S.Option(69420, S.Number), + singularSum: S.Sum('type')({ + a: S.Struct({ + type: S.Literal('a'), + a: S.NonEmptyArray( S.Padding.padRight( - { by: 'MaxLength', maxLength: 2 }, - '=', + { by: 'ExactLength', exactLength: 1 }, + '*', )( - S.Padding.padLeft( - { by: 'MaxLength', maxLength: 3 }, + S.Padding.padRight( + { by: 'MaxLength', maxLength: 2 }, '=', )( S.Padding.padLeft( - { by: 'ExactLength', exactLength: 4 }, + { by: 'MaxLength', maxLength: 3 }, '=', )( - S.Padding.padRight( - { by: 'ExactLength', exactLength: () => 0 }, - '&', + S.Padding.padLeft( + { by: 'ExactLength', exactLength: 4 }, + '=', )( S.Padding.padRight( - { by: 'MaxLength', maxLength: () => 0 }, - '=', + { by: 'ExactLength', exactLength: () => 0 }, + '&', )( - S.Padding.padLeft( + S.Padding.padRight( { by: 'MaxLength', maxLength: () => 0 }, '=', )( S.Padding.padLeft( - { by: 'ExactLength', exactLength: () => 0 }, + { by: 'MaxLength', maxLength: () => 0 }, '=', - )(S.String), + )( + S.Padding.padLeft( + { by: 'ExactLength', exactLength: () => 0 }, + '=', + )(S.String), + ), ), ), ), @@ -71,301 +167,301 @@ describe('JsonSchema', () => { ), ), ), - ), + }), }), }), - }), - ) - const jsonSchema = getJsonSchema(testSchema) - const testValue = JSON.parse(JSON.stringify(jsonSchema)) as any - test('struct', () => { - expect(testValue.type).toBe('object') - expect(testValue.required).toStrictEqual([ - 'literal', - 'nully', - 'partial', - 'rec', - 'arr', - 'tup', - 'sum', - 'intersection', - 'date', - 'isoDate', - 'map', - 'int', - 'option', - 'singularSum', - ]) - }) - test('literal', () => { - expect(testValue.properties.literal).toStrictEqual({ - oneOf: [ - { type: 'string', const: 'string' } as any, - { type: 'number', const: 5 }, - { type: 'boolean', const: true }, - { type: 'null', const: null }, - ], + ) + const jsonSchema = getJsonSchema(testSchema) + const testValue = JSON.parse(JSON.stringify(jsonSchema)) as any + test('struct', () => { + expect(testValue.type).toBe('object') + expect(testValue.required).toStrictEqual([ + 'literal', + 'nully', + 'partial', + 'rec', + 'arr', + 'tup', + 'sum', + 'intersection', + 'date', + 'isoDate', + 'map', + 'int', + 'option', + 'singularSum', + ]) }) - }) - test('nullable', () => { - expect(testValue.properties.nully).toStrictEqual({ - oneOf: [ - { type: 'integer', minimum: 0, maximum: 1 }, - { type: 'null', const: null }, - ], + test('literal', () => { + expect(testValue.properties.literal).toStrictEqual({ + oneOf: [ + { type: 'string', const: 'string' } as any, + { type: 'number', const: 5 }, + { type: 'boolean', const: true }, + { type: 'null', const: null }, + ], + }) }) - }) - test('partial', () => { - expect(testValue.properties.partial).toStrictEqual({ - type: 'object', - properties: { - optNull: { - oneOf: [ - { type: 'number', minimum: 0, maximum: 1 }, - { type: 'null', const: null }, - ], - }, - optUndefined: {}, - }, - required: [], + test('nullable', () => { + expect(testValue.properties.nully).toStrictEqual({ + oneOf: [ + { type: 'integer', minimum: 0, maximum: 1 }, + { type: 'null', const: null }, + ], + }) }) - }) - test('record', () => { - expect(testValue.properties.rec).toStrictEqual({ - type: 'object', - additionalProperties: { - description: 'CreditCard', - type: 'string', - pattern: - '^(4(\\d{12}|\\d{15})|(5[1-5]\\d{4}|(222)[1-9]\\d{2}|(22)[3-9]\\d{3}|(2)[3-6]\\d{4}|(27)[01]\\d{3}|(2720)\\d{2})\\d{10}|3[47]\\d{13}|(3(0([0-5]\\d{5}|(95)\\d{4})|[89]\\d{6})\\d{8,11}|(36)\\d{6}\\d{6,11})|((6011)(0[5-9]\\d{2}|[2-4]\\d{3}|(74)\\d{2}|(7)[7-9]\\d{2}|(8)[6-9]\\d{2}|(9)\\d{3})|(64)[4-9]\\d{5}|(650)[0-5]\\d{4}|(65060)[1-9]\\d{2}|(65061)[1-9]\\d{2}|(6506)[2-9]\\d{3}|(650)[7-9]\\d{4}|(65)[1-9]\\d{5})\\d{8,11}|((352)[89]\\d{4}|(35)[3-8]\\d{5})\\d{8,11}|(((60)|(65)|(81)|(82))\\d{14}|(508)\\d{14})|(62)(2((12)[6-9]\\d{2}|1[3-9]\\d{3}|[2-8]\\d|(9)[01]\\d{3}|(92)[0-5]\\d{2})|[4-6]\\d{5}|(8)[2-8]\\d{4})\\d{8,11})$', - }, + test('partial', () => { + expect(testValue.properties.partial).toStrictEqual({ + type: 'object', + properties: { + optNull: { + oneOf: [ + { type: 'number', minimum: 0, maximum: 1 }, + { type: 'null', const: null }, + ], + }, + optUndefined: {}, + }, + required: [], + }) }) - }) - test('array', () => { - expect(testValue.properties.arr).toStrictEqual({ - type: 'array', - items: { - type: 'string', - contentMediaType: 'application/json', - }, + test('record', () => { + expect(testValue.properties.rec).toStrictEqual({ + type: 'object', + additionalProperties: { + description: 'CreditCard', + type: 'string', + pattern: + '^(4(\\d{12}|\\d{15})|(5[1-5]\\d{4}|(222)[1-9]\\d{2}|(22)[3-9]\\d{3}|(2)[3-6]\\d{4}|(27)[01]\\d{3}|(2720)\\d{2})\\d{10}|3[47]\\d{13}|(3(0([0-5]\\d{5}|(95)\\d{4})|[89]\\d{6})\\d{8,11}|(36)\\d{6}\\d{6,11})|((6011)(0[5-9]\\d{2}|[2-4]\\d{3}|(74)\\d{2}|(7)[7-9]\\d{2}|(8)[6-9]\\d{2}|(9)\\d{3})|(64)[4-9]\\d{5}|(650)[0-5]\\d{4}|(65060)[1-9]\\d{2}|(65061)[1-9]\\d{2}|(6506)[2-9]\\d{3}|(650)[7-9]\\d{4}|(65)[1-9]\\d{5})\\d{8,11}|((352)[89]\\d{4}|(35)[3-8]\\d{5})\\d{8,11}|(((60)|(65)|(81)|(82))\\d{14}|(508)\\d{14})|(62)(2((12)[6-9]\\d{2}|1[3-9]\\d{3}|[2-8]\\d|(9)[01]\\d{3}|(92)[0-5]\\d{2})|[4-6]\\d{5}|(8)[2-8]\\d{4})\\d{8,11})$', + }, + }) }) - }) - test('tuple', () => { - expect(testValue.properties.tup).toStrictEqual({ - type: 'array', - items: [ - { + test('array', () => { + expect(testValue.properties.arr).toStrictEqual({ + type: 'array', + items: { type: 'string', contentMediaType: 'application/json', }, - { - type: 'number', - }, - ], - minItems: 2, - maxItems: 2, + }) }) - }) - test('sum', () => { - expect(testValue.properties.sum).toStrictEqual({ - oneOf: [ - { - type: 'object', - properties: { - type: { - type: 'string', - const: 'a', - }, - a: { - type: 'boolean', - }, + test('tuple', () => { + expect(testValue.properties.tup).toStrictEqual({ + type: 'array', + items: [ + { + type: 'string', + contentMediaType: 'application/json', }, - required: ['type', 'a'], - }, - { - type: 'object', - properties: { - type: { - type: 'string', - const: 'b', + { + type: 'number', + }, + ], + minItems: 2, + maxItems: 2, + }) + }) + test('sum', () => { + expect(testValue.properties.sum).toStrictEqual({ + oneOf: [ + { + type: 'object', + properties: { + type: { + type: 'string', + const: 'a', + }, + a: { + type: 'boolean', + }, }, - b: { - type: 'integer', - maximum: 9007199254740991, - minimum: 0, + required: ['type', 'a'], + }, + { + type: 'object', + properties: { + type: { + type: 'string', + const: 'b', + }, + b: { + type: 'integer', + maximum: 9007199254740991, + minimum: 0, + }, }, + required: ['type', 'b'], }, - required: ['type', 'b'], - }, - ], + ], + }) }) - }) - test('date', () => { - expect(testValue.properties.date).toStrictEqual({ - type: 'string', - format: 'date', + test('date', () => { + expect(testValue.properties.date).toStrictEqual({ + type: 'string', + format: 'date', + }) }) - }) - test('isoDate', () => { - expect(testValue.properties.isoDate).toStrictEqual({ - type: 'string', - description: 'IsoDateString', - pattern: - '^(((\\d{4}|[+\\x2d]\\d{6})-((0[1-9])|(1[0-2]))-((0[1-9])|(1\\d|[2]\\d|3[0-1]))|((\\d{4}|[+\\x2d]\\d{6})-((0[1-9])|(1[0-2])))|\\d{4}|[+\\x2d]\\d{6})(T| )(((0\\d)|(1\\d|2[0-3])):((0\\d)|(1\\d|[2-4]\\d|5\\d)):((0\\d)|(1\\d|[2-4]\\d|5\\d))\\.(\\d+?)|((0\\d)|(1\\d|2[0-3])):((0\\d)|(1\\d|[2-4]\\d|5\\d)):((0\\d)|(1\\d|[2-4]\\d|5\\d))|((0\\d)|(1\\d|2[0-3])):((0\\d)|(1\\d|[2-4]\\d|5\\d)))(Z|[+\\x2d]((0\\d)|(1\\d|2[0-3])):((0\\d)|(1\\d|[2-4]\\d|5\\d))))$', + test('isoDate', () => { + expect(testValue.properties.isoDate).toStrictEqual({ + type: 'string', + description: 'IsoDateString', + pattern: + '^(((\\d{4}|[+\\x2d]\\d{6})-((0[1-9])|(1[0-2]))-((0[1-9])|(1\\d|[2]\\d|3[0-1]))|((\\d{4}|[+\\x2d]\\d{6})-((0[1-9])|(1[0-2])))|\\d{4}|[+\\x2d]\\d{6})(T| )(((0\\d)|(1\\d|2[0-3])):((0\\d)|(1\\d|[2-4]\\d|5\\d)):((0\\d)|(1\\d|[2-4]\\d|5\\d))\\.(\\d+?)|((0\\d)|(1\\d|2[0-3])):((0\\d)|(1\\d|[2-4]\\d|5\\d)):((0\\d)|(1\\d|[2-4]\\d|5\\d))|((0\\d)|(1\\d|2[0-3])):((0\\d)|(1\\d|[2-4]\\d|5\\d)))(Z|[+\\x2d]((0\\d)|(1\\d|2[0-3])):((0\\d)|(1\\d|[2-4]\\d|5\\d))))$', + }) }) - }) - test('intersection', () => { - expect(testValue.properties.intersection).toStrictEqual({ - allOf: [ - { - type: 'object', - properties: { - a: { - type: 'integer', - maximum: 0, - minimum: -9007199254740991, + test('intersection', () => { + expect(testValue.properties.intersection).toStrictEqual({ + allOf: [ + { + type: 'object', + properties: { + a: { + type: 'integer', + maximum: 0, + minimum: -9007199254740991, + }, }, + required: ['a'], }, - required: ['a'], - }, - { - type: 'object', - properties: { - b: { - type: 'number', - maximum: 1.7976931348623157e308, - minimum: 0, + { + type: 'object', + properties: { + b: { + type: 'number', + maximum: 1.7976931348623157e308, + minimum: 0, + }, + c: { + type: 'number', + maximum: 0, + minimum: -1.7976931348623157e308, + }, }, - c: { + required: ['b', 'c'], + }, + ], + }) + }) + test('map', () => { + expect(testValue.properties.map).toStrictEqual({ + type: 'array', + items: { + type: 'array', + items: [ + { + type: 'string', + }, + { type: 'number', - maximum: 0, + maximum: 1.7976931348623157e308, minimum: -1.7976931348623157e308, }, - }, - required: ['b', 'c'], + ], + minItems: 2, + maxItems: 2, }, - ], + }) }) - }) - test('map', () => { - expect(testValue.properties.map).toStrictEqual({ - type: 'array', - items: { - type: 'array', - items: [ + test('int', () => { + expect(testValue.properties.int).toStrictEqual({ + type: 'integer', + maximum: 9007199254740991, + minimum: -9007199254740991, + }) + }) + test('option', () => { + expect(testValue.properties.option).toStrictEqual({ + allOf: [ { - type: 'string', + not: { + const: 69420, + }, }, { type: 'number', - maximum: 1.7976931348623157e308, - minimum: -1.7976931348623157e308, }, ], - minItems: 2, - maxItems: 2, - }, + }) }) - }) - test('int', () => { - expect(testValue.properties.int).toStrictEqual({ - type: 'integer', - maximum: 9007199254740991, - minimum: -9007199254740991, - }) - }) - test('option', () => { - expect(testValue.properties.option).toStrictEqual({ - allOf: [ - { - type: 'number', - }, - { - not: { - const: 69420, + test('singularSum', () => { + expect(testValue.properties.singularSum).toStrictEqual({ + type: 'object', + properties: { + type: { + type: 'string', + const: 'a', }, - }, - ], - }) - }) - test('singularSum', () => { - expect(testValue.properties.singularSum).toStrictEqual({ - type: 'object', - properties: { - type: { - type: 'string', - const: 'a', - }, - a: { - type: 'array', - items: { - allOf: [ - { - type: 'string', - maxLength: 1, - minLength: 1, - }, - { - allOf: [ - { - type: 'string', - maxLength: 2, - }, - { - allOf: [ - { - type: 'string', - maxLength: 3, - }, - { - allOf: [ - { - type: 'string', - maxLength: 4, - minLength: 4, - }, - { - allOf: [ - { - type: 'string', - }, - { - allOf: [ - { - type: 'string', - }, - { - allOf: [ - { - type: 'string', - }, - { - allOf: [ - { - type: 'string', - }, - { - type: 'string', - }, - ], - }, - ], - }, - ], - }, - ], - }, - ], - }, - ], - }, - ], - }, - ], + a: { + type: 'array', + items: { + allOf: [ + { + type: 'string', + maxLength: 1, + minLength: 1, + }, + { + allOf: [ + { + type: 'string', + maxLength: 2, + }, + { + allOf: [ + { + type: 'string', + maxLength: 3, + }, + { + allOf: [ + { + type: 'string', + maxLength: 4, + minLength: 4, + }, + { + allOf: [ + { + type: 'string', + }, + { + allOf: [ + { + type: 'string', + }, + { + allOf: [ + { + type: 'string', + }, + { + allOf: [ + { + type: 'string', + }, + { + type: 'string', + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + }, }, }, - }, - required: ['type', 'a'], + required: ['type', 'a'], + }) }) }) }) diff --git a/tests/schemables/WithAnnotate.test.ts b/tests/schemables/WithAnnotate.test.ts index 3240bc5f..26b9d4a5 100644 --- a/tests/schemables/WithAnnotate.test.ts +++ b/tests/schemables/WithAnnotate.test.ts @@ -9,7 +9,7 @@ describe('annotation', () => { expect(jsonSchema).toEqual({ type: 'string', pattern: '^((0b)[0-1]+?|(0o)[0-7]+?|-?\\d+?|(0x)[0-9A-Fa-f]+?)$', - name: 'root', + title: 'root', description: 'BigIntFromString', }) }) From e87199a98fb9da733547a270ba30e17bf79ca7f6 Mon Sep 17 00:00:00 2001 From: Jacob Alford Date: Tue, 3 Jan 2023 19:36:42 -0700 Subject: [PATCH 13/21] [#137] add rudimentary test for validation against json-schema-library --- package.json | 1 + tests/JsonSchema.test.ts | 51 ++++++++++++++++++++++++++++++++++++++++ yarn.lock | 34 +++++++++++++++++++++++++++ 3 files changed, 86 insertions(+) diff --git a/package.json b/package.json index 95b3c60f..50faf453 100644 --- a/package.json +++ b/package.json @@ -94,6 +94,7 @@ "glob": "^8.0.3", "husky": "^8.0.0", "jest": "^26.6.3", + "json-schema-library": "^7.4.4", "lint-staged": "^13.0.3", "markdown-magic": "^2.0.0", "prettier": "^2.2.1", diff --git a/tests/JsonSchema.test.ts b/tests/JsonSchema.test.ts index 934529de..5d48b40a 100644 --- a/tests/JsonSchema.test.ts +++ b/tests/JsonSchema.test.ts @@ -1,5 +1,8 @@ +import * as fc from 'fast-check' import * as Str from 'fp-ts/string' +import { Draft04, Draft06, Draft07 } from 'json-schema-library' +import { getArbitrary } from '../src/Arbitrary' import * as base from '../src/base/JsonSchemaBase' import { getJsonSchema } from '../src/JsonSchema' import * as S from '../src/schemata' @@ -98,6 +101,7 @@ describe('JsonSchema', () => { ).toBe(true) }) }) + describe('derivation', () => { const testSchema = S.Readonly( S.Struct({ @@ -464,4 +468,51 @@ describe('JsonSchema', () => { }) }) }) + + describe('externally valid', () => { + const testSchema = S.Readonly( + S.Struct({ + literal: S.Literal('string', 5, true, null), + nully: S.Nullable(S.Int({ min: 0, max: 1 })), + rec: S.Record(S.CreditCard), + arr: S.Array(S.Json.jsonString), + + tup: S.Tuple(S.Number), + sum: S.Sum('type')({ + a: S.Struct({ type: S.Literal('a'), a: S.Boolean }), + b: S.Struct({ type: S.Literal('b'), b: S.Lazy('Sum[b].b', () => S.Natural) }), + }), + intersection: S.Intersection(S.Struct({ a: S.NonPositiveInt }))( + S.Struct({ b: S.NonNegativeFloat, c: S.NonPositiveFloat }), + ), + int: S.Int(), + }), + ) + const jsonSchema = getJsonSchema(testSchema) + const arbitrary = getArbitrary(testSchema).arbitrary(fc) + it('validates for version 4', () => { + const v4 = new Draft04(jsonSchema) + fc.assert( + fc.property(arbitrary, value => { + expect(v4.isValid(value)).toBe(true) + }), + ) + }) + it('validates for version 6', () => { + const v6 = new Draft06(jsonSchema) + fc.assert( + fc.property(arbitrary, value => { + expect(v6.isValid(value)).toBe(true) + }), + ) + }) + it('validates for version 7', () => { + const v7 = new Draft07(jsonSchema) + fc.assert( + fc.property(arbitrary, value => { + expect(v7.isValid(value)).toBe(true) + }), + ) + }) + }) }) diff --git a/yarn.lock b/yarn.lock index b8103f7d..50879208 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1558,6 +1558,19 @@ tiny-glob "^0.2.9" tslib "^2.4.0" +"@sagold/json-pointer@^5.0.0": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@sagold/json-pointer/-/json-pointer-5.0.0.tgz#49a3f072cd95c89d82f67cc3d88b8bb86060c9e6" + integrity sha512-+kIj/GyAlBfZPPcxXlpMdcDP/HyQBbmVCrQ9VZQ8wEqphIHCg0oLojBCLTqpICie/tIc7nwXh3joLVJ4PTD+NQ== + +"@sagold/json-query@^6.0.0": + version "6.0.0" + resolved "https://registry.yarnpkg.com/@sagold/json-query/-/json-query-6.0.0.tgz#a250a98d19cc20d8c06e8e9eeaf9d1de833f1ed4" + integrity sha512-fk9BimvNrzlhXiy+dvlwyA97W2N6GmPWCJo/2kwKEtU9oc93cVKamca9NcnjKx1hhjECjPfu30NQ8Tg2JGv/pA== + dependencies: + "@sagold/json-pointer" "^5.0.0" + ebnf "^1.9.0" + "@sinonjs/commons@^1.7.0": version "1.8.3" resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.3.tgz#3802ddd21a50a949b6721ddd72da36e67e7f1b2d" @@ -3070,6 +3083,11 @@ eastasianwidth@^0.2.0: resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== +ebnf@^1.9.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/ebnf/-/ebnf-1.9.0.tgz#9c2dd6052f3ed43a69c1f0b07b15bd03cefda764" + integrity sha512-LKK899+j758AgPq00ms+y90mo+2P86fMKUWD28sH0zLKUj7aL6iIH2wy4jejAMM9I2BawJ+2kp6C3mMXj+Ii5g== + electron-to-chromium@^1.4.202: version "1.4.246" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.246.tgz#802132d1bbd3ff32ce82fcd6a6ed6ab59b4366dc" @@ -5020,6 +5038,17 @@ json-parse-even-better-errors@^2.3.0: resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== +json-schema-library@^7.4.4: + version "7.4.4" + resolved "https://registry.yarnpkg.com/json-schema-library/-/json-schema-library-7.4.4.tgz#8c09aa0a4e20cb5cc46c0b148ad9384ca1f861c0" + integrity sha512-pn1uP4UChkqFvJ9KAZnPgZZbn8QDMmROwhR0EnjS4yW97aCy7jiu4bwEPsAZbqLE+ioSosaayHgC8HzsX2OZkQ== + dependencies: + "@sagold/json-pointer" "^5.0.0" + "@sagold/json-query" "^6.0.0" + deepmerge "^4.2.2" + fast-deep-equal "^3.1.3" + valid-url "^1.0.9" + json-schema-traverse@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" @@ -7593,6 +7622,11 @@ v8-to-istanbul@^7.0.0: convert-source-map "^1.6.0" source-map "^0.7.3" +valid-url@^1.0.9: + version "1.0.9" + resolved "https://registry.yarnpkg.com/valid-url/-/valid-url-1.0.9.tgz#1c14479b40f1397a75782f115e4086447433a200" + integrity sha512-QQDsV8OnSf5Uc30CKSwG9lnhMPe6exHtTXLRYX8uMwKENy640pU+2BgBL0LRbDh/eYRahNCS7aewCx0wf3NYVA== + validate-npm-package-license@^3.0.1: version "3.0.4" resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" From 123a0a5f014e201538f2a506873718a402efdc94 Mon Sep 17 00:00:00 2001 From: Jacob Alford Date: Tue, 3 Jan 2023 19:49:54 -0700 Subject: [PATCH 14/21] [#137] remove unnecessary non-null assertion disablement --- src/base/JsonSchemaBase.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/base/JsonSchemaBase.ts b/src/base/JsonSchemaBase.ts index b39abe8f..3cb70346 100644 --- a/src/base/JsonSchemaBase.ts +++ b/src/base/JsonSchemaBase.ts @@ -246,7 +246,6 @@ export const isJsonExclude = (u: JsonSchema): u is JsonExclude => isJsonIntersection(u) && (RNEA.head(u.allOf) instanceof JsonExclude || hasKey('not', RNEA.head(u.allOf))) && u.allOf.length === 2 && - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion isJsonConst((RNEA.head(u.allOf) as JsonExclude).not) /** From e37c505209b61f8f8a22d68f01a65ec01a274e3a Mon Sep 17 00:00:00 2001 From: Jacob Alford Date: Tue, 3 Jan 2023 19:53:00 -0700 Subject: [PATCH 15/21] chore: bump package version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 50faf453..21fede19 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "schemata-ts", - "version": "1.1.0", + "version": "1.2.0", "description": "A collection of Schemata inspired by io-ts-types and validators.js", "homepage": "https://jacob-alford.github.io/schemata-ts/", "repository": { From bf5ac89d69c32659e242b943e5f172d62cb94034 Mon Sep 17 00:00:00 2001 From: Jacob Alford Date: Wed, 4 Jan 2023 17:53:07 -0700 Subject: [PATCH 16/21] [#137] convert arguments to object params --- src/base/JsonSchemaBase.ts | 110 +++++++++--------- src/schemables/WithAnnotate/definition.ts | 32 ++--- .../WithAnnotate/instances/schema.ts | 4 +- .../WithDate/instances/json-schema.ts | 4 +- .../WithFloat/instances/json-schema.ts | 2 +- .../WithInt/instances/json-schema.ts | 2 +- .../WithJson/instances/json-schema.ts | 4 +- .../WithMap/instances/json-schema.ts | 2 +- .../WithPadding/instances/json-schema.ts | 28 ++--- .../WithPattern/instances/json-schema.ts | 8 +- .../instances/json-schema.ts | 2 +- tests/JsonSchema.test.ts | 18 +-- tests/schemables/WithAnnotate.test.ts | 8 +- 13 files changed, 104 insertions(+), 120 deletions(-) diff --git a/src/base/JsonSchemaBase.ts b/src/base/JsonSchemaBase.ts index 3cb70346..7f95efa7 100644 --- a/src/base/JsonSchemaBase.ts +++ b/src/base/JsonSchemaBase.ts @@ -44,16 +44,15 @@ export type JsonSchema = */ export type JsonSchemaWithDescription = JsonSchema & Description -/** @internal */ interface Description { readonly title?: string readonly description?: string } -/** @internal */ +/** Matches anything */ class JsonEmpty {} -/** @internal */ +/** Matches a subset of strings */ class JsonString { readonly type = 'string' constructor( @@ -67,32 +66,32 @@ class JsonString { ) {} } -/** @internal */ +/** Matches a subset of floats */ class JsonNumber { readonly type = 'number' constructor(readonly minimum?: number, readonly maximum?: number) {} } -/** @internal */ +/** Matches a subset of integers */ class JsonInteger implements Omit { readonly type = 'integer' constructor(readonly minimum?: number, readonly maximum?: number) {} } -/** @internal */ +/** Matches true or false */ class JsonBoolean { readonly type = 'boolean' } -/** @internal */ +/** Matches a constant value */ interface JsonConst { readonly const: unknown } -/** @internal */ +/** Matches a boolean, number, string, or null constant value */ type JsonLiteral = (JsonBoolean | JsonNumber | JsonString | JsonNull) & JsonConst -/** @internal */ +/** Matches a set of properties with a given set of required properties. */ class JsonStruct { readonly type = 'object' constructor( @@ -101,13 +100,13 @@ class JsonStruct { ) {} } -/** @internal */ +/** Matches an object with uniform key values */ class JsonRecord { readonly type = 'object' constructor(readonly additionalProperties: JsonSchema) {} } -/** @internal */ +/** Matches a subset of arrays with uniform index values (or specific index values) */ class JsonArray { readonly type = 'array' constructor( @@ -117,23 +116,23 @@ class JsonArray { ) {} } -/** @internal */ +/** Matches null exactly */ class JsonNull { readonly type = 'null' readonly const = null } -/** @internal */ +/** Negates a schema */ class JsonExclude { constructor(readonly not: JsonSchema) {} } -/** @internal */ +/** Matches any of the supplied schemas */ class JsonUnion { constructor(readonly oneOf: ReadonlyArray) {} } -/** @internal */ +/** Matches all of the supplied schemas */ class JsonIntersection { constructor(readonly allOf: RNEA.ReadonlyNonEmptyArray) {} } @@ -243,10 +242,7 @@ export const isJsonArray = (u: JsonSchema): u is JsonArray => * @category Guards */ export const isJsonExclude = (u: JsonSchema): u is JsonExclude => - isJsonIntersection(u) && - (RNEA.head(u.allOf) instanceof JsonExclude || hasKey('not', RNEA.head(u.allOf))) && - u.allOf.length === 2 && - isJsonConst((RNEA.head(u.allOf) as JsonExclude).not) + u instanceof JsonExclude || hasKey('not', u) /** * @since 1.2.0 @@ -277,23 +273,25 @@ export const emptySchema = make(new JsonEmpty()) * @category Constructors */ export const makeStringSchema = ( - minLength?: number, - maxLength?: number, - pattern?: string, - contentEncoding?: string, - contentMediaType?: string, - contentSchema?: JsonSchema, - format?: string, + params: { + minLength?: number + maxLength?: number + pattern?: string + contentEncoding?: string + contentMediaType?: string + contentSchema?: JsonSchema + format?: string + } = {}, ): Const => make( new JsonString( - minLength, - maxLength, - pattern, - contentEncoding, - contentMediaType, - contentSchema, - format, + params.minLength, + params.maxLength, + params.pattern, + params.contentEncoding, + params.contentMediaType, + params.contentSchema, + params.format, ), ) @@ -302,18 +300,22 @@ export const makeStringSchema = ( * @category Constructors */ export const makeNumberSchema = ( - minimum?: number, - maximum?: number, -): Const => make(new JsonNumber(minimum, maximum)) + params: { + minimum?: number + maximum?: number + } = {}, +): Const => make(new JsonNumber(params.minimum, params.maximum)) /** * @since 1.2.0 * @category Constructors */ export const makeIntegerSchema = ( - minimum?: number, - maximum?: number, -): Const => make(new JsonInteger(minimum, maximum)) + params: { + minimum?: number + maximum?: number + } = {}, +): Const => make(new JsonInteger(params.minimum, params.maximum)) /** * @since 1.2.0 @@ -363,11 +365,10 @@ export const makeRecordSchema = ( * @since 1.2.0 * @category Constructors */ -export const makeArraySchema = ( - items: Const, - minItems?: number, - maxItems?: number, -): Const> => make(new JsonArray(items, minItems, maxItems)) +export const makeArraySchema = + (params: { minItems?: number; maxItems?: number } = {}) => + (items: Const): Const> => + make(new JsonArray(items, params.minItems, params.maxItems)) /** * @since 1.2.0 @@ -398,10 +399,10 @@ export const makeUnionSchema = ( - left: Const, - right: Const, -): Const => make(new JsonIntersection([left, right])) +export const makeIntersectionSchema = + (right: Const) => + (left: Const): Const => + make(new JsonIntersection([left, right])) /** * @since 1.2.0 @@ -417,11 +418,12 @@ export const makeExclusionSchema = ( * @since 1.2.0 * @category Combintators */ -export const annotate: ( - title?: string, - description?: string, -) => (schema: JsonSchema) => Const = - (title, description) => schema => +export const annotate: (params?: { + title?: string + description?: string +}) => (schema: JsonSchema) => Const = + ({ title, description } = {}) => + schema => title === undefined && description === undefined ? make(schema) : make({ @@ -472,7 +474,7 @@ export const Schemable: Schemable2 = { // @ts-expect-error -- typelevel difference partial: makeStructSchema, record: makeRecordSchema, - array: makeArraySchema, + array: makeArraySchema(), // @ts-expect-error -- typelevel difference tuple: >( ...items: { [K in keyof A]: Const } diff --git a/src/schemables/WithAnnotate/definition.ts b/src/schemables/WithAnnotate/definition.ts index 311a80e0..60d8eb5f 100644 --- a/src/schemables/WithAnnotate/definition.ts +++ b/src/schemables/WithAnnotate/definition.ts @@ -11,10 +11,10 @@ import { HKT2, Kind, Kind2, URIS, URIS2 } from 'fp-ts/HKT' * @category Model */ export interface WithAnnotateHKT2 { - readonly annotate: ( - title?: string, - description?: string, - ) => (schema: HKT2) => HKT2 + readonly annotate: (params?: { + title?: string + description?: string + }) => (schema: HKT2) => HKT2 } /** @@ -22,10 +22,10 @@ export interface WithAnnotateHKT2 { * @category Model */ export interface WithAnnotate1 { - readonly annotate: ( - title?: string, - description?: string, - ) => (schema: Kind) => Kind + readonly annotate: (params?: { + title?: string + description?: string + }) => (schema: Kind) => Kind } /** @@ -33,10 +33,10 @@ export interface WithAnnotate1 { * @category Model */ export interface WithAnnotate2 { - readonly annotate: ( - title?: string, - description?: string, - ) => (schema: Kind2) => Kind2 + readonly annotate: (params?: { + title?: string + description?: string + }) => (schema: Kind2) => Kind2 } /** @@ -44,8 +44,8 @@ export interface WithAnnotate2 { * @category Model */ export interface WithAnnotate2C { - readonly annotate: ( - title?: string, - description?: string, - ) => (schema: Kind2) => Kind2 + readonly annotate: (params?: { + title?: string + description?: string + }) => (schema: Kind2) => Kind2 } diff --git a/src/schemables/WithAnnotate/instances/schema.ts b/src/schemables/WithAnnotate/instances/schema.ts index dbe187b6..338e8775 100644 --- a/src/schemables/WithAnnotate/instances/schema.ts +++ b/src/schemables/WithAnnotate/instances/schema.ts @@ -11,6 +11,6 @@ import * as SC from '../../../SchemaExt' * @category Combinators */ export const Schema = - (title?: string, description?: string) => + (params?: { title?: string; description?: string }) => (schema: SC.SchemaExt): SC.SchemaExt => - SC.make(s => s.annotate(title, description)(schema(s))) + SC.make(s => s.annotate(params)(schema(s))) diff --git a/src/schemables/WithDate/instances/json-schema.ts b/src/schemables/WithDate/instances/json-schema.ts index 9d3f3e03..c99f6070 100644 --- a/src/schemables/WithDate/instances/json-schema.ts +++ b/src/schemables/WithDate/instances/json-schema.ts @@ -6,13 +6,11 @@ import * as JS from '../../../base/JsonSchemaBase' import { WithDate2 } from '../definition' -const _ = undefined - /** * @since 1.2.0 * @category Instances */ export const JsonSchema: WithDate2 = { date: JS.emptySchema, - dateFromString: JS.makeStringSchema(_, _, _, _, _, _, 'date'), + dateFromString: JS.makeStringSchema({ format: 'date' }), } diff --git a/src/schemables/WithFloat/instances/json-schema.ts b/src/schemables/WithFloat/instances/json-schema.ts index cd2b5212..af22598b 100644 --- a/src/schemables/WithFloat/instances/json-schema.ts +++ b/src/schemables/WithFloat/instances/json-schema.ts @@ -19,6 +19,6 @@ import { WithFloat2 } from '../definition' export const JsonSchema: WithFloat2 = { float(params = {}) { const { min = -Number.MAX_VALUE, max = Number.MAX_VALUE } = params - return JS.makeNumberSchema(min, max) + return JS.makeNumberSchema({ minimum: min, maximum: max }) }, } diff --git a/src/schemables/WithInt/instances/json-schema.ts b/src/schemables/WithInt/instances/json-schema.ts index babbc323..762a0fa6 100644 --- a/src/schemables/WithInt/instances/json-schema.ts +++ b/src/schemables/WithInt/instances/json-schema.ts @@ -19,6 +19,6 @@ import { WithInt2 } from '../definition' export const JsonSchema: WithInt2 = { int: (params = {}) => { const { min = Number.MIN_SAFE_INTEGER, max = Number.MAX_SAFE_INTEGER } = params - return JS.makeIntegerSchema(min, max) + return JS.makeIntegerSchema({ minimum: min, maximum: max }) }, } diff --git a/src/schemables/WithJson/instances/json-schema.ts b/src/schemables/WithJson/instances/json-schema.ts index 7c9affae..58ffcbd1 100644 --- a/src/schemables/WithJson/instances/json-schema.ts +++ b/src/schemables/WithJson/instances/json-schema.ts @@ -6,13 +6,11 @@ import * as JS from '../../../base/JsonSchemaBase' import { WithJson2 } from '../definition' -const _ = undefined - /** * @since 1.2.0 * @category Instances */ export const JsonSchema: WithJson2 = { json: JS.emptySchema, - jsonString: JS.makeStringSchema(_, _, _, _, 'application/json'), + jsonString: JS.makeStringSchema({ contentMediaType: 'application/json' }), } diff --git a/src/schemables/WithMap/instances/json-schema.ts b/src/schemables/WithMap/instances/json-schema.ts index 8df8969e..a5690ae0 100644 --- a/src/schemables/WithMap/instances/json-schema.ts +++ b/src/schemables/WithMap/instances/json-schema.ts @@ -11,5 +11,5 @@ import { WithMap2 } from '../definition' * @category Instances */ export const JsonSchema: WithMap2 = { - mapFromEntries: (_, jsK, jsA) => JS.makeArraySchema(JS.Schemable.tuple(jsK, jsA)), + mapFromEntries: (_, jsK, jsA) => JS.makeArraySchema()(JS.Schemable.tuple(jsK, jsA)), } diff --git a/src/schemables/WithPadding/instances/json-schema.ts b/src/schemables/WithPadding/instances/json-schema.ts index f344798a..d553fd87 100644 --- a/src/schemables/WithPadding/instances/json-schema.ts +++ b/src/schemables/WithPadding/instances/json-schema.ts @@ -20,18 +20,14 @@ export const JsonSchema: WithPadding2 = { match({ MaxLength: ({ maxLength }) => typeof maxLength === 'number' - ? JS.makeIntersectionSchema( - JS.makeStringSchema(undefined, maxLength), - stringSchema, - ) - : JS.makeIntersectionSchema(JS.makeStringSchema(), stringSchema), + ? JS.makeIntersectionSchema(stringSchema)(JS.makeStringSchema({ maxLength })) + : JS.makeIntersectionSchema(stringSchema)(JS.makeStringSchema()), ExactLength: ({ exactLength }) => typeof exactLength === 'number' - ? JS.makeIntersectionSchema( - JS.makeStringSchema(exactLength, exactLength), - stringSchema, + ? JS.makeIntersectionSchema(stringSchema)( + JS.makeStringSchema({ minLength: exactLength, maxLength: exactLength }), ) - : JS.makeIntersectionSchema(JS.makeStringSchema(), stringSchema), + : JS.makeIntersectionSchema(stringSchema)(JS.makeStringSchema()), }), ), padRight: length => stringSchema => @@ -40,18 +36,14 @@ export const JsonSchema: WithPadding2 = { match({ MaxLength: ({ maxLength }) => typeof maxLength === 'number' - ? JS.makeIntersectionSchema( - JS.makeStringSchema(undefined, maxLength), - stringSchema, - ) - : JS.makeIntersectionSchema(JS.makeStringSchema(), stringSchema), + ? JS.makeIntersectionSchema(stringSchema)(JS.makeStringSchema({ maxLength })) + : JS.makeIntersectionSchema(stringSchema)(JS.makeStringSchema()), ExactLength: ({ exactLength }) => typeof exactLength === 'number' - ? JS.makeIntersectionSchema( - JS.makeStringSchema(exactLength, exactLength), - stringSchema, + ? JS.makeIntersectionSchema(stringSchema)( + JS.makeStringSchema({ minLength: exactLength, maxLength: exactLength }), ) - : JS.makeIntersectionSchema(JS.makeStringSchema(), stringSchema), + : JS.makeIntersectionSchema(stringSchema)(JS.makeStringSchema()), }), ), } diff --git a/src/schemables/WithPattern/instances/json-schema.ts b/src/schemables/WithPattern/instances/json-schema.ts index 828e108b..c272ca51 100644 --- a/src/schemables/WithPattern/instances/json-schema.ts +++ b/src/schemables/WithPattern/instances/json-schema.ts @@ -9,8 +9,6 @@ import * as JS from '../../../base/JsonSchemaBase' import * as PB from '../../../PatternBuilder' import { WithPattern2 } from '../definition' -const _ = undefined - /** * @since 1.2.0 * @category Instances @@ -18,7 +16,9 @@ const _ = undefined export const JsonSchema: WithPattern2 = { pattern: (pattern, description, caseInsensitive) => pipe( - JS.makeStringSchema(_, _, PB.regexFromPattern(pattern, caseInsensitive).source), - JS.annotate(_, description), + JS.makeStringSchema({ + pattern: PB.regexFromPattern(pattern, caseInsensitive).source, + }), + JS.annotate({ description }), ), } diff --git a/src/schemables/WithUnknownContainers/instances/json-schema.ts b/src/schemables/WithUnknownContainers/instances/json-schema.ts index a76a418f..0e17d5e4 100644 --- a/src/schemables/WithUnknownContainers/instances/json-schema.ts +++ b/src/schemables/WithUnknownContainers/instances/json-schema.ts @@ -7,6 +7,6 @@ import { WithUnknownContainers2 } from '../definition' * @category Instances */ export const JsonSchema: WithUnknownContainers2 = { - UnknownArray: JS.makeArraySchema(JS.emptySchema), + UnknownArray: JS.makeArraySchema()(JS.emptySchema), UnknownRecord: JS.makeRecordSchema(JS.emptySchema), } diff --git a/tests/JsonSchema.test.ts b/tests/JsonSchema.test.ts index 5d48b40a..0d507b49 100644 --- a/tests/JsonSchema.test.ts +++ b/tests/JsonSchema.test.ts @@ -38,7 +38,7 @@ describe('JsonSchema', () => { expect(base.isJsonLiteral({ type: 'string', const: 'string' })).toBe(true) }) test('isJsonArray', () => { - expect(base.isJsonArray(base.makeArraySchema(base.emptySchema))).toBe(true) + expect(base.isJsonArray(base.makeArraySchema()(base.emptySchema))).toBe(true) expect(base.isJsonArray({ type: 'array', items: {} })).toBe(true) }) test('isJsonStruct', () => { @@ -60,19 +60,11 @@ describe('JsonSchema', () => { ).toBe(true) }) test('isJsonExclude', () => { - expect( - base.isJsonExclude(base.makeExclusionSchema(5, base.makeNumberSchema())), - ).toBe(true) expect( base.isJsonExclude({ - allOf: [ - { - not: { - const: 5, - }, - }, - { type: 'number' }, - ], + not: { + const: 5, + }, }), ).toBe(true) }) @@ -91,7 +83,7 @@ describe('JsonSchema', () => { test('isJsonIntersection', () => { expect( base.isJsonIntersection( - base.makeIntersectionSchema(base.makeStringSchema(), base.makeNumberSchema()), + base.makeIntersectionSchema(base.makeStringSchema())(base.makeNumberSchema()), ), ).toBe(true) expect( diff --git a/tests/schemables/WithAnnotate.test.ts b/tests/schemables/WithAnnotate.test.ts index 26b9d4a5..7192d596 100644 --- a/tests/schemables/WithAnnotate.test.ts +++ b/tests/schemables/WithAnnotate.test.ts @@ -4,8 +4,8 @@ import * as S from '../../src/schemata' describe('annotation', () => { it('annotates', () => { - const schema = S.Annotate('root')(S.BigIntFromString()) - const jsonSchema = JSON.parse(JSON.stringify(getJsonSchema(schema))) + const schema = S.Annotate({ title: 'root' })(S.BigIntFromString()) + const jsonSchema = getJsonSchema(schema) expect(jsonSchema).toEqual({ type: 'string', pattern: '^((0b)[0-1]+?|(0o)[0-7]+?|-?\\d+?|(0x)[0-9A-Fa-f]+?)$', @@ -16,6 +16,8 @@ describe('annotation', () => { it('doesnt trample class names', () => { const schema = S.Annotate()(S.Natural) const jsonSchema = getJsonSchema(schema) - expect(jsonSchema).toStrictEqual(makeIntegerSchema(0, 9007199254740991)) + expect(jsonSchema).toStrictEqual( + makeIntegerSchema({ minimum: 0, maximum: 9007199254740991 }), + ) }) }) From 2ea000ca0cf867cb3071e2f223f07a049a60236d Mon Sep 17 00:00:00 2001 From: Jacob Alford Date: Wed, 4 Jan 2023 17:53:52 -0700 Subject: [PATCH 17/21] [#137] correct lib --- src/base/JsonSchemaBase.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/base/JsonSchemaBase.ts b/src/base/JsonSchemaBase.ts index 7f95efa7..a1aeeb2c 100644 --- a/src/base/JsonSchemaBase.ts +++ b/src/base/JsonSchemaBase.ts @@ -4,8 +4,8 @@ * @since 1.2.0 * @see https://json-schema.org/draft/2020-12/json-schema-validation.html */ +import { Const, make } from 'fp-ts/Const' import { identity, pipe } from 'fp-ts/function' -import { Const, make } from 'fp-ts/lib/Const' import * as RNEA from 'fp-ts/ReadonlyNonEmptyArray' import * as RR from 'fp-ts/ReadonlyRecord' import * as Str from 'fp-ts/string' From 44f41484cadd5deab62d243383ffcf11fd7deb34 Mon Sep 17 00:00:00 2001 From: Jacob Alford Date: Wed, 4 Jan 2023 18:16:13 -0700 Subject: [PATCH 18/21] chore: make witherS consistent with wither signature, cleanup printer --- src/base/PrinterBase.ts | 85 ++++++++++++++++--------------------- src/internal/util.ts | 16 ++++--- tests/internal.util.test.ts | 8 ++-- 3 files changed, 51 insertions(+), 58 deletions(-) diff --git a/src/base/PrinterBase.ts b/src/base/PrinterBase.ts index 1816ca47..3bb1460e 100644 --- a/src/base/PrinterBase.ts +++ b/src/base/PrinterBase.ts @@ -218,28 +218,24 @@ export const boolean: Printer = { * @category Primitives */ export const UnknownArray: Printer, Array> = { - domainToJson: input => - pipe( - input, - RA.traverseWithIndex(printerValidation)((i, v) => - pipe( - toJson(v), - E.mapLeft(err => new PE.ErrorAtIndex(i, err)), - ), + domainToJson: flow( + RA.traverseWithIndex(printerValidation)((i, v) => + pipe( + toJson(v), + E.mapLeft(err => new PE.ErrorAtIndex(i, err)), ), - E.map(safeJsonArray), ), - codomainToJson: input => - pipe( - input, - RA.traverseWithIndex(printerValidation)((i, v) => - pipe( - toJson(v), - E.mapLeft(err => new PE.ErrorAtIndex(i, err)), - ), + E.map(safeJsonArray), + ), + codomainToJson: flow( + RA.traverseWithIndex(printerValidation)((i, v) => + pipe( + toJson(v), + E.mapLeft(err => new PE.ErrorAtIndex(i, err)), ), - E.map(safeJsonArray), ), + E.map(safeJsonArray), + ), } /** @@ -247,28 +243,24 @@ export const UnknownArray: Printer, Array> = { * @category Primitives */ export const UnknownRecord: Printer, Record> = { - domainToJson: input => - pipe( - input, - RR.traverseWithIndex(printerValidation)((key, v) => - pipe( - toJson(v), - E.mapLeft(err => new PE.ErrorAtKey(key, err)), - ), + domainToJson: flow( + RR.traverseWithIndex(printerValidation)((key, v) => + pipe( + toJson(v), + E.mapLeft(err => new PE.ErrorAtKey(key, err)), ), - E.map(safeJsonRecord), ), - codomainToJson: input => - pipe( - input, - RR.traverseWithIndex(printerValidation)((key, v) => - pipe( - toJson(v), - E.mapLeft(err => new PE.ErrorAtKey(key, err)), - ), + E.map(safeJsonRecord), + ), + codomainToJson: flow( + RR.traverseWithIndex(printerValidation)((key, v) => + pipe( + toJson(v), + E.mapLeft(err => new PE.ErrorAtKey(key, err)), ), - E.map(safeJsonRecord), ), + E.map(safeJsonRecord), + ), } // ------------------------------------------------------------------------------------- @@ -318,11 +310,9 @@ export const struct =

>>( pipe( properties, witherS(PE.semigroupPrintingError)((key, printer) => - O.some( - pipe( - printer.domainToJson(input[key]), - E.mapLeft((err): PE.PrintError => new PE.ErrorAtKey(key as string, err)), - ), + pipe( + printer.domainToJson(input[key]), + E.bimap((err): PE.PrintError => new PE.ErrorAtKey(key as string, err), O.some), ), ), E.map(safeJsonRecord), @@ -331,11 +321,9 @@ export const struct =

>>( pipe( properties, witherS(PE.semigroupPrintingError)((key, printer) => - O.some( - pipe( - printer.codomainToJson(input[key]), - E.mapLeft(err => new PE.ErrorAtKey(key as string, err)), - ), + pipe( + printer.codomainToJson(input[key]), + E.bimap(err => new PE.ErrorAtKey(key as string, err), O.some), ), ), E.map(safeJsonRecord), @@ -359,7 +347,7 @@ export const partial =

>>( pipe( input[key], O.fromNullable, - O.map(value => + O.traverse(E.Applicative)(value => pipe( printer.domainToJson(value), E.mapLeft(err => new PE.ErrorAtKey(key as string, err)), @@ -376,7 +364,7 @@ export const partial =

>>( pipe( input[key], O.fromNullable, - O.map(value => + O.traverse(E.Applicative)(value => pipe( printer.codomainToJson(value), E.mapLeft(err => new PE.ErrorAtKey(key as string, err)), @@ -402,7 +390,6 @@ export const record = ( E.mapLeft(err => new PE.ErrorAtKey(k, err)), ), ), - E.map(safeJsonRecord), ), codomainToJson: flow( diff --git a/src/internal/util.ts b/src/internal/util.ts index 851f450c..da86851e 100644 --- a/src/internal/util.ts +++ b/src/internal/util.ts @@ -39,7 +39,7 @@ export const typeOf = (x: unknown): string => (x === null ? 'null' : typeof x) export const witherS = (sgErrors: Sg.Semigroup) => , A>( - f: (key: K, value: In[K]) => O.Option>, + f: (key: K, value: In[K]) => E.Either>, ) => (s: In): E.Either => { const errors: E[] = [] @@ -48,13 +48,19 @@ export const witherS = for (const key in s) { /* Ignores inherited properties */ if (!hasOwn(s, key)) continue + /* Perform effect */ const result = f(key, s[key]) + + /* add any errors to accumulation */ + if (E.isLeft(result)) { + errors.push(result.left) + continue + } + /* none => skip */ - if (O.isNone(result)) continue - /* Bail early if effect failed */ - if (E.isLeft(result.value)) errors.push(result.value.left) - /* Otherwise, add result to output */ else out[key] = result.value.right + if (O.isNone(result.right)) continue + else out[key] = result.right.value } return RA.isNonEmpty(errors) ? E.left(pipe(errors, RNEA.concatAll(sgErrors))) diff --git a/tests/internal.util.test.ts b/tests/internal.util.test.ts index 8aae57c1..42fb5248 100644 --- a/tests/internal.util.test.ts +++ b/tests/internal.util.test.ts @@ -19,7 +19,7 @@ describe('traverseESO', () => { obj, witherS(RA.getMonoid())((_, value) => { tester(value) - return O.some(E.right(value)) + return E.right(O.some(value)) }), ) expect(tester).toHaveBeenCalledTimes(1) @@ -37,7 +37,7 @@ describe('traverseESO', () => { obj, witherS(RA.getMonoid())((_, value) => { tester(value) - return O.some(E.right(value)) + return E.right(O.some(value)) }), ) expect(tester).toHaveBeenCalledTimes(1) @@ -52,7 +52,7 @@ describe('traverseESO', () => { const result = pipe( obj, witherS(RA.getMonoid())((_, value) => { - return value === 2 ? O.none : O.some(E.right(value)) + return value === 2 ? E.right(O.none) : E.right(O.some(value)) }), ) expect(result).toStrictEqual(E.right({ a: 1, c: 3 })) @@ -68,7 +68,7 @@ describe('traverseESO', () => { obj, witherS(RA.getMonoid())((_, value) => { tester(value) - return value >= 2 ? O.some(E.left(['fail'])) : O.some(E.right(value)) + return value >= 2 ? E.left(['fail']) : E.right(O.some(value)) }), ) expect(result).toStrictEqual(E.left(['fail', 'fail'])) From 960d6a9a3ba45ef53d5fc852a3120866dd6749c4 Mon Sep 17 00:00:00 2001 From: Jacob Alford Date: Wed, 4 Jan 2023 19:53:38 -0700 Subject: [PATCH 19/21] [#137] update README --- README.md | 96 +++++++++++++++++++++++++++++++++++--- src/PatternBuilder.ts | 14 +++++- src/base/JsonSchemaBase.ts | 41 ++++++++++++++++ tests/JsonSchema.test.ts | 5 +- 4 files changed, 145 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 19460e7c..0aad3983 100644 --- a/README.md +++ b/README.md @@ -42,14 +42,10 @@ yarn add schemata-ts npm install schemata-ts ``` -## Codec +## Codec and Arbitrary A codec is a typeclass that contains the methods of `Decoder`, `Encoder`, `JsonSerializer`, `JsonDeserializer`, and `Guard`. Decoder and encoder are lossless when composed together. This means that for all domain types for which an encoder encodes to, a decoder will return a valid `E.Right` value. -Likewise, `JsonSerializer` and `JsonDeserializer` are lossless when composed together. Certain data types in Javascript like `NaN`, `undefined`, `Infinity`, and others are not part of the JSON specification, and `JSON.stringify` will turn these values into something different. This means that if you stringify these types and attempt to parse, you will get a different object than you originally started with. Additionally, JSON cannot stringify `bigint`, and cannot contain circular references. Under these circumstances `JSON.stringify` may throw an error. - -`JsonSerializer` and `JsonDeserializer` are typeclasses included in a Codec which are lossless when composed together. So anything that successfully stringifies using `JsonSerializer` will successfully parse with `JsonDeserializer` and be equivalent objects. This is useful to avoid bugs when using JSON strings for storing data. Additionally, `JsonDeserializer` will decode the Json string into a domain type for immediate use in your program. - ### User Document Example This is a live example found in `src/Codec.ts` type-checked and tested with [docs-ts](https://github.com/gcanti/docs-ts). @@ -140,6 +136,94 @@ assert.deepStrictEqual( ) ``` +## Json Serializer and Deserializer + +Like encoder and decoder, `JsonSerializer` and `JsonDeserializer` are lossless when composed together. Certain data types in Javascript like `NaN`, `undefined`, `Infinity`, and others are not part of the JSON specification, and `JSON.stringify` will turn these values into something different (or omit them). This means that if you stringify these types and attempt to parse, you will get a different object than you originally started with. Additionally, JSON cannot stringify `bigint`, and cannot contain circular references. Under these circumstances `JSON.stringify` will throw an error. + +Anything that successfully stringifies using `JsonSerializer` will successfully parse with `JsonDeserializer` and will be equivalent objects. This is useful to avoid bugs when using JSON strings for storing data. Additionally, `JsonDeserializer` will decode the Json string into a domain type for immediate use in your program. + +## Deriving JSON Schema + +Schemata-ts comes with its own implementation of [JSON-Schema](https://json-schema.org/) and is a validation standard that can be used to validate artifacts in many other languages and frameworks. Schemata-ts's implementation is compatible with JSON Schema Draft 4, Draft 6, Draft 7, Draft 2019-09, and has partial support for 2020-12. _Note_: string `format` (like regex, contentType, or mediaType) is only available starting with Draft 6, and tuples are not compatible with Draft 2020-12. + +### Customer JSON Schema Example + +This is a live example generating a JSON Schema in `src/base/JsonSchemaBase.ts` + +```ts +import * as JS from 'schemata-ts/base/JsonSchemaBase' +import * as S from 'schemata-ts/schemata' +import { getJsonSchema } from 'schemata-ts/JsonSchema' + +const schema = S.Struct({ + id: S.Natural, + jwt: S.Jwt, + tag: S.Literal('Customer'), +}) + +const jsonSchema = getJsonSchema(schema) + +assert.deepStrictEqual(JS.stripIdentity(jsonSchema), { + type: 'object', + required: ['id', 'jwt', 'tag'], + properties: { + id: { type: 'integer', minimum: 0, maximum: 9007199254740991 }, + jwt: { + type: 'string', + description: 'Jwt', + pattern: + '^(([A-Za-z0-9_\\x2d]*)\\.([A-Za-z0-9_\\x2d]*)(\\.([A-Za-z0-9_\\x2d]*)){0,1})$', + }, + tag: { type: 'string', const: 'Customer' }, + }, +}) +``` + +A note on `JS.stripIdentity` in the above example: internally, JSON Schema is represented as a union of Typescript classes. This is handy when inspecting the schema because the name of the schema is shown next to its properties. Because this prevents equality comparison, schemata-ts exposes a method `stripIdentity` to remove the object's class identity. _Caution_: this method stringifies and parses the schema and may throw if the schema itself contains circularity. + +## Pattern Builder + +Schemata-ts comes with powerful regex combinators that are used to construct regex from comprehensible atoms. The `Pattern` schema allows extension of a string schema to a subset of strings defined by a pattern. Decoders and Guards guarantee that a string conforms to the specified pattern, Arbitrary generates strings that follow the pattern, and Json Schema generates string schemata that lists the pattern as a property. + +### US Phone Number Example + +This is a live example validating US Phone numbers found in `src/PatternBuilder.ts` + +```ts +import * as PB from 'schemata-ts/PatternBuilder' +import { pipe } from 'fp-ts/function' + +const digit = PB.characterClass(false, ['0', '9']) + +const areaCode = pipe( + pipe( + PB.char('('), + PB.then(PB.times(3)(digit)), + PB.then(PB.char(')')), + PB.then(PB.maybe(PB.char(' '))), + ), + PB.or(PB.times(3)(digit)), + PB.subgroup, +) + +const prefix = PB.times(3)(digit) + +const lineNumber = PB.times(4)(digit) + +export const usPhoneNumber = pipe( + areaCode, + PB.then(pipe(PB.char('-'), PB.maybe)), + PB.then(prefix), + PB.then(PB.char('-')), + PB.then(lineNumber), +) + +assert.equal(PB.regexFromPattern(usPhoneNumber).test('(123) 456-7890'), true) +assert.equal(PB.regexFromPattern(usPhoneNumber).test('(123)456-7890'), true) +assert.equal(PB.regexFromPattern(usPhoneNumber).test('123-456-7890'), true) +assert.equal(PB.regexFromPattern(usPhoneNumber).test('1234567890'), false) +``` + ## Documentation - [schemata-ts](https://jacob-alford.github.io/schemata-ts/docs/modules) @@ -203,7 +287,7 @@ Additionally, there are more unlisted base schemable schemata also exported from schemata-ts is planned to support the following features in various 1.x and 2.x versions in the near future: - ~~JsonSerializer and JsonDeserializer: encoding / decoding from Json strings~~ Added in 1.1.0 -- Json Schema: generating JSON-Schema from schemata ([#137](https://github.com/jacob-alford/schemata-ts/issues/137)) +- ~~Json Schema: generating JSON-Schema from schemata ([#137](https://github.com/jacob-alford/schemata-ts/issues/137))~~ Added in 1.2.0 - Optic Schema: generating optics from schemata ([#134](https://github.com/jacob-alford/schemata-ts/issues/134)) - Mapped Structs: conversions between struct types, i.e. `snake-case` keys to `camelCase` keys - More generic schemata: (SetFromArray, ~~NonEmptyArray~~ Added in 1.1.0) diff --git a/src/PatternBuilder.ts b/src/PatternBuilder.ts index ed65efc9..0b7af83a 100644 --- a/src/PatternBuilder.ts +++ b/src/PatternBuilder.ts @@ -13,7 +13,12 @@ * const digit = PB.characterClass(false, ['0', '9']) * * const areaCode = pipe( - * pipe(PB.char('('), PB.then(PB.times(3)(digit)), PB.then(PB.char(')'))), + * pipe( + * PB.char('('), + * PB.then(PB.times(3)(digit)), + * PB.then(PB.char(')')), + * PB.then(PB.maybe(PB.char(' '))), + * ), * PB.or(PB.times(3)(digit)), * PB.subgroup, * ) @@ -24,11 +29,16 @@ * * export const usPhoneNumber = pipe( * areaCode, - * PB.then(PB.char('-')), + * PB.then(pipe(PB.char('-'), PB.maybe)), * PB.then(prefix), * PB.then(PB.char('-')), * PB.then(lineNumber), * ) + * + * assert.equal(PB.regexFromPattern(usPhoneNumber).test('(123) 456-7890'), true) + * assert.equal(PB.regexFromPattern(usPhoneNumber).test('(123)456-7890'), true) + * assert.equal(PB.regexFromPattern(usPhoneNumber).test('123-456-7890'), true) + * assert.equal(PB.regexFromPattern(usPhoneNumber).test('1234567890'), false) */ import { pipe } from 'fp-ts/function' import * as O from 'fp-ts/Option' diff --git a/src/base/JsonSchemaBase.ts b/src/base/JsonSchemaBase.ts index a1aeeb2c..60abc7d7 100644 --- a/src/base/JsonSchemaBase.ts +++ b/src/base/JsonSchemaBase.ts @@ -2,6 +2,34 @@ * Models for JsonSchema as subsets of JSON Schema Draft 4, Draft 6, and Draft 7. * * @since 1.2.0 + * @example + * import * as JS from 'schemata-ts/base/JsonSchemaBase' + * import * as S from 'schemata-ts/schemata' + * import { getJsonSchema } from 'schemata-ts/JsonSchema' + * + * const schema = S.Struct({ + * id: S.Natural, + * jwt: S.Jwt, + * tag: S.Literal('Customer'), + * }) + * + * const jsonSchema = getJsonSchema(schema) + * + * assert.deepStrictEqual(JS.stripIdentity(jsonSchema), { + * type: 'object', + * required: ['id', 'jwt', 'tag'], + * properties: { + * id: { type: 'integer', minimum: 0, maximum: 9007199254740991 }, + * jwt: { + * type: 'string', + * description: 'Jwt', + * pattern: + * '^(([A-Za-z0-9_\\x2d]*)\\.([A-Za-z0-9_\\x2d]*)(\\.([A-Za-z0-9_\\x2d]*)){0,1})$', + * }, + * tag: { type: 'string', const: 'Customer' }, + * }, + * }) + * * @see https://json-schema.org/draft/2020-12/json-schema-validation.html */ import { Const, make } from 'fp-ts/Const' @@ -432,6 +460,19 @@ export const annotate: (params?: { ...(description === undefined ? {} : { description }), }) +// ------------------------------------------------------------------------------------- +// Destructors +// ------------------------------------------------------------------------------------- + +/** + * Removes the internal class identities from a `JsonSchema` + * + * @since 1.2.0 + * @category Destructors + */ +export const stripIdentity: (schema: Const) => JsonSchema = schema => + JSON.parse(JSON.stringify(schema)) + // ------------------------------------------------------------------------------------- // instances // ------------------------------------------------------------------------------------- diff --git a/tests/JsonSchema.test.ts b/tests/JsonSchema.test.ts index 0d507b49..637d0f73 100644 --- a/tests/JsonSchema.test.ts +++ b/tests/JsonSchema.test.ts @@ -167,8 +167,7 @@ describe('JsonSchema', () => { }), }), ) - const jsonSchema = getJsonSchema(testSchema) - const testValue = JSON.parse(JSON.stringify(jsonSchema)) as any + const testValue = base.stripIdentity(getJsonSchema(testSchema)) as any test('struct', () => { expect(testValue.type).toBe('object') expect(testValue.required).toStrictEqual([ @@ -469,7 +468,7 @@ describe('JsonSchema', () => { rec: S.Record(S.CreditCard), arr: S.Array(S.Json.jsonString), - tup: S.Tuple(S.Number), + tup: S.Tuple(S.Number, S.Number), sum: S.Sum('type')({ a: S.Struct({ type: S.Literal('a'), a: S.Boolean }), b: S.Struct({ type: S.Literal('b'), b: S.Lazy('Sum[b].b', () => S.Natural) }), From 3e9f6d732c7078bcb27f58e736984b9de0f60ce3 Mon Sep 17 00:00:00 2001 From: Jacob Alford Date: Wed, 4 Jan 2023 19:55:34 -0700 Subject: [PATCH 20/21] [#137] update docs index --- docs/index.md | 110 ++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 106 insertions(+), 4 deletions(-) diff --git a/docs/index.md b/docs/index.md index fd196c0a..72e43595 100644 --- a/docs/index.md +++ b/docs/index.md @@ -17,13 +17,27 @@ nav_order: 1 A schema is an expression of a type structure that can be used to generate typeclass instances from a single declaration. Typeclass instances can perform a variety of tasks, for instance `Decoder` can take a pesky `unknown` value and give you an Either in return where the success case abides by the `schema` that generated it. The example below constructs a `User` schema. -## Codec +## Installation -A codec is a typeclass that contains the methods of `Decoder`, `Encoder`, `JsonSerializer`, `JsonDeserializer`, and `Guard`. Decoder and encoder are lossless when composed together. This means that for all domain types for which an encoder encodes to, a decoder will return a valid `E.Right` value. +Uses `fp-ts`, and `io-ts` as peer dependencies. Read more about peer dependencies at [nodejs.org](https://nodejs.org/en/blog/npm/peer-dependencies/). + +Also contains `fast-check` as a soft peer dependency. Soft peer dependency implies that usage of the `Arbitrary` module requires fast-check as a peer-dependency. + +### Yarn + +```bash +yarn add schemata-ts +``` -Likewise, `JsonSerializer` and `JsonDeserializer` are lossless when composed together. Certain data types in Javascript like `NaN`, `undefined`, `Infinity`, and others are not part of the JSON specification, and `JSON.stringify` will turn these values into something different. This means that if you stringify these types and attempt to parse, you will get a different object than you originally started with. Additionally, JSON cannot stringify `bigint`, and cannot contain circular references. Under these circumstances `JSON.stringify` may throw an error. +### NPM -`JsonSerializer` and `JsonDeserializer` are typeclasses included in a Codec which are lossless when composed together. So anything that successfully stringifies using `JsonSerializer` will successfully parse with `JsonDeserializer` and be equivalent objects. This is useful to avoid bugs when using JSON strings for storing data. Additionally, `JsonDeserializer` will decode the Json string into a domain type for immediate use in your program. +```bash +npm install schemata-ts +``` + +## Codec and Arbitrary + +A codec is a typeclass that contains the methods of `Decoder`, `Encoder`, `JsonSerializer`, `JsonDeserializer`, and `Guard`. Decoder and encoder are lossless when composed together. This means that for all domain types for which an encoder encodes to, a decoder will return a valid `E.Right` value. ### User Document Example @@ -114,3 +128,91 @@ assert.deepStrictEqual( E.right(testUsers), ) ``` + +## Json Serializer and Deserializer + +Like encoder and decoder, `JsonSerializer` and `JsonDeserializer` are lossless when composed together. Certain data types in Javascript like `NaN`, `undefined`, `Infinity`, and others are not part of the JSON specification, and `JSON.stringify` will turn these values into something different (or omit them). This means that if you stringify these types and attempt to parse, you will get a different object than you originally started with. Additionally, JSON cannot stringify `bigint`, and cannot contain circular references. Under these circumstances `JSON.stringify` will throw an error. + +Anything that successfully stringifies using `JsonSerializer` will successfully parse with `JsonDeserializer` and will be equivalent objects. This is useful to avoid bugs when using JSON strings for storing data. Additionally, `JsonDeserializer` will decode the Json string into a domain type for immediate use in your program. + +## Deriving JSON Schema + +Schemata-ts comes with its own implementation of [JSON-Schema](https://json-schema.org/) and is a validation standard that can be used to validate artifacts in many other languages and frameworks. Schemata-ts's implementation is compatible with JSON Schema Draft 4, Draft 6, Draft 7, Draft 2019-09, and has partial support for 2020-12. _Note_: string `format` (like regex, contentType, or mediaType) is only available starting with Draft 6, and tuples are not compatible with Draft 2020-12. + +### Customer JSON Schema Example + +This is a live example generating a JSON Schema in `src/base/JsonSchemaBase.ts` + +```ts +import * as JS from 'schemata-ts/base/JsonSchemaBase' +import * as S from 'schemata-ts/schemata' +import { getJsonSchema } from 'schemata-ts/JsonSchema' + +const schema = S.Struct({ + id: S.Natural, + jwt: S.Jwt, + tag: S.Literal('Customer'), +}) + +const jsonSchema = getJsonSchema(schema) + +assert.deepStrictEqual(JS.stripIdentity(jsonSchema), { + type: 'object', + required: ['id', 'jwt', 'tag'], + properties: { + id: { type: 'integer', minimum: 0, maximum: 9007199254740991 }, + jwt: { + type: 'string', + description: 'Jwt', + pattern: + '^(([A-Za-z0-9_\\x2d]*)\\.([A-Za-z0-9_\\x2d]*)(\\.([A-Za-z0-9_\\x2d]*)){0,1})$', + }, + tag: { type: 'string', const: 'Customer' }, + }, +}) +``` + +A note on `JS.stripIdentity` in the above example: internally, JSON Schema is represented as a union of Typescript classes. This is handy when inspecting the schema because the name of the schema is shown next to its properties. Because this prevents equality comparison, schemata-ts exposes a method `stripIdentity` to remove the object's class identity. _Caution_: this method stringifies and parses the schema and may throw if the schema itself contains circularity. + +## Pattern Builder + +Schemata-ts comes with powerful regex combinators that are used to construct regex from comprehensible atoms. The `Pattern` schema allows extension of a string schema to a subset of strings defined by a pattern. Decoders and Guards guarantee that a string conforms to the specified pattern, Arbitrary generates strings that follow the pattern, and Json Schema generates string schemata that lists the pattern as a property. + +### US Phone Number Example + +This is a live example validating US Phone numbers found in `src/PatternBuilder.ts` + +```ts +import * as PB from 'schemata-ts/PatternBuilder' +import { pipe } from 'fp-ts/function' + +const digit = PB.characterClass(false, ['0', '9']) + +const areaCode = pipe( + pipe( + PB.char('('), + PB.then(PB.times(3)(digit)), + PB.then(PB.char(')')), + PB.then(PB.maybe(PB.char(' '))), + ), + PB.or(PB.times(3)(digit)), + PB.subgroup, +) + +const prefix = PB.times(3)(digit) + +const lineNumber = PB.times(4)(digit) + +export const usPhoneNumber = pipe( + areaCode, + PB.then(pipe(PB.char('-'), PB.maybe)), + PB.then(prefix), + PB.then(PB.char('-')), + PB.then(lineNumber), +) + +assert.equal(PB.regexFromPattern(usPhoneNumber).test('(123) 456-7890'), true) +assert.equal(PB.regexFromPattern(usPhoneNumber).test('(123)456-7890'), true) +assert.equal(PB.regexFromPattern(usPhoneNumber).test('123-456-7890'), true) +assert.equal(PB.regexFromPattern(usPhoneNumber).test('1234567890'), false) +``` \ No newline at end of file From d39161d85a1152f2ceb8fd1b3c2a97572513d34f Mon Sep 17 00:00:00 2001 From: Jacob Alford Date: Wed, 4 Jan 2023 20:24:09 -0700 Subject: [PATCH 21/21] [#137] tighten intersection signature --- src/base/JsonSchemaBase.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/base/JsonSchemaBase.ts b/src/base/JsonSchemaBase.ts index 60abc7d7..a0dca8b5 100644 --- a/src/base/JsonSchemaBase.ts +++ b/src/base/JsonSchemaBase.ts @@ -428,8 +428,8 @@ export const makeUnionSchema = (right: Const) => - (left: Const): Const => + (right: Const) => + (left: Const): Const => make(new JsonIntersection([left, right])) /**