Object-To-Model library, lets the user construct a strict model definition using ES6 class and Typescript Decorators api, the model class will wrapped the parsing validation and transforming processes, just like a tunnel were in one side a raw object enters and exit in the other side after passes the "casting" process.
- Embedding an input processing logic & validation in the model class definitions using decorators.
- Supporting common types and your own Models as (nested) class attributes in your model class definition.
- Providing an easy API to customize each stage in the process, parsing, validating and transforming.
- Supporting Model class extending (eg.
class AdminModel extends UserModel ...
)
npm install tunnel-cast
- Clone the project repo.
- Move to project folder.
- Run
npm i
- Run
npm run test
import { field, parsing } from 'tunnel-cast/core/decorator'
import { cast } from 'tunnel-cast/core/cast'
class User {
@field.String({ required: false })
username: string;
@field.String()
email: string;
@field.Boolean({ required: false })
notificationOn: number;
}
class ServerResponse {
@field.Number({
min: 3,
required: true,
parsing: [(val) => Number(val)]
})
apiVersion: number;
@parsing.JsonParse
@field.Array()
imageTensor: Array<Array<number>>;
@field.Model()
user: User
}
const { value, errors } = cast(ServerResponse, {
apiVersion: '9',
imageTensor: "[[1,2],[2,3]]",
user: { email: 'user@examle.com' }
});
console.log(JSON.stringify({ value, errors }, undefined, 2))
// output :
{
"value": {
"apiVersion": 9,
"imageTensor": [
[1, 2], [2, 3]
],
"user": {
"email": "user@examle.com"
}
}
}
class User {
@field.String({
fallbackAttribute: "name"
})
username: string;
@field.String({
format: /.+@.+\.com/
})
email: string;
@field.Boolean({
required: false,
default: false
})
notificationOn: number;
@field.Array({
validations: [(value) => value.length > 2 && value[1].startsWith('Rocky')],
minLength: 1,
maxLength: 15,
ofType: 'string'
})
favorites: Array<string>
@field.String({
parsing: [(value) => (typeof value == 'string' ? value.toLowerCase() : value)],
enums: ['male', 'female'],
})
gender: string
}
const { value, errors } = cast(User, {
name: "Bob",
email: "bob@gmail.com",
favorites: ["Rocky 1", "Rocky 2", "Rocky 3", "Rocky 4", "Rocky 5"],
gender: "MALE"
});
console.log(JSON.stringify({ value, errors }, undefined, 2))
// output :
{
"value": {
"username": "Bob",
"email": "bob@gmail.com",
"notificationOn": false,
"favorites": [
"Rocky 1",
"Rocky 2",
"Rocky 3",
"Rocky 4",
"Rocky 5"
],
"gender": "male"
}
}
Lets define some terms regarding the flow;
-
casting
- The process of applying a model definitions on an object and trying to transform the object to the model instance. -
model
/model definitions
- As a concept, a model is a collection of restrictions and conditions where a value that satisfied those, can be referred to as a model instance.
In practice a model would be a class declared with this module fields decorators and such.
A process of casting (as this module implements) an object to a model (a class with decorated attributes), is done by iterating the model's decorated attributes, and one by one, trying to fit the matching key's (or mapping key) value from the input object to the current processed model's (definition) attribute.
The "fit" action assembled from cupel of stages (executed in that order) :
- [ Extracting stage ]
Extracting the value from the input object. In one of three ways:
- using the origin field name, in case
options.attribute
wasn't provided, - using
options.attribute
has the field, in case it was provided. - using
options.fallbackAttribute
has the field, in case it was provided, and the value in the origin field name is not defined.
// simplified value extraction.
let value = input[ops.attribute || originFieldName];
if(value == undefined && ops.fallbackAttribute) {
value = input[fallbackAttribute]
}
- [ Existents stage ]
Evaluating "existents" status of the current input attribute, and checking the attribute value against therequired
(ornullable
) requirement (from the provided options or the global default setting).
// simplified required-status evaluation.
let requiredStatus;
if(ops.required != null) {
requiredStatus = ops.required;
} else if(ops.nullable != null) {
requiredStatus = !ops.nullable;
} else {
requiredStatus = globalSetting.DEFAULT_REQUIRED; // can be configured
}
If stage 2. fails - an error will be thrown.
If stage 2. passes with an existing value - the process will continue to the "parsing" stage.
If stage 2. passes with non existing value - the process will continue to the "defaulting" stage.
- [ Defaulting stage ]
Assigning a default value, if one was provided in the decorator options. The default value will be assigned on projected output object. After this stage the "fit" action is done and the casting process will continue to the next model's attribute.
- [ Assertion stage ]
Asserting the original field's value against a given type, it can be one of the following :
- A string indicating a primitive type (e.g
number
,string
...), assertion performed astypeof input == ops.assert
- A class indicating a non primitive type (e.g
User
), assertion performed asinput instanceof ops.assert
- An array of one of the above indication an array of that type, (e.g
[]
,[number]
,[User]
... ).
The assertion acknowledge the first item in the assert array, and assert each item in the input array.
If the input is not an array the assertion fails, assert array with the value[]
will allow any type of input array.
Note : Assertion stage allows you to create a first contact layer interface, giving you control over both sides of the tunnel, assert the original given input, and validate final casted value.
- [ Parsing stage ]
After stage 4, a list of function (provided in the decorator options under the keyparsing
), will run one after the other where the output of the last function will be provided as the input for the next, has the input for the first function will be the original value.
- [ Native Validation stage ]
The native-validation are the type's "from the box" validations, e.gmin
,max
fromNumber
type. If all the native-validation (provided in the decorator options) passed the process continue to stage 7.
- [ Extra Validation stage ]
a list of function (provided in the decorator options under the keyvalidation
), will run one after the other. If all the validation function return will true the process continue to stage 8.
- [ Transforming stage ]
This stage acts just likeparsing
stage only after the entire validations requirements ended successfully. a list of function (provided in the decorator options under the keytransforming
), will run one after the other where the output of the last function will be provided as the input for the next, has the input for the first function will be the existing value from the last stage.
Define the field type.
@field.Number(options?: NumberFieldOptions)
Description :
Defines an attribute as anumber
type, with additional requirements.
Example :
class MyModel {
@fields.Number({
min: 10
})
myNumber: number;
}
@field.String(options?: StringFieldOptions)
Description :
Defines an attribute as astring
type, with additional requirements.
Example :
class User {
@field.String()
id: string;
@field.String({ format: /^ ... $/ })
email: string;
}
@field.Boolean(options?: BooleanFieldOptions)
Description :
Defines an attribute as aboolean
type, with additional requirements.
Example :
class MyModel {
@fields.Boolean({
required: false,
default: true
})
myNumber: boolean;
}
@field.Array(options?: ArrayFieldOptions)
Description :
Defines an attribute as aarray
type, with additional requirements.
Example :
class ImageData {
@field.Array()
imageTensor: Array<Array<number>>;
}
const { value } = cast(ImageData, {
imageTensor: [ [1,2], [2,3] ],
});
@field.Model(options?: ModelFieldOptions)
Description :
Defines an attribute as aModel
type, with additional requirements.
Example :
class User {
@field.String()
id: string;
@field.String({ format: /^ ... $/ })
email: string;
}
class MyModel {
@fields.Model()
user: User;
}
Define the original (untouched) attribute type.
@field.Assert(type?: PrimitiveType | Class | [PrimitiveType] | [Class])
Description :
Define the original (untouched) attribute type, an alternative for usingoptions.assert
.
Example :
class Result {
@field.Assert(['string'])
@field.Array({
parsing: [(value) => value.map(s => Number(s))]
ofType: 'number',
})
age_range: : string
}
const { value } = cast(Result, {
age_rage: ["13","30"],
});
// value : [13, 30]
Define (append to) the field's (pre-validation) parsing process.
The parsing
object is a collection of commonly used parsing functions, simply for reducing duplication.
@parsing.JsonStringify
Description :
Add (the native) JSON.stringify function to the parsing function list.
Example :
class RequestQs {
@parsing.JsonStringify
@field.String()
age_range: : string
}
const { value } = cast(RequestQs, {
age_rage: [13,30],
});
// value : "[13, 30]"
@parsing.JsonParse
Description :
Add (the native) JSON.parse function to the parsing function list.
Example :
class ImageData {
@parsing.JsonParse
@field.Array()
imageTensor: Array<Array<number>>;
}
const { value } = cast(ImageData, {
imageTensor: "[[1,2],[2,3]]",
});
// value : [ [1,2], [2,3] ]
interface BaseFieldOptions
Key | Type | Default | Description |
---|---|---|---|
attribute |
string |
same as the decorated attribute |
Mapping key |
fallbackAttribute |
string |
undefined |
A fallback mapping key, if the value for [ attribute ] is undefined the value will be taken from [fallbackAttribute ] |
assert |
`PrimitiveType |
Class | [PrimitiveType] |
validate |
boolean |
true |
|
required |
boolean |
true |
|
requiredIf |
Function |
undefined |
|
nullable |
boolean |
undefined |
|
nullableIf |
Function |
undefined |
|
default |
any |
undefined |
will be assigned as the default value if the field not exists and defined as not required. |
error |
string |
generic error massage |
|
parsing |
Array<Function> |
[] |
will run if pass required / nullable validation, and before any other validation / transformation will run. |
validations |
Array<Function> |
[] |
will run if all native validation pass and after. |
transformations |
Array<Function> |
[] |
array of functions that receives the final value of the field till the will run if all validation pass and after. |
interface NumberFieldOptions extends BaseFieldOptions
Key | Type | Default | Description |
---|---|---|---|
min |
number |
undefined |
Minimum value restriction. |
max |
number |
undefined |
Maximum value restriction. |
interface StringFieldOptions extends BaseFieldOptions
Key | Type | Default | Description |
---|---|---|---|
format |
string / RegExp |
undefined |
Value's format restriction. |
enums |
Array<string> |
undefined |
Group of valid of values restriction. |
interface BooleanFieldOptions extends BaseFieldOptions
Key | Type | Default | Description |
---|---|---|---|
-- | -- | -- | -- |
interface ArrayFieldOptions extends BaseFieldOptions
Key | Type | Default | Description |
---|---|---|---|
minLength |
number |
-- | -- |
maxLength |
number |
-- | -- |
ofType |
Array<string> |
undefined |
Validate the type of each item in the array. |
allowType |
any primitive type string | undefined |
-- |
interface ModelFieldOptions extends BaseFieldOptions
Key | Type | Default | Description |
---|---|---|---|
-- | -- | -- | -- |
interface ModelSpec
- Experimental
Key | Type | Default | Description |
---|---|---|---|
globals | { [key: string]: Array<any> } |
-- | -- |
definitions | { [key: string]: Array<any> } |
-- | -- |
fields | Array<string> |
-- | -- |
name | string |
-- | -- |
serialize | (space?: number): string |
-- | -- |
cast<T>(model: Class<T>, object: any): { value: T, errors: }
Description :
Preform the casting process of a model on an object, it applies the model definitions on an object, and attempt to cast it to the model instance, in case of success the return value, will be an actual instance of the model class.
Example :
class User {
@field.String({
fallbackAttribute: "email"
})
username: string;
@field.String()
email: string;
@field.Boolean({ required: false })
notificationOn: number;
}
const { value, errors } = cast(User, {
email: 'john@examle.com',
username: 'John',
});
if(errors) {
throw Error('Invalid input.')
}