@superhero/oas
Version:
OpenAPI Specification (OAS) router
1,393 lines (1,240 loc) • 37.7 kB
JavaScript
import deepmerge from '@superhero/deep/merge'
import deepclone from '@superhero/deep/clone'
import net from 'node:net'
import ComponentsAbstraction from './abstraction.js'
/**
* @memberof Oas.Components
*/
export default class Schemas extends ComponentsAbstraction
{
validComponentAttributes =
[
'title', 'description', 'type', 'default', 'deprecated', 'example', 'examples',
'nullable', 'readOnly', 'writeOnly', 'externalDocs', 'minLength', 'maxLength',
'pattern', 'format', 'minimum', 'maximum', 'exclusiveMinimum', 'exclusiveMaximum',
'multipleOf', 'items', 'minItems', 'maxItems', 'uniqueItems', 'prefixItems',
'properties', 'required', 'additionalProperties', 'minProperties', 'maxProperties',
'propertyNames', 'allOf', 'oneOf', 'anyOf', 'not', 'const', 'enum',
'if', 'then', 'else', '$ref', true, false
]
validateNumberFormat = new Map
validateStringFormat = new Map
constructor(specification)
{
super(specification)
this.validateNumberFormat.set('float', this.validateNumberFormatFloat)
this.validateNumberFormat.set('double', this.validateNumberFormatDouble)
this.validateNumberFormat.set('int32', this.validateNumberFormatInt32)
this.validateNumberFormat.set('int64', this.validateNumberFormatInt64)
this.validateStringFormat.set('date', this.validateStringFormatDate)
this.validateStringFormat.set('time', this.validateStringFormatTime)
this.validateStringFormat.set('datetime', this.validateStringFormatDatetime)
this.validateStringFormat.set('date-time', this.validateStringFormatDatetime)
this.validateStringFormat.set('base64', this.validateStringFormatBase64)
this.validateStringFormat.set('byte', this.validateStringFormatBase64)
this.validateStringFormat.set('email', this.validateStringFormatEmail)
this.validateStringFormat.set('ipv4', this.validateStringFormatIpv4)
this.validateStringFormat.set('ipv6', this.validateStringFormatIpv6)
this.validateStringFormat.set('url', this.validateStringFormatUrl)
this.validateStringFormat.set('uuid', this.validateStringFormatUuid)
}
denormalize(component)
{
const denormalized = super.denormalize(component)
if('boolean' === typeof denormalized)
{
return denormalized
}
if('properties' in component)
{
for(const property in denormalized.properties)
{
if('schema' in denormalized.properties[property])
{
denormalized.properties[property] = this.denormalize(denormalized.properties[property])
}
}
}
const denormalize = this.denormalize.bind(this)
for(const key of [ 'prefixItems', 'allOf', 'oneOf', 'anyOf' ])
{
if(key in denormalized)
{
if(Array.isArray(denormalized.items))
{
denormalized[key] = denormalized[key].map(denormalize)
}
else
{
const error = new Error(`Invalid "${key}" schema component`)
error.code = 'E_OAS_INVALID_SCHEMAS_SPECIFICATION'
error.cause = `The "${key}" schema component must be an array of schemas`
throw error
}
}
}
if('items' in denormalized)
{
if(Array.isArray(denormalized.items))
{
denormalized.items = denormalized.items.map(denormalize)
}
else
{
denormalized.items = this.denormalize(denormalized.items)
}
}
for(const key of [ 'additionalProperties', 'if', 'then', 'else', 'not' ])
{
if(key in denormalized)
{
denormalized[key] = this.denormalize(denormalized[key])
}
}
return denormalized
}
conform(component, instance, isWriting)
{
try
{
if(true === component)
{
return instance
}
if(false === component)
{
const error = new Error(`Invalid schema component`)
error.code = 'E_OAS_INVALID_SCHEMAS_INSTANCE'
error.cause = 'The schema component has declared that the instance must not be used'
throw error
}
if(component.$ref)
{
return this.conformRef(component.$ref, instance, isWriting)
}
if('readOnly' in component
&& true === !!isWriting)
{
return
}
if('writeOnly' in component
&& false === !!isWriting)
{
return
}
instance = this.conformDefault(component, instance)
instance = this.conformIfThenElse(component, instance, isWriting)
instance = this.conformAllOf(component, instance, isWriting)
instance = this.conformAnyOf(component, instance, isWriting)
instance = this.conformOneOf(component, instance, isWriting)
const instanceType = Object.prototype.toString.call(instance)
if('[object Null]' !== instanceType
&& '[object Undefined]' !== instanceType)
{
switch(component.type)
{
case 'array':
{
instance = this.conformTypeArray(component, instance, instanceType, isWriting)
break
}
case 'boolean':
{
instance = this.conformTypeBoolean(instance, instanceType)
break
}
case 'integer':
case 'number':
{
instance = this.conformTypeNumber(component, instance, instanceType)
break
}
case 'object':
{
instance = this.conformTypeObject(component, instance, instanceType, isWriting)
break
}
case 'string':
{
instance = this.conformTypeString(component, instance, instanceType)
break
}
case 'null':
{
instance = this.conformTypeNull(component, instance, instanceType)
break
}
case undefined:
{
break
}
default:
{
const error = new Error(`Invalid component type ${component.type}`)
error.code = 'E_OAS_INVALID_SCHEMAS_SPECIFICATION'
throw error
}
}
}
if(null === instance
&& true !== component.nullable
&& 'null' !== component.type)
{
const error = new Error(`Invalid instance`)
error.code = 'E_OAS_INVALID_SCHEMAS_INSTANCE'
error.cause = 'The instance must NOT be null'
throw error
}
this.validateNot(component, instance, isWriting)
this.validateConst(component, instance)
this.validateEnum(component, instance)
return instance
}
catch(reason)
{
const error = new Error(`Invalid schema`)
error.code = 'E_OAS_INVALID_SCHEMA'
error.cause = reason
throw error
}
}
conformDefault(component, instance)
{
if('default' in component)
{
if(undefined === instance)
{
instance = deepclone(component.default)
}
}
return instance
}
conformIfThenElse(component, instance, isWriting)
{
if('if' in component)
{
if('then' in component
|| 'else' in component)
{
try
{
this.conform(component.if, instance, isWriting)
if('then' in component)
{
instance = this.conform(component.then, instance, isWriting)
}
}
catch(error)
{
if('else' in component)
{
instance = this.conform(component.else, instance, isWriting)
}
}
}
}
return instance
}
conformAllOf(component, instance, isWriting)
{
if('allOf' in component)
{
const instances = []
for(const allOfComponent of component.allOf)
{
instances.push(this.conform(allOfComponent, instance, isWriting))
}
instance = deepmerge(...instances)
}
return instance
}
conformAnyOf(component, instance, isWriting)
{
if('anyOf' in component)
{
let errors = 0
for(const anyOfComponent of component.anyOf)
{
try
{
instance = this.conform(anyOfComponent, instance, isWriting)
break
}
catch(error)
{
errors++
}
}
if(errors === component.anyOf.length)
{
const error = new Error(`Invalid instance`)
error.code = 'E_OAS_INVALID_SCHEMAS_INSTANCE'
error.cause = 'The instance must match at least one of the "anyOf" specifiations'
throw error
}
}
return instance
}
conformOneOf(component, instance, isWriting)
{
if('oneOf' in component)
{
let errors = 0
for(const oneOfComponent of component.oneOf)
{
try
{
instance = this.conform(oneOfComponent, instance, isWriting)
}
catch(error)
{
errors++
}
}
if(errors !== component.oneOf.length - 1)
{
const error = new Error(`Invalid instance`)
error.code = 'E_OAS_INVALID_SCHEMAS_INSTANCE'
error.cause = 'The instance must match exactly one of the "oneOf" specifiations'
throw error
}
}
return instance
}
conformTypeArray(component, instance, instanceType, isWriting)
{
this.validateTypeArrayInstanceType(instanceType)
instance = this.conformTypeArrayItems(component, instance, isWriting)
this.validateTypeArrayMinItems(component, instance)
this.validateTypeArrayMaxItems(component, instance)
instance = this.validateTypeArrayUniqueItems(component, instance)
return instance
}
// Attempting to support both 3.0 and 3.1 OAS tuple validation
conformTypeArrayItems(component, instance, isWriting)
{
if(false === !!component.items)
{
const error = new Error(`Invalid "array" schema component`)
error.code = 'E_OAS_INVALID_SCHEMAS_SPECIFICATION'
error.cause = 'The schema type: "array" must have an "items" attribute'
throw error
}
if(Array.isArray(component.items) && component.items.length === 0)
{
const error = new Error(`Invalid "array" schema component`)
error.code = 'E_OAS_INVALID_SCHEMAS_SPECIFICATION'
error.cause = 'The schema type: "array" must have an "items" attribute with at least one item'
throw error
}
const
items = [ component.items ].flat(),
output = [],
conform = (itemComponent, itemInstance) =>
{
const conformed = this.conform(itemComponent, itemInstance, isWriting)
if(conformed !== undefined
|| itemInstance === undefined)
{
output.push(conformed)
}
}
let i = 0
if(component.prefixItems)
{
const prefixItems = [ component.prefixItems ].flat()
for(; i < instance.length && i < prefixItems.length; i++)
{
conform(prefixItems[i], instance[i])
}
}
for(let n = 0; i < instance.length; i++, n++)
{
conform(items[n % items.length], instance[i])
}
return output
}
conformTypeBoolean(instance, instanceType)
{
if('[object Boolean]' !== instanceType)
{
switch(String(instance).toLowerCase().trim())
{
case 'true':
case 'on':
case 'yes':
case '1':
{
instance = true
break
}
case 'false':
case 'off':
case 'no':
case '0':
{
instance = false
break
}
default:
{
const error = new Error(`Invalid boolean instance type ${instanceType}`)
error.code = 'E_OAS_INVALID_SCHEMAS_INSTANCE'
throw error
}
}
}
return instance
}
conformTypeNumber(component, instance, instanceType)
{
instance = this.transformTypeNumber(instance, instanceType)
this.validateTypeNumberInstanceType(component, instance)
this.validateTypeNumberInteger(component, instance)
this.validateTypeNumberMinimum(component, instance)
this.validateTypeNumberMaximum(component, instance)
this.validateTypeNumberMultipleOf(component, instance)
this.validateTypeNumberFormat(component, instance)
return instance
}
transformTypeNumber(instance, instanceType)
{
switch(instanceType)
{
case '[object String]' : return Number(instance)
case '[object Date]' : return instance.getTime()
default : return instance
}
}
conformTypeObject(component, instance, instanceType, isWriting)
{
this.validateTypeObjectInstanceType(instanceType)
instance = this.conformTypeObjectProperties(component, instance, isWriting)
this.validateTypeObjectRequired(component, instance)
this.validateTypeObjectAdditionalProperties(component, instance)
this.validateTypeObjectMinProperties(component, instance)
this.validateTypeObjectMaxProperties(component, instance)
this.validateTypeObjectPropertyNames(component, instance)
return instance
}
conformTypeObjectProperties(component, instance, isWriting)
{
const output = deepclone(instance)
if('properties' in component)
{
const errors = []
for(const name in component.properties)
{
try
{
output[name] = this.conform(component.properties[name], output[name], isWriting)
if(undefined === output[name])
{
delete output[name]
}
}
catch(reason)
{
const error = new Error(`Invalid property "${name}"`)
error.code = 'E_OAS_INVALID_SCHEMAS_PROPERTY'
error.cause = reason
errors.push(error)
}
}
if(errors.length)
{
const error = new Error(`Invalid properties`)
error.code = 'E_OAS_INVALID_SCHEMAS_INSTANCE'
error.cause = errors
throw error
}
}
return output
}
conformTypeString(component, instance, instanceType)
{
this.validateTypeStringInstanceType(instanceType)
this.validateTypeStringMinLength(component, instance)
this.validateTypeStringMaxLength(component, instance)
this.validateTypeStringPattern(component, instance)
this.validateTypeStringFormat(component, instance)
return instance
}
conformTypeNull(component, instance, instanceType)
{
if(null !== instance)
{
switch(String(instance).toLowerCase().trim())
{
case 'null':
case '':
{
instance = null
break
}
default:
{
const error = new Error(`Invalid null instance type ${instanceType}`)
error.code = 'E_OAS_INVALID_SCHEMAS_INSTANCE'
error.cause = 'The instance value must be null'
throw error
}
}
}
return instance
}
validateRefPointer(pointer)
{
super.validateRefPointer(pointer)
if(false === pointer.startsWith('/components/schemas/'))
{
const error = new Error(`The ref pointer "#${pointer}" must point to a schema component`)
error.code = 'E_OAS_INVALID_SCHEMAS_SPECIFICATION'
throw error
}
}
validateComponent(component)
{
super.validateComponent(component)
const invalidAttributes = []
switch(component.type)
{
case 'array':
{
this.validateComponentArray(component, invalidAttributes)
break
}
case 'boolean':
{
this.validateComponentBoolean(component, invalidAttributes)
break
}
case 'integer':
case 'number':
{
this.validateComponentNumber(component, invalidAttributes)
break
}
case 'string':
{
this.validateComponentString(component, invalidAttributes)
break
}
case 'object':
{
this.validateComponentObject(component, invalidAttributes)
break
}
case 'null':
{
this.validateComponentNull(component, invalidAttributes)
break
}
case undefined:
{
break
}
default:
{
const error = new Error(`Invalid component type ${component.type}`)
error.code = 'E_OAS_INVALID_SCHEMAS_SPECIFICATION'
throw error
}
}
for(const invalidAttribute of invalidAttributes)
{
if(invalidAttribute in component)
{
const error = new Error(`Invalid attribute "${invalidAttribute}" for type ${component.type}`)
error.code = 'E_OAS_INVALID_SCHEMAS_SPECIFICATION'
throw error
}
}
}
validateComponentArray(component, invalidAttributes)
{
if('enum' in component)
{
for(const item of component.enum)
{
if(null === item && component.nullable)
{
continue
}
if(false === Array.isArray(item))
{
const error = new Error('Invalid component enum')
error.code = 'E_OAS_INVALID_SCHEMAS_SPECIFICATION'
error.cause = 'The component enum must be an array of arrays'
throw error
}
}
}
if(false === !!component.prefixItems
&& false === !!component.items)
{
const error = new Error('Invalid component array')
error.code = 'E_OAS_INVALID_SCHEMAS_SPECIFICATION'
error.cause = 'The component array must have an "items" attribute'
throw error
}
invalidAttributes.push(
'pattern', 'minimum', 'maximum', 'minLength', 'maxLength', 'format', 'properties',
'minProperties', 'maxProperties', 'additionalProperties', 'required',
'propertyNames', 'multipleOf')
}
validateComponentBoolean(component, invalidAttributes)
{
if('enum' in component)
{
for(const item of component.enum)
{
if('boolean' !== typeof item)
{
if(null === item && component.nullable)
{
continue
}
const error = new Error('Invalid component enum')
error.code = 'E_OAS_INVALID_SCHEMAS_SPECIFICATION'
error.cause = 'The component enum must be an array of booleans'
throw error
}
}
}
invalidAttributes.push(
'pattern', 'minimum', 'maximum', 'minLength', 'maxLength', 'format', 'properties',
'minProperties', 'items', 'minItems', 'maxProperties', 'additionalProperties',
'maxItems', 'required', 'uniqueItems', 'propertyNames', 'multipleOf')
}
validateComponentNumber(component, invalidAttributes)
{
if('enum' in component)
{
for(const item of component.enum)
{
if('number' !== typeof item)
{
if(null === item && component.nullable)
{
continue
}
const error = new Error('Invalid component enum')
error.code = 'E_OAS_INVALID_SCHEMAS_SPECIFICATION'
error.cause = 'The component enum must be an array of numbers'
throw error
}
}
}
invalidAttributes.push(
'items', 'properties', 'minItems', 'maxItems', 'minLength', 'maxLength',
'pattern', 'minProperties', 'maxProperties', 'additionalProperties', 'required',
'uniqueItems', 'propertyNames')
}
validateComponentString(component, invalidAttributes)
{
if('enum' in component)
{
for(const item of component.enum)
{
if('string' !== typeof item)
{
if(null === item && component.nullable)
{
continue
}
const error = new Error('Invalid component enum')
error.code = 'E_OAS_INVALID_SCHEMAS_SPECIFICATION'
error.cause = 'The component enum must be an array of strings'
throw error
}
}
}
invalidAttributes.push(
'items', 'minimum', 'maximum', 'properties', 'minItems', 'maxItems', 'minProperties',
'maxProperties', 'propertyNames', 'additionalProperties', 'required', 'uniqueItems',
'multipleOf')
}
validateComponentObject(component, invalidAttributes)
{
if('enum' in component)
{
for(const item of component.enum)
{
if('[object Object]' !== Object.prototype.toString.call(item))
{
if(null === item && component.nullable)
{
continue
}
const error = new Error('Invalid component enum')
error.code = 'E_OAS_INVALID_SCHEMAS_SPECIFICATION'
error.cause = 'The component enum must be an array of objects'
throw error
}
}
}
invalidAttributes.push(
'pattern', 'minimum', 'maximum', 'minLength', 'maxLength', 'items', 'minItems',
'maxItems', 'uniqueItems', 'multipleOf')
}
validateComponentNull(component, invalidAttributes)
{
invalidAttributes.push(
'pattern', 'minimum', 'maximum', 'minLength', 'maxLength', 'items', 'properties',
'minItems', 'maxItems', 'minProperties', 'maxProperties', 'propertyNames',
'additionalProperties', 'required', 'uniqueItems', 'multipleOf', 'format')
}
validateTypeArrayInstanceType(instanceType)
{
if('[object Array]' !== instanceType)
{
const error = new Error(`Invalid array instance type ${instanceType}`)
error.code = 'E_OAS_INVALID_SCHEMAS_INSTANCE'
throw error
}
}
validateTypeArrayMinItems(component, instance)
{
if('minItems' in component)
{
if(instance.length < component.minItems)
{
const error = new Error(`Invalid amount of array items ${instance.length}`)
error.code = 'E_OAS_INVALID_SCHEMAS_INSTANCE'
error.cause = `The array must have at least ${component.minItems} items`
throw error
}
}
}
validateTypeArrayMaxItems(component, instance)
{
if('maxItems' in component)
{
if(instance.length > component.maxItems)
{
const error = new Error(`Invalid amount of array items ${instance.length}`)
error.code = 'E_OAS_INVALID_SCHEMAS_INSTANCE'
error.cause = `The array can not have more than ${component.maxItems} items`
throw error
}
}
}
validateTypeArrayUniqueItems(component, instance)
{
if(component.uniqueItems)
{
const uniqueItems = []
next : for(let i = 0; i < instance.length; i++)
{
for(let n = i + 1; n < instance.length; n++)
{
try
{
this.deepEqual(instance[i], instance[n])
continue next // not unique, so continue to next
}
catch(error)
{
continue
}
}
// If we reach this point, then
// the item is unique.
uniqueItems.push(instance[i])
}
instance = uniqueItems
}
return instance
}
validateTypeNumberInstanceType(component, instance)
{
if(false === Number.isFinite(instance))
{
const error = new Error(`Invalid ${component.type} instance`)
error.code = 'E_OAS_INVALID_SCHEMAS_INSTANCE'
error.cause = `The instance: "${instance}" is not a valid ${component.type}`
throw error
}
}
validateTypeNumberInteger(component, instance)
{
if('integer' === component.type
&& false === Number.isInteger(instance))
{
const error = new Error(`Invalid ${component.type} insance ${instance}`)
error.code = 'E_OAS_INVALID_SCHEMAS_INSTANCE'
error.cause = `The ${component.type} is not an integer`
throw error
}
}
validateTypeNumberMinimum(component, instance)
{
if('minimum' in component)
{
if(component.exclusiveMinimum)
{
if(instance <= component.minimum)
{
const error = new Error(`Invalid ${component.type} instance ${instance}`)
error.code = 'E_OAS_INVALID_SCHEMAS_INSTANCE'
error.cause = `The ${component.type} must be greater than ${component.minimum}`
throw error
}
}
else
{
if(instance < component.minimum)
{
const error = new Error(`Invalid ${component.type} instance ${instance}`)
error.code = 'E_OAS_INVALID_SCHEMAS_INSTANCE'
error.cause = `The ${component.type} must be greater than or equal to ${component.minimum}`
throw error
}
}
}
}
validateTypeNumberMaximum(component, instance)
{
if('maximum' in component)
{
if(component.exclusiveMaximum)
{
if(instance >= component.maximum)
{
const error = new Error(`Invalid ${component.type} instance ${instance}`)
error.code = 'E_OAS_INVALID_SCHEMAS_INSTANCE'
error.cause = `The ${component.type} must be less than ${component.maximum}`
throw error
}
}
else
{
if(instance > component.maximum)
{
const error = new Error(`Invalid ${component.type} instance ${instance}`)
error.code = 'E_OAS_INVALID_SCHEMAS_INSTANCE'
error.cause = `The ${component.type} must be less than or equal to ${component.maximum}`
throw error
}
}
}
}
validateTypeNumberMultipleOf(component, instance)
{
if('multipleOf' in component)
{
if(instance % component.multipleOf)
{
const error = new Error(`Invalid ${component.type} instance ${instance}`)
error.code = 'E_OAS_INVALID_SCHEMAS_INSTANCE'
error.cause = `The ${component.type} must be a multiple of ${component.multipleOf}`
throw error
}
}
}
validateTypeNumberFormat(component, instance)
{
if('format' in component)
{
if(false === this.validateNumberFormat.has(component.format))
{
const error = new Error(`Invalid format ${component.format}`)
error.code = 'E_OAS_INVALID_SCHEMAS_SPECIFICATION'
error.cause = 'The provided format is unknown'
throw error
}
const validateNumberFormat = this.validateNumberFormat.get(component.format)
validateNumberFormat(instance)
}
}
validateNumberFormatFloat(instance)
{
// no validation required imo
}
validateNumberFormatDouble(instance)
{
// no validation required imo
}
validateNumberFormatInt32(instance)
{
if(instance < -2147483648
|| instance > 2147483647)
{
const error = new Error(`Invalid int32 format ${instance}`)
error.code = 'E_OAS_INVALID_SCHEMAS_INSTANCE'
error.cause = 'The int32 instance must be between -2147483648 and 2147483647'
throw error
}
}
validateNumberFormatInt64(instance)
{
if(instance < -9007199254740991
|| instance > 9007199254740991)
{
const error = new Error(`Invalid int64 format ${instance}`)
error.code = 'E_OAS_INVALID_SCHEMAS_INSTANCE'
error.cause = 'The int64 instance must be between -9007199254740991 and 9007199254740991'
throw error
}
}
validateTypeObjectInstanceType(instanceType)
{
if('[object Object]' !== instanceType)
{
const error = new Error(`Invalid object instance type ${instanceType}`)
error.code = 'E_OAS_INVALID_SCHEMAS_INSTANCE'
throw error
}
}
validateTypeObjectRequired(component, instance)
{
if('required' in component)
{
const missing = []
for(const name of component.required)
{
if(undefined === instance[name])
{
missing.push(name)
}
}
if(missing.length)
{
const error = new Error(`Missing required properties`)
error.code = 'E_OAS_SCHEMA_OBJECT_MISSING_REQUIRED_PROPERTIES'
error.cause = `The object must have ${missing.length === 1 ? 'property' : 'properties'} ${this.listFormat.format(missing)}`
throw error
}
}
}
validateTypeObjectAdditionalProperties(component, instance)
{
if(false === component.additionalProperties)
{
const properties = new Set(Object.keys(component.properties ?? {}))
for(const name in instance)
{
if(false === properties.has(name))
{
delete instance[name]
}
}
}
}
validateTypeObjectMinProperties(component, instance)
{
if('minProperties' in component)
{
if(Object.keys(instance).length < component.minProperties)
{
const error = new Error(`Invalid amount of properties in object instance`)
error.code = 'E_OAS_SCHEMA_OBJECT_MIN_PROPERTIES'
error.cause = `The object must have at least ${component.minProperties} properties`
throw error
}
}
}
validateTypeObjectMaxProperties(component, instance)
{
if('maxProperties' in component)
{
if(Object.keys(instance).length > component.maxProperties)
{
const error = new Error(`Invalid amount of object instance properties`)
error.code = 'E_OAS_SCHEMA_OBJECT_MIN_PROPERTIES'
error.cause = `The object can not have more than ${component.maxProperties} properties`
throw error
}
}
}
validateTypeObjectPropertyNames(component, instance)
{
if('propertyNames' in component)
{
let regexp
if('pattern' in component.propertyNames)
{
try
{
regexp = new RegExp(component.propertyNames.pattern)
}
catch(reason)
{
const error = new Error(`Invalid regexp pattern ${component.propertyNames.pattern}`)
error.code = 'E_OAS_SCHEMA_OBJECT_PROPERTY_NAMES_INVALID_REGEXP'
error.cause = reason
throw error
}
for(const name in instance)
{
if(false === regexp.test(name))
{
const error = new Error(`Invalid object property name ${name}`)
error.code = 'E_OAS_SCHEMA_OBJECT_PROPERTY_NAMES_INVALID_NAME'
error.cause = `The object property name "${name}" must match the pattern ${component.propertyNames.pattern}`
throw error
}
}
}
else
{
const error = new Error(`Invalid "propertyNames" attribute in object component`)
error.code = 'E_OAS_INVALID_SCHEMAS_SPECIFICATION'
error.cause = 'The "propertyNames" attribute in the schema object component must define a "pattern" attribute'
throw error
}
}
}
validateTypeStringInstanceType(instanceType)
{
if('[object String]' !== instanceType)
{
const error = new Error(`Invalid string instance type ${instanceType}`)
error.code = 'E_OAS_INVALID_SCHEMAS_INSTANCE'
throw error
}
}
validateTypeStringMinLength(component, instance)
{
if('minLength' in component)
{
if(instance.length < component.minLength)
{
const error = new Error(`Invalid string instance length ${instance.length}`)
error.code = 'E_OAS_INVALID_SCHEMAS_INSTANCE'
error.cause = `The string must have at least ${component.minLength} characters`
throw error
}
}
}
validateTypeStringMaxLength(component, instance)
{
if('maxLength' in component)
{
if(instance.length > component.maxLength)
{
const error = new Error(`Invalid string instance length ${instance.length}`)
error.code = 'E_OAS_INVALID_SCHEMAS_INSTANCE'
error.cause = `The string can not have more than ${component.maxLength} characters`
throw error
}
}
}
validateTypeStringPattern(component, instance)
{
if('pattern' in component)
{
let regexp
try
{
regexp = new RegExp(component.pattern)
}
catch(reason)
{
const error = new Error(`Invalid regexp pattern ${component.pattern}`)
error.code = 'E_OAS_INVALID_SCHEMAS_SPECIFICATION'
error.cause = reason
throw error
}
if(false === regexp.test(instance))
{
const error = new Error(`Invalid string instance ${instance}`)
error.code = 'E_OAS_INVALID_SCHEMAS_INSTANCE'
error.cause = `The string must match the pattern ${component.pattern}`
throw error
}
}
}
validateTypeStringFormat(component, instance)
{
if('format' in component)
{
if(false === this.validateStringFormat.has(component.format))
{
const error = new Error(`Invalid format ${component.format}`)
error.code = 'E_OAS_INVALID_SCHEMAS_SPECIFICATION'
error.cause = 'The provided format is unknown'
throw error
}
const validateStringFormat = this.validateStringFormat.get(component.format)
validateStringFormat(instance)
}
}
validateNot(component, instance, isWriting)
{
if('not' in component)
{
let valid = false
try
{
this.conform(component.not, instance, isWriting)
}
catch(error)
{
valid = true
}
if(false === valid)
{
const error = new Error(`Invalid instance`)
error.code = 'E_OAS_INVALID_SCHEMAS_INSTANCE'
error.cause = 'The instance must not match the "not" specifiations'
throw error
}
}
}
validateConst(component, instance)
{
if('const' in component)
{
try
{
this.deepEqual(instance, component.const)
}
catch(reason)
{
const error = new Error(`Invalid instance ${instance}`)
error.code = 'E_OAS_INVALID_SCHEMAS_INSTANCE'
error.cause = `The instance must be equal to ${component.const}`
error.cause.cause = reason
throw error
}
}
}
validateEnum(component, instance)
{
if('enum' in component)
{
let errors = 0
for(const item of component.enum)
{
try
{
this.deepEqual(instance, item)
break
}
catch(error)
{
errors++
}
}
if(errors === component.enum.length)
{
const
enumsJson = component.enum.map(JSON.stringify),
enumsList = this.listFormat.format(enumsJson)
if(component.type)
{
const error = new Error(`Invalid ${component.type} instance ${instance}`)
error.code = 'E_OAS_INVALID_SCHEMAS_INSTANCE'
error.cause = `The ${component.type} must be one of: ${enumsList}`
throw error
}
else
{
const error = new Error(`Invalid instance ${instance}`)
error.code = 'E_OAS_INVALID_SCHEMAS_INSTANCE'
error.cause = `The instance must be one of: ${enumsList}`
throw error
}
}
}
}
validateStringFormatDate(instance)
{
const date = new Date(instance).toJSON()?.slice(0, 10)
if(date !== instance)
{
const error = new Error(`Invalid date instance ${instance}`)
error.code = 'E_OAS_INVALID_SCHEMAS_INSTANCE'
error.cause = 'The date must be in the format YYYY-MM-DD'
throw error
}
}
validateStringFormatTime(instance)
{
const time = new Date('2000-01-01T' + instance + 'Z').toJSON()?.slice(11, 23)
if(false === !!time?.startsWith(instance))
{
const error = new Error(`Invalid time instance ${instance}`)
error.code = 'E_OAS_INVALID_SCHEMAS_INSTANCE'
error.cause = 'The time must be in the format HH:MM:SS[.sss]'
throw error
}
}
validateStringFormatDatetime(instance)
{
if(isNaN(new Date(instance)))
{
const error = new Error(`Invalid datetime instance ${instance}`)
error.code = 'E_OAS_INVALID_SCHEMAS_INSTANCE'
error.cause = 'The datetime must be a valid date'
throw error
}
}
validateStringFormatBase64(instance)
{
if(false === /^[a-z0-9\+\/]*={0,2}$/i.test(instance))
{
const error = new Error(`Invalid base64 instance ${instance}`)
error.code = 'E_OAS_INVALID_SCHEMAS_INSTANCE'
error.cause = 'The base64 must be in the format aGVsbG8='
throw error
}
}
validateStringFormatEmail(instance)
{
if(false === /^[^@]+@[^@]+$/.test(instance))
{
const error = new Error(`Invalid email instance ${instance}`)
error.code = 'E_OAS_INVALID_SCHEMAS_INSTANCE'
error.cause = 'The email must be in the format example@domain'
throw error
}
}
validateStringFormatIpv4(instance)
{
if(false === net.isIPv4(instance))
{
const error = new Error(`Invalid ipv4 instance ${instance}`)
error.code = 'E_OAS_INVALID_SCHEMAS_INSTANCE'
error.cause = 'The ipv4 must be in valid ipv4 format'
throw error
}
}
validateStringFormatIpv6(instance)
{
if(false === net.isIPv6(instance))
{
const error = new Error(`Invalid ipv6 instance ${instance}`)
error.code = 'E_OAS_INVALID_SCHEMAS_INSTANCE'
error.cause = 'The ipv6 must be in valid ipv6 format'
throw error
}
}
validateStringFormatUrl(instance)
{
try
{
new URL(instance)
}
catch(reason)
{
const error = new Error(`Invalid URL instance ${instance}`)
error.code = 'E_OAS_INVALID_SCHEMAS_INSTANCE'
error.cause = reason
throw error
}
}
// RFC 4122. Section 3. Format
validateStringFormatUuid(instance)
{
if(false === /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(instance))
{
const error = new Error(`Invalid UUID instance ${instance}`)
error.code = 'E_OAS_INVALID_SCHEMAS_INSTANCE'
error.cause = 'The UUID must be in the format FFFFFFFF-FFFF-5FFF-BFFF-FFFFFFFFFFFF'
throw error
}
}
}