Architecture
The internal architecture is described only in it´s basics. Always have a look in the code.
1. Schema classes
Each schema class should inherit from Schema
or any of it´s subclasses and is placed in the type
folder.
import AnySchema from './Any'
import ValidationError from '../Error'
import type Data from '../Data'
import Reference from '../Reference'
class IPSchema extends AnySchema {
// content goes here
}
You also see some imports which are basically needed in any schema. All further code goes inside this class.
A constructor class like below which calls the parent constructor using super
is needed.
constructor(base?: any) {
super(base)
this._setting.format = 'short'
// add check rules
let raw = this._rules.descriptor.pop()
let allow = this._rules.descriptor.pop()
this._rules.descriptor.push(
this._typeDescriptor,
allow,
this._versionDescriptor,
this._formatDescriptor,
raw,
)
raw = this._rules.validator.pop()
allow = this._rules.validator.pop()
this._rules.validator.push(
this._typeValidator,
allow,
this._versionValidator,
this._formatValidator,
raw,
)
}
In this you see a lot of things. on line 3 a default will be set for the format
setting. It is used
if nothing is setup by the implementation.
The rest of the code shows two rule sets which are changed (because order is relevant):
- the description rules
- the validation rules
On each of them the last two rules from the parent are popped off to be added later at the correct position again. Also three new rules are added, the definitions will follow later.
Next you may add some new options:
version(value?: 4 | 6 | Reference): this {
return this._setAny('version', value)
}
mapping(flag?: bool | Reference): this { return this._setFlag('mapping', flag) }
This set up the methods to set the options (really stored in this._setting.xxx
). The real storing
is done using some set methods but in more complex scenarios you may do this on your own:
valid(value?: any): this {
const set = this._setting
if (value instanceof Reference) {
throw new Error('Reference is only allowed in allow() and disallow() for complete list')
}
if (value === undefined) set.required = false
else if (set.allow instanceof Reference) {
throw new Error('No single value if complete allow() list is set as reference.')
} else {
set.allow.add(value)
if (!(set.allow instanceof Reference)) set.disallow.delete(value)
}
return this
}
Here you see the checking for possible reference values everywhere and that if a value is set at
one point it may also change another one. This methods may also throw Error
if anything impossible is set.
And at last the methods for the rules which are referenced in the constructor have to be set. The best way is to name the methods after the area and type. Each part may have:
- descriptor - which generates a human description ending with single newline
- check - which may change the schema check (resolved setting) values
- validator - which finally will test and sanitize the value
_versionDescriptor() {
const set = this._setting
let msg = ''
if (set.version) {
if (this._isReference('version')) {
msg += `Valid addresses has to be of IP version defined at ${set.version.description}. `
} else msg += `Only IPv${set.version} addresses are valid. `
}
if (set.mapping) {
if (this._isReference('mapping')) {
msg += `IPv4 adresses may be automatically converted if set under ${set.mapping.description}. `
} else msg += 'IPv4 addresses may be automatically converted. '
}
return msg.length ? `${msg.trim()}\n` : msg
}
_versionValidator(data: Data): Promise<void> {
const check = this._check
try {
this._checkNumber('version')
this._checkBoolean('mapping')
if (check.version && ![4, 6].includes(check.version)) {
throw new Error(`Only IP version 4 or 6 are valid, ${check.version} is unknown`)
}
} catch (err) {
return Promise.reject(new ValidationError(this, data, err.message))
}
// version
if (check.version) {
if (check.version === 4) {
if (data.value.kind() === 'ipv6') {
if (check.mapping && data.value.isIPv4MappedAddress()) data.value = data.value.toIPv4Address()
else {
return Promise.reject(new ValidationError(this, data,
`The given value is no valid IPv${check.version} address`))
}
}
} else if (data.value.kind() === 'ipv4') {
if (check.mapping) data.value = data.value.toIPv4MappedAddress()
else {
return Promise.reject(new ValidationError(this, data,
`The given value is no valid IPv${check.version} address`))
}
}
}
return Promise.resolve()
}
While the descriptor method has to be always synchronous, the validator methods may be synchronous or return a Promise.
You may also overwrite some methods from the parent classes to make them more appropriate to this type. This is often needed for the allow/deny mechanism to check type sensitive.
2. References
References are a core element of the validator. They should be supported nearly everywhere. But as always there are places in which it makes no sense and only enhances the complexity. But in all other ones they should be supported by the descriptor and validator methods.
3. Control Flow
The workflow will lokk like:
- load the schema
- set schema (define it)
- the schema is checked while defining
- validate data structure
- data references are resolved
- validate each schema definition level
- schema references are resolved
- check for definition errors
- validator rules are called
- resulting data structure is returned
- collect in data class
- return asynchronous with error or resulting data structure
3.1. How it works
The methods which are used from the outside are description
and validate
which
are both defined in the base Schema
class and don´t need to be overwritten.
With the defined rules they will collect all information from the concrete subclass or run all validations.
3.2. Setter methods
To keep it simple there are no alias method names to set properties. Also there are no pretty coding methods like 'is', 'be', 'should'...
All boolean settings are set using getters and support the not
property before.
As possible the default should be a false
value which is then set. So if the
default is to be optional the method should better be called require
to
don´t need the not in most cases.