UNPKG

juniper

Version:

ESM JSON Schema builder for static Typescript inference.

423 lines (422 loc) 13.5 kB
import { reservedWords } from './types.js'; import { mergeAllOf, mergeRef } from './utils.js'; const allOfSym = Symbol('allOf'); const anyOfSym = Symbol('anyOf'); const conditionalsSym = Symbol('conditional'); const examplesSym = Symbol('examples'); const metadataSym = Symbol('metadata'); const notSym = Symbol('not'); const nullableSym = Symbol('nullable'); const oneOfSym = Symbol('oneOf'); const refSym = Symbol('ref'); export class AbstractSchema { #allOf; #anyOf; #conditionals; #default; #deprecated; #description; #examples; #metadata; #nots; #nullable; #oneOf; #readOnly; #ref; #title; #writeOnly; constructor(options = {}){ this.#default = options.default; this.#deprecated = options.deprecated ?? false; this.#description = options.description ?? null; this.#readOnly = options.readOnly ?? false; this.#title = options.title ?? null; this.#writeOnly = options.writeOnly ?? false; this.#allOf = options[allOfSym] ?? []; this.#anyOf = options[anyOfSym] ?? []; this.#conditionals = options[conditionalsSym] ?? []; this.#examples = options[examplesSym] ?? []; this.#metadata = options[metadataSym] ?? {}; this.#nots = options[notSym] ?? []; this.#nullable = options[nullableSym] ?? false; this.#oneOf = options[oneOfSym] ?? []; this.#ref = options[refSym] ?? null; } static getDefaultValues() { return { deprecated: false, description: null, readOnly: false, title: null, writeOnly: false }; } toJSON({ id, openApi30 = false, schema = false } = {}) { const base = this.getChildSchema({ openApi30 }); if (!openApi30) { if (id) { base.$id = id; } if (schema) { base.$schema = 'https://json-schema.org/draft/2020-12/schema'; } } return base; } title(title) { return this.clone({ title }); } description(description) { return this.clone({ description }); } default(val) { return this.clone({ default: val }); } deprecated(deprecated) { return this.clone({ deprecated }); } example(example) { return this.examples([ example ]); } examples(examples) { return this.clone({ [examplesSym]: [ ...this.#examples, ...examples ] }); } readOnly(readOnly) { return this.clone({ readOnly }); } writeOnly(writeOnly) { return this.clone({ writeOnly }); } metadata(...meta) { const metadata = { ...this.#metadata }; if (meta.length === 1) { for (const key of Object.keys(meta[0])){ if (reservedWords.has(key)) { throw new Error(`Illegal use of reserved word: ${key}`); } } Object.assign(metadata, meta[0]); } else { const key = meta[0]; if (reservedWords.has(key)) { throw new Error(`Illegal use of reserved word: ${key}`); } metadata[key] = meta[1]; } return this.clone({ [metadataSym]: metadata }); } ref(path) { return this.clone({ [refSym]: { path, schema: this } }); } cast() { return this; } allOf(schema) { return this.clone({ [allOfSym]: [ ...this.#allOf, schema ] }); } anyOf(schemas) { return this.clone({ [anyOfSym]: [ ...this.#anyOf, schemas ] }); } if(schema, conditionals) { return this.clone({ [conditionalsSym]: [ ...this.#conditionals, { if: schema, then: conditionals.then ?? null, else: conditionals.else ?? null } ] }); } not(schema) { return this.clone({ [notSym]: [ ...this.#nots, schema ] }); } nullable() { return this.clone({ [nullableSym]: true }); } oneOf(schemas) { return this.clone({ [oneOfSym]: [ ...this.#oneOf, schemas ] }); } getChildSchema(params) { return AbstractSchema.#getChildSchema.bind(this.constructor)(this, params); } getCloneParams() { return { default: this.#default, deprecated: this.#deprecated, description: this.#description, readOnly: this.#readOnly, title: this.#title, writeOnly: this.#writeOnly, [allOfSym]: [ ...this.#allOf ], [anyOfSym]: [ ...this.#anyOf ], [conditionalsSym]: [ ...this.#conditionals ], [examplesSym]: [ ...this.#examples ], [metadataSym]: { ...this.#metadata }, [notSym]: [ ...this.#nots ], [nullableSym]: this.#nullable, [oneOfSym]: [ ...this.#oneOf ], [refSym]: this.#ref }; } clone(overrideParams) { return this.constructor.create({ ...this.getCloneParams(), ...overrideParams }); } toSchema(params) { const base = { ...this.#metadata }; const nullable = this.#getNullable(params); const schemaType = this.#getSchemaType(params); if (schemaType) { if (params.composition && !this.#ref) { if (params.composition.type !== schemaType) { if (nullable) { if (params.openApi30) { base.type = schemaType; base.nullable = true; } else { base.type = [ schemaType, 'null' ]; } } else { base.type = schemaType; } } else if (params.composition.nullable && !nullable) { base.type = schemaType; } } else if (nullable) { if (params.openApi30) { base.type = schemaType; base.nullable = true; } else { base.type = [ schemaType, 'null' ]; } } else { base.type = schemaType; } } if (this.#title) { base.title = this.#title; } if (this.#default !== undefined) { base.default = this.#default; } if (this.#description) { base.description = this.#description; } if (this.#examples.length > 0) { if (params.openApi30) { [base.example] = this.#examples; } else { base.examples = this.#examples; } } if (this.#deprecated) { base.deprecated = this.#deprecated; } if (this.#readOnly) { base.readOnly = this.#readOnly; } if (this.#writeOnly) { base.writeOnly = this.#writeOnly; } const compositionParams = { ...params, composition: { type: schemaType, nullable } }; if (this.#allOf.length > 0) { base.allOf = this.#allOf.map((schema)=>schema.getChildSchema(compositionParams)); } const [conditional, ...conditionals] = params.openApi30 ? this.#conditionals.flatMap((condition)=>{ const conditions = []; const ifSchema = condition.if.getChildSchema(compositionParams); if (condition.then) { conditions.push({ anyOf: [ { not: ifSchema }, condition.then.getChildSchema(compositionParams) ] }); } if (condition.else) { conditions.push({ anyOf: [ ifSchema, condition.else.getChildSchema(compositionParams) ] }); } return conditions; }) : this.#conditionals.map((condition)=>{ const mergeSchema = { if: condition.if.getChildSchema(compositionParams) }; if (condition.then) { mergeSchema.then = condition.then.getChildSchema(compositionParams); } if (condition.else) { mergeSchema.else = condition.else.getChildSchema(compositionParams); } return mergeSchema; }); if (conditional?.then) { conditionals.unshift(conditional); } else { Object.assign(base, conditional); } mergeAllOf(base, conditionals); const [not, ...nots] = this.#nots.map((schema)=>({ not: schema.getChildSchema(compositionParams) })); Object.assign(base, not); mergeAllOf(base, nots); const [anyOf, ...anyOfs] = this.#anyOf.map((schemas)=>schemas.map((schema)=>schema.getChildSchema(compositionParams))); if (anyOf) { if (base.anyOf) { anyOfs.unshift(anyOf); } else { base.anyOf = anyOf; } mergeAllOf(base, anyOfs.map((o)=>({ anyOf: o }))); } const [oneOf, ...oneOfs] = this.#oneOf.map((schemas)=>schemas.map((schema)=>schema.getChildSchema(compositionParams))); if (oneOf) { base.oneOf = oneOf; if (oneOfs.length > 0) { mergeAllOf(base, oneOfs.map((o)=>({ oneOf: o }))); } } return base; } static getSchema(schema, { openApi30 }) { return AbstractSchema.#getChildSchema.bind(this.constructor)(schema, { openApi30 }); } static #getChildSchema(schema, params) { const baseSchema = schema.toSchema(params); if (schema.#ref) { const refSchema = schema.#ref.schema.toSchema({ openApi30: params.openApi30 }); return mergeRef({ baseSchema, refSchema, defaultValues: this.getDefaultValues(params), refPath: schema.#ref.path }); } return baseSchema; } #getNullable(params) { if (params.composition?.type && !params.composition.nullable) { return this.#ref ? this.#ref.schema.#getNullable({ openApi30: params.openApi30 }) : false; } return this.#nullable && this.#conditionals.every((predicate)=>{ if (predicate.if.#getNullable(params)) { return predicate.then ? predicate.then.#getNullable(params) : true; } return predicate.else ? predicate.else.#getNullable(params) : true; }) && this.#nots.every((not)=>!not.#getNullable(params)) && this.#allOf.every((allOf)=>allOf.#getNullable(params)) && this.#anyOf.every((anyOf)=>anyOf.some((any)=>any.#getNullable(params))) && this.#oneOf.every((oneOf)=>oneOf.filter((one)=>one.#getNullable(params)).length === 1); } #canOptimizeInteger(params) { return params.composition?.type === 'integer' || this.#conditionals.some((predicate)=>(predicate.if.#getSchemaType(params) === 'integer' || (predicate.then ? predicate.then.#getSchemaType(params) === 'integer' : false)) && (predicate.else ? predicate.else.#getSchemaType(params) === 'integer' : false)) || this.#allOf.some((allOf)=>allOf.#getSchemaType(params) === 'integer') || this.#anyOf.some((anyOf)=>anyOf.length > 0 && anyOf.every((any)=>any.#getSchemaType(params) === 'integer')) || this.#oneOf.some((oneOf)=>oneOf.length > 0 && oneOf.every((one)=>one.#getSchemaType(params) === 'integer')); } #getSchemaType(params) { const { schemaType } = this; if ((schemaType === 'integer' || schemaType === 'number') && this.#canOptimizeInteger(params) && (!this.#ref || this.#ref.schema.#getSchemaType({ openApi30: params.openApi30 }) !== 'number')) { return 'integer'; } return schemaType ?? null; } } //# sourceMappingURL=schema.js.map