@naturalcycles/js-lib
Version:
Standard library for universal (browser + Node.js) javascript
360 lines (359 loc) • 10.4 kB
JavaScript
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 and Zod.
*/
export const j = {
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();
},
isoDate() {
return new JsonSchemaStringBuilder().isoDate();
},
// 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));
}
/**
* @experimental
*/
infer;
}
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').description('UnixTimestamp');
unixTimestamp2000 = () => this.format('unixTimestamp2000').description('UnixTimestamp2000');
unixTimestampMillis = () => this.format('unixTimestampMillis').description('UnixTimestampMillis');
unixTimestampMillis2000 = () => this.format('unixTimestampMillis2000').description('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');
isoDate = () => this.format('date').description('IsoDate'); // todo: make it custom isoDate instead
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,
});
}
}