Comparing version 0.1.0 to 0.2.0
import { Exception } from 'typesafe-exception'; | ||
import { Schema } from './schema'; | ||
import { Schema, SchemaLike } from './schema'; | ||
import { SchemaStore } from './store'; | ||
export declare class ValidationError extends Exception<{ | ||
@@ -9,6 +10,2 @@ errors: DecodeError[]; | ||
} | ||
export interface DecodeResult<T> { | ||
value: T; | ||
errors: DecodeError[]; | ||
} | ||
export interface DecodeError { | ||
@@ -18,2 +15,12 @@ path: string[]; | ||
} | ||
export declare function decode<T>(schema: Schema<T>, value: unknown, throwOnInvalid?: boolean): T; | ||
export interface DecodeOptions { | ||
throw?: boolean; | ||
refs?: SchemaLike[]; | ||
} | ||
export declare function decode<T>(schema: Schema<T>, value: unknown, options?: DecodeOptions): T; | ||
export declare class Decoder<T> { | ||
readonly schema: Schema<T>; | ||
readonly store: SchemaStore; | ||
constructor(schema: Schema<T>, store?: SchemaStore); | ||
decode(value: unknown, options?: DecodeOptions): T; | ||
} |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.decode = exports.ValidationError = void 0; | ||
exports.Decoder = exports.decode = exports.ValidationError = void 0; | ||
const typesafe_exception_1 = require("typesafe-exception"); | ||
const coerce_1 = require("./coerce"); | ||
const defaults_1 = require("./defaults"); | ||
const store_1 = require("./store"); | ||
const util_1 = require("./util"); | ||
@@ -15,118 +16,150 @@ class ValidationError extends typesafe_exception_1.Exception { | ||
exports.ValidationError = ValidationError; | ||
function decode(schema, value, throwOnInvalid = false) { | ||
const errors = []; | ||
const res = decodeAny(schema, value, [], errors); | ||
if (throwOnInvalid && errors.length > 0) { | ||
throw new ValidationError(errors); | ||
} | ||
return res; | ||
function decode(schema, value, options = {}) { | ||
return new Decoder(schema).decode(value, options); | ||
} | ||
exports.decode = decode; | ||
function decodeAny(schema, value, path, errors) { | ||
const untypedSchema = schema; | ||
// Null/Undefined | ||
if (value == null) { | ||
if (untypedSchema.optional) { | ||
return undefined; | ||
} | ||
if (untypedSchema.nullable) { | ||
return null; | ||
} | ||
errors.push({ path, message: 'must not be null' }); | ||
value = defaultValue(schema); | ||
class Decoder { | ||
constructor(schema, store) { | ||
this.schema = schema; | ||
this.store = store !== null && store !== void 0 ? store : new store_1.SchemaStore().add(schema); | ||
} | ||
// Any Schema | ||
if (schema.type === 'any') { | ||
return value; | ||
decode(value, options = {}) { | ||
return new DecodeJob(this, value, options).decode(); | ||
} | ||
// Coercion | ||
if (schema.type !== util_1.getType(value)) { | ||
const coercedValue = coerce_1.coerce(schema.type, value); | ||
if (coercedValue === undefined) { | ||
errors.push({ path, message: `must be ${schema.type}` }); | ||
return defaultValue(schema); | ||
} | ||
exports.Decoder = Decoder; | ||
class DecodeJob { | ||
constructor(decoder, value, options) { | ||
this.decoder = decoder; | ||
this.value = value; | ||
this.options = options; | ||
this.errors = []; | ||
} | ||
decode() { | ||
const res = this.decodeAny(this.decoder.schema, this.value, []); | ||
if (this.options.throw && this.errors.length > 0) { | ||
throw new ValidationError(this.errors); | ||
} | ||
value = coercedValue; | ||
return res; | ||
} | ||
// Per-type | ||
switch (schema.type) { | ||
case 'boolean': | ||
decodeAny(schema, value, path) { | ||
const untypedSchema = schema; | ||
// Null/Undefined | ||
if (value == null) { | ||
if (untypedSchema.optional) { | ||
return undefined; | ||
} | ||
if (untypedSchema.nullable) { | ||
return null; | ||
} | ||
this.errors.push({ path, message: 'must not be null' }); | ||
value = this.defaultValue(schema); | ||
} | ||
// Any Schema | ||
if (schema.type === 'any') { | ||
return value; | ||
case 'number': | ||
case 'integer': | ||
return decodeNumber(untypedSchema, value, path, errors); | ||
case 'string': | ||
return decodeString(untypedSchema, value, path, errors); | ||
case 'object': | ||
return decodeObject(untypedSchema, value, path, errors); | ||
case 'array': | ||
return decodeArray(untypedSchema, value, path, errors); | ||
default: | ||
errors.push({ path, message: 'must be a valid data type' }); | ||
return defaultValue(schema); | ||
} | ||
// Ref Schema | ||
if (schema.type === 'ref') { | ||
return this.decodeRef(schema.schemaId, value, path); | ||
} | ||
// Coercion | ||
if (schema.type !== util_1.getType(value)) { | ||
const coercedValue = coerce_1.coerce(schema.type, value); | ||
if (coercedValue === undefined) { | ||
this.errors.push({ path, message: `must be ${schema.type}` }); | ||
return this.defaultValue(schema); | ||
} | ||
value = coercedValue; | ||
} | ||
// Per-type | ||
switch (schema.type) { | ||
case 'boolean': | ||
return value; | ||
case 'number': | ||
case 'integer': | ||
return this.decodeNumber(untypedSchema, value, path); | ||
case 'string': | ||
return this.decodeString(untypedSchema, value, path); | ||
case 'object': | ||
return this.decodeObject(untypedSchema, value, path); | ||
case 'array': | ||
return this.decodeArray(untypedSchema, value, path); | ||
default: | ||
this.errors.push({ path, message: 'must be a valid data type' }); | ||
return this.defaultValue(schema); | ||
} | ||
} | ||
} | ||
function decodeNumber(schema, value, path, errors) { | ||
const num = value; | ||
let valid = true; | ||
if (schema.minimum != null && num < schema.minimum) { | ||
errors.push({ path, message: `must be greater than or equal to ${schema.minimum}` }); | ||
valid = false; | ||
decodeNumber(schema, value, path) { | ||
const num = value; | ||
let valid = true; | ||
if (schema.minimum != null && num < schema.minimum) { | ||
this.errors.push({ path, message: `must be greater than or equal to ${schema.minimum}` }); | ||
valid = false; | ||
} | ||
if (schema.maximum != null && num > schema.maximum) { | ||
this.errors.push({ path, message: `must be less than or equal to ${schema.maximum}` }); | ||
valid = false; | ||
} | ||
return valid ? num : this.defaultValue(schema); | ||
} | ||
if (schema.maximum != null && num > schema.maximum) { | ||
errors.push({ path, message: `must be less than or equal to ${schema.maximum}` }); | ||
valid = false; | ||
decodeString(schema, value, path) { | ||
var _a; | ||
const str = value; | ||
let valid = true; | ||
if (schema.enum != null && !schema.enum.includes(str)) { | ||
this.errors.push({ path, message: `must be an allowed value` }); | ||
valid = false; | ||
} | ||
if (schema.regex != null && new RegExp(schema.regex, (_a = schema.regexFlags) !== null && _a !== void 0 ? _a : '').test(str)) { | ||
this.errors.push({ path, message: `must be in allowed format` }); | ||
valid = false; | ||
} | ||
return valid ? str : this.defaultValue(schema); | ||
} | ||
return valid ? num : defaultValue(schema); | ||
} | ||
function decodeString(schema, value, path, errors) { | ||
var _a; | ||
const str = value; | ||
let valid = true; | ||
if (schema.enum != null && !schema.enum.includes(str)) { | ||
errors.push({ path, message: `must be an allowed value` }); | ||
valid = false; | ||
decodeObject(schema, value, path) { | ||
const propKeys = new Set(); | ||
const result = {}; | ||
const original = value; | ||
for (const [key, propSchema] of Object.entries(schema.properties)) { | ||
const value = original[key]; | ||
const decoded = this.decodeAny(propSchema, value, path.concat([key])); | ||
if (decoded !== undefined) { | ||
result[key] = decoded; | ||
} | ||
propKeys.add(key); | ||
} | ||
if (schema.additionalProperties) { | ||
for (const [key, value] of original) { | ||
if (propKeys.has(key)) { | ||
continue; | ||
} | ||
result[key] = this.decodeAny(schema.additionalProperties, value, path.concat([key])); | ||
} | ||
} | ||
return result; | ||
} | ||
if (schema.regex != null && new RegExp(schema.regex, (_a = schema.regexFlags) !== null && _a !== void 0 ? _a : '').test(str)) { | ||
errors.push({ path, message: `must be in allowed format` }); | ||
valid = false; | ||
} | ||
return valid ? str : defaultValue(schema); | ||
} | ||
function decodeObject(schema, value, path, errors) { | ||
const propKeys = new Set(); | ||
const result = {}; | ||
const original = value; | ||
for (const [key, propSchema] of Object.entries(schema.properties)) { | ||
const value = original[key]; | ||
const decoded = decodeAny(propSchema, value, path.concat([key]), errors); | ||
if (decoded !== undefined) { | ||
result[key] = decoded; | ||
decodeArray(schema, value, path) { | ||
const result = []; | ||
const original = value; | ||
for (const value of original) { | ||
const item = this.decodeAny(schema.items, value, path.concat(['*'])); | ||
result.push(item); | ||
} | ||
propKeys.add(key); | ||
return result; | ||
} | ||
if (schema.additionalProperties) { | ||
for (const [key, value] of original) { | ||
if (propKeys.has(key)) { | ||
continue; | ||
} | ||
result[key] = decodeAny(schema.additionalProperties, value, path.concat([key]), errors); | ||
decodeRef(schemaId, value, path) { | ||
const refSchema = this.decoder.store.get(schemaId); | ||
if (!refSchema) { | ||
this.errors.push({ path, message: `unknown type ${schemaId}` }); | ||
return undefined; | ||
} | ||
return this.decodeAny(refSchema, value, path); | ||
} | ||
return result; | ||
} | ||
function decodeArray(schema, value, path, errors) { | ||
const result = []; | ||
const original = value; | ||
for (const value of original) { | ||
const item = decodeAny(schema.items, value, path.concat(['*']), errors); | ||
result.push(item); | ||
defaultValue(schema) { | ||
var _a; | ||
return (_a = schema.default) !== null && _a !== void 0 ? _a : (schema.optional ? undefined : | ||
schema.nullable ? null : | ||
defaults_1.defaults[schema.type]); | ||
} | ||
return result; | ||
} | ||
function defaultValue(schema) { | ||
var _a; | ||
return (_a = schema.default) !== null && _a !== void 0 ? _a : (schema.optional ? undefined : | ||
schema.nullable ? null : | ||
defaults_1.defaults[schema.type]); | ||
} |
@@ -6,2 +6,3 @@ "use strict"; | ||
any: null, | ||
ref: null, | ||
array: [], | ||
@@ -8,0 +9,0 @@ boolean: false, |
@@ -1,2 +0,2 @@ | ||
export declare type Schema<T> = (StrictTypeSchema<T> | AnySchema) & (undefined extends T ? { | ||
export declare type Schema<T> = (StrictTypeSchema<T> | AnySchema | RefSchema) & (undefined extends T ? { | ||
optional: true; | ||
@@ -7,6 +7,6 @@ } : {}) & (null extends T ? { | ||
export declare type SchemaType = Schema<any>['type']; | ||
export declare type SchemaLike = RefSchema | { | ||
type: SchemaType; | ||
} & BaseSchema; | ||
export declare type StrictTypeSchema<T> = (T extends boolean ? BooleanSchema : T extends number ? NumberSchema : T extends string ? StringSchema : T extends Array<infer P> ? ArraySchema<P> : T extends object ? ObjectSchema<T> : never); | ||
export declare type ReferenceSchema = { | ||
reference: string; | ||
}; | ||
export declare type BaseSchema = { | ||
@@ -16,2 +16,3 @@ id?: string; | ||
description?: string; | ||
metadata?: any; | ||
}; | ||
@@ -22,2 +23,6 @@ export declare type AnySchema = { | ||
} & BaseSchema; | ||
export declare type RefSchema = { | ||
type: 'ref'; | ||
schemaId: string; | ||
}; | ||
export declare type BooleanSchema = { | ||
@@ -24,0 +29,0 @@ type: 'boolean'; |
{ | ||
"name": "airtight", | ||
"version": "0.1.0", | ||
"version": "0.2.0", | ||
"description": "Tight subset of JSON schema", | ||
@@ -38,3 +38,3 @@ "main": "out/main/index.js", | ||
"@types/node": "^16.3.1", | ||
"@ubio/eslint-config": "^1.1.6", | ||
"@ubio/eslint-config": "^1.2.1", | ||
"eslint": "^7.30.0", | ||
@@ -41,0 +41,0 @@ "mocha": "^9.0.2", |
14590
16
408