UNPKG

@naturalcycles/js-lib

Version:

Standard library for universal (browser + Node.js) javascript

356 lines (355 loc) 10.2 kB
import { _uniq } from '../array/array.util.js'; import { _deepCopy } from '../object/object.util.js'; import { _sortObject } from '../object/sortObject.js'; import { JSON_SCHEMA_ORDER } from './jsonSchema.cnst.js'; import { mergeJsonSchemaObjects } from './jsonSchema.util.js'; /** * Fluent (chainable) API to manually create Json Schemas. * Inspired by Joi */ export const jsonSchema = { any() { return new JsonSchemaAnyBuilder({}); }, const(value) { return new JsonSchemaAnyBuilder({ const: value, }); }, null() { return new JsonSchemaAnyBuilder({ type: 'null', }); }, ref($ref) { return new JsonSchemaAnyBuilder({ $ref, }); }, enum(enumValues) { return new JsonSchemaAnyBuilder({ enum: enumValues }); }, boolean() { return new JsonSchemaAnyBuilder({ type: 'boolean', }); }, buffer() { return new JsonSchemaAnyBuilder({ instanceof: 'Buffer', }); }, // number types number() { return new JsonSchemaNumberBuilder(); }, integer() { return new JsonSchemaNumberBuilder().integer(); }, unixTimestamp() { return new JsonSchemaNumberBuilder().unixTimestamp(); }, unixTimestamp2000() { return new JsonSchemaNumberBuilder().unixTimestamp2000(); }, // string types string() { return new JsonSchemaStringBuilder(); }, dateString() { return new JsonSchemaStringBuilder().date(); }, // email: () => new JsonSchemaStringBuilder().email(), // complex types object(props) { return new JsonSchemaObjectBuilder().addProperties(props); }, rootObject(props) { return new JsonSchemaObjectBuilder().addProperties(props).$schemaDraft7(); }, array(itemSchema) { return new JsonSchemaArrayBuilder(itemSchema); }, tuple(items) { return new JsonSchemaTupleBuilder(items); }, oneOf(items) { return new JsonSchemaAnyBuilder({ oneOf: items.map(b => b.build()), }); }, allOf(items) { return new JsonSchemaAnyBuilder({ allOf: items.map(b => b.build()), }); }, }; export class JsonSchemaAnyBuilder { schema; constructor(schema) { this.schema = schema; } /** * Used in ObjectBuilder to access schema.optionalProperty */ getSchema() { return this.schema; } $schema($schema) { Object.assign(this.schema, { $schema }); return this; } $schemaDraft7() { this.$schema('http://json-schema.org/draft-07/schema#'); return this; } $id($id) { Object.assign(this.schema, { $id }); return this; } title(title) { Object.assign(this.schema, { title }); return this; } description(description) { Object.assign(this.schema, { description }); return this; } deprecated(deprecated = true) { Object.assign(this.schema, { deprecated }); return this; } type(type) { Object.assign(this.schema, { type }); return this; } default(v) { Object.assign(this.schema, { default: v }); return this; } oneOf(schemas) { Object.assign(this.schema, { oneOf: schemas }); return this; } allOf(schemas) { Object.assign(this.schema, { allOf: schemas }); return this; } instanceof(of) { this.schema.instanceof = of; return this; } optional(optional = true) { if (optional) { this.schema.optionalField = true; } else { this.schema.optionalField = undefined; } return this; } /** * Produces a "clean schema object" without methods. * Same as if it would be JSON.stringified. */ build() { return _sortObject(JSON.parse(JSON.stringify(this.schema)), JSON_SCHEMA_ORDER); } clone() { return new JsonSchemaAnyBuilder(_deepCopy(this.schema)); } } export class JsonSchemaNumberBuilder extends JsonSchemaAnyBuilder { constructor() { super({ type: 'number', }); } integer() { Object.assign(this.schema, { type: 'integer' }); return this; } multipleOf(multipleOf) { Object.assign(this.schema, { multipleOf }); return this; } min(minimum) { Object.assign(this.schema, { minimum }); return this; } exclusiveMin(exclusiveMinimum) { Object.assign(this.schema, { exclusiveMinimum }); return this; } max(maximum) { Object.assign(this.schema, { maximum }); return this; } exclusiveMax(exclusiveMaximum) { Object.assign(this.schema, { exclusiveMaximum }); return this; } /** * Both ranges are inclusive. */ range(minimum, maximum) { Object.assign(this.schema, { minimum, maximum }); return this; } format(format) { Object.assign(this.schema, { format }); return this; } int32 = () => this.format('int32'); int64 = () => this.format('int64'); float = () => this.format('float'); double = () => this.format('double'); unixTimestamp = () => this.format('unixTimestamp'); unixTimestamp2000 = () => this.format('unixTimestamp2000'); unixTimestampMillis = () => this.format('unixTimestampMillis'); unixTimestampMillis2000 = () => this.format('unixTimestampMillis2000'); utcOffset = () => this.format('utcOffset'); utcOffsetHours = () => this.format('utcOffsetHours'); } export class JsonSchemaStringBuilder extends JsonSchemaAnyBuilder { constructor() { super({ type: 'string', }); } pattern(pattern) { Object.assign(this.schema, { pattern }); return this; } min(minLength) { Object.assign(this.schema, { minLength }); return this; } max(maxLength) { Object.assign(this.schema, { maxLength }); return this; } length(minLength, maxLength) { Object.assign(this.schema, { minLength, maxLength }); return this; } format(format) { Object.assign(this.schema, { format }); return this; } email = () => this.format('email'); date = () => this.format('date'); url = () => this.format('url'); ipv4 = () => this.format('ipv4'); ipv6 = () => this.format('ipv6'); password = () => this.format('password'); id = () => this.format('id'); slug = () => this.format('slug'); semVer = () => this.format('semVer'); languageTag = () => this.format('languageTag'); countryCode = () => this.format('countryCode'); currency = () => this.format('currency'); trim = (trim = true) => this.transformModify('trim', trim); toLowerCase = (toLowerCase = true) => this.transformModify('toLowerCase', toLowerCase); toUpperCase = (toUpperCase = true) => this.transformModify('toUpperCase', toUpperCase); transformModify(t, add) { if (add) { this.schema.transform = _uniq([...(this.schema.transform || []), t]); } else { this.schema.transform = this.schema.transform?.filter(s => s !== t); } return this; } } export class JsonSchemaObjectBuilder extends JsonSchemaAnyBuilder { constructor() { super({ type: 'object', properties: {}, required: [], additionalProperties: false, }); } addProperties(props) { Object.entries(props).forEach(([k, builder]) => { const schema = builder.build(); if (!schema.optionalField) { this.schema.required.push(k); } else { schema.optionalField = undefined; } this.schema.properties[k] = schema; }); this.required(this.schema.required); // ensure it's sorted and _uniq return this; } /** * Ensures `required` is always sorted and _uniq */ required(required) { Object.assign(this.schema, { required }); this.schema.required = _uniq(required).sort(); return this; } addRequired(required) { return this.required([...this.schema.required, ...required]); } minProps(minProperties) { Object.assign(this.schema, { minProperties }); return this; } maxProps(maxProperties) { Object.assign(this.schema, { maxProperties }); return this; } additionalProps(additionalProperties) { Object.assign(this.schema, { additionalProperties }); return this; } baseDBEntity() { Object.assign(this.schema.properties, { id: { type: 'string' }, created: { type: 'number', format: 'unixTimestamp2000' }, updated: { type: 'number', format: 'unixTimestamp2000' }, }); return this.addRequired(['id', 'created', 'updated']); } extend(s2) { const builder = new JsonSchemaObjectBuilder(); Object.assign(builder.schema, _deepCopy(this.schema)); mergeJsonSchemaObjects(builder.schema, s2.schema); return builder; } } export class JsonSchemaArrayBuilder extends JsonSchemaAnyBuilder { constructor(itemsSchema) { super({ type: 'array', items: itemsSchema.build(), }); } min(minItems) { Object.assign(this.schema, { minItems }); return this; } max(maxItems) { Object.assign(this.schema, { maxItems }); return this; } unique(uniqueItems) { Object.assign(this.schema, { uniqueItems }); return this; } } export class JsonSchemaTupleBuilder extends JsonSchemaAnyBuilder { constructor(items) { super({ type: 'array', items: items.map(b => b.build()), minItems: items.length, maxItems: items.length, }); } }