juniper
Version:
ESM JSON Schema builder for static Typescript inference.
423 lines (422 loc) • 13.5 kB
JavaScript
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