Non-Production Machines

    easyspec
    TypeScript icon, indicating that this package has built-in type declarations

    0.2.0 • Public • Published

    Easyspec

    Object validation library. It provides Validate method that takes two arguments:

    1. ValidationSpec
    2. Object of type T

    Validate

    A binary function that takes two arguments:

    1. validation specification
    2. an object

    It checks whether object adheres to the specification. and returns validation summary

    Validation specification looks like this:

    export type ValidationSpec<T1 extends DataObject> = {
        [P in keyof T1]:
            | ValidationSpec<Required<T1>[P]>               // spec for nested obj
            | ValidationPropertyRule<T1, P>[]               // or an array of validationPropertyRules
    } & { [ValidationOptionsSym]?: ValidationOptions<T1> }; // optional options
    
    export type ValidationPropertyRule<T1, P extends keyof T1> = [ // tuple of:
        (v: T1[P], k: P, o: T1) => boolean,                // 0.validator 
        (v: T1[P], k: P, o: T1) => string                 // 1.message factory
    ];

    Options

    export type ValidationOptions<T extends DataObject> = {
        // used to increase performance in case you are only interested in 
        // validity of an object and not in all the properties that are invalid
        stopAfterInvalid?: boolean,
    
        // allows you to create custom messages in case validator throws an exception
        errorHandler?: (e: ValidationException) => string,
    
        // should redundant properties make object invalid (true by default)
        redundantIsError?: boolean,
    
        // properties of an object that are allowed to be null or undefined
        optionalProps?: (keyof T)[],
    
        // whether object is allowed to be null or undefined
        isOptional?: boolean
    }
    
    // errorHandler option input value type looks like this:
    export type ValidationException = {
        key: string,       // key that caused error
        value: any,        // value that caused error
        ruleIndex: number, // index of the rule where error occured
        error: Error       // exception itself
    }

    You don't have to provide any of the options, there's a default value for every option. Default options look like this:

    const defaultOptions = {
        optionalProps: [],
        redundantIsError: true,
        stopAfterInvalid: false,
        errorHandler: ({key}) => `Error while validating property "${key}".`,
        isOptional: false
    }

    Validation summary

    The result of the Validate function is of type ValidationSummary

    export type ValidationSummary<T1 extends DataObject> = {
        valid: boolean,                                     // whether object is considered valid
        errorCount: number,                                 // number of errors
        missingProperties: string[],                        // missing properties
        redundantProperties: string[],                      // redundant properties
        errors: Record<keyof T1 | '_self', string[]>        // error messages that occured
    }

    Examples

    Simple objects

    type CatChild = {
        age?: number;
        name?: string;
        weight?: number;
    }
    
    const CatChildSpec: ValidationSpec<CatChild> = {
        age: [
            [   // validator function takes these three parameters
                // NOTE THE TYPES (they can be inferred)
                (v: number, k: 'age', o: CatChild) => typeof v === 'number',
                // message factory function same three parameters
                (value, key, obj) => `${key} must be of type number but was of type ${typeof value}`
            ],
            [   
                (value, key, obj) => value > 0,
                (value, key, obj) => `${key} must be greater than 0 but was ${value}`
            ],
        ],
        name: [
            [
                (value, key, obj) => typeof value === 'string',
                (value, key, obj) => `${key} must be of type string but was of type ${typeof value}`
            ],
        ],
        weight: [
            [
                (value, key, obj) => typeof value === 'number',
                (value, key, obj) => `${key} must be of type number but was of type ${typeof value}`
            ],
        ],
        // to specify options you need to import <ValidationOptionsSym> symbol 
        [ValidationOptionsSym]: {
            // according to the type above all properties should be optional
            optionalProps: ['name', 'age', 'weight'], 
        }
    }
    
    
    const validCatChild: CatChild = {
        age: 1,
        name: 'Tonny',
        weight: 4,
    }
    
    const summary = Validate(CatChildSpec, validCatChild);

    Result will be:

    {
        "valid": true,
        "errorCount": 0,
        "missingProperties": [],
        "redundantProperties": [],
        "errors": {}
    }
    
    const validCatChild: CatChild = {
        age: 1,
        name: 'Tonny',
        // weight: 4,  # because every property is optional you can safely omit any of them
    }
    
    const summary = Validate(CatChildSpec, validCatChild); 

    Result will be:

    {
        "valid": true,
        "errorCount": 0,
        "missingProperties": [],        # weight is not considered missing because it's optional
        "redundantProperties": [],
        "errors": {}
    }
    

    When we pass invalid object:

    const invalidCatChild: CatChild = {
        age: 'Tom',
        name: 8,
        weight: { number: 3 },
        isTiger: false
    }
    const summary = Validate(CatChildSpec, invalidCatChild);

    Result will be:

    {
      "valid": false,                                               # object is invalid
      "errorCount": 5,                                              # 5 errors were found
      "missingProperties": [],
      "redundantProperties": [
        "isTiger"                                                   # isTiger property is redundant
      ],
      "errors": {                                                   # "errors" is an object that contains 
        "age": [                                                    # an array of errors for every property:
          "age must be of type number but was of type string",
          "age must be greater than 0 but was Tom"
        ],
        "name": [
          "name must be of type string but was of type number"
        ],
        "weight": [
          "weight must be of type number but was of type object"
        ]
      }
    }
    

    Reducing amount of code

    To reduce the amount of code that you need to write to declare a spec you can try using some helper function library such as:

    • lodash
    • fp-way-core
    • ramda

    For this example I am going to use fp-way-core. Let's declare the same spec as we did above, but now using the lib.

    // create a helper for common functions
    const TypeErrorFactory = (t) => (v, k) => `${k} must be of type ${t} but was of type ${TypeOf(v)}`
    const CatChildSpec: ValidationSpec<CatChild> = {
        age: [
            [IsOfType('number'), TypeErrorFactory('number')],
            [Gt(0), (v, k) => `${k} must be greater than 0 but was ${v}`],
        ],
        name: [
            [IsOfType('string'), TypeErrorFactory('string')],
        ],
        weight: [
            [IsOfType('number'), TypeErrorFactory('number')],
        ],
        [ValidationOptionsSym]: {
            optionalProps: ['name', 'age', 'weight'],
        }
    }

    Nested objects and nested specs

    type CatChild = {
        age?: number;
        name?: string;
        weigth?: number;
    }
    
    type Cat = {
        age: number;
        name?: string;
        weigth: number;
        child: CatChild  // we are going to apply NESTED SPEC to validate this property
    }
    
    type CatParent = {
        age?: number;
        name: string;
        weigth: number;
        childCat: Cat;  // we are going to apply NESTED SPEC to validate this property
    }
    
    const CatChildSpec: ValidationSpec<CatChild> = {
        age: [
            [IsOfType('number'), TypeErrorFactory('number')],
            [Gt(0), (v, k) => `${k} must be greater than 0 but was ${v}`],
        ],
        name: [
            [IsOfType('string'),  TypeErrorFactory('string')]
        ],
        weight: [
            [IsOfType('number'),  TypeErrorFactory('number')]
        ],
        [ValidationOptionsSym]: {
            optionalProps: ['name', 'age', 'weight'],
            isOptional: true,
        }
    }
    
    const CatSpec: ValidationSpec<Cat> = {
        age: [
            [IsOfType('number'), TypeErrorFactory('number')],
        ],
        name: [
            [IsOfType('string'),  TypeErrorFactory('string')]
        ],
        weight: [
            [IsOfType('number'),  TypeErrorFactory('number')]
        ],
        child: CatChildSpec,        // just pass the SPECIFICATION for the 
                                    // nested object instead of an array of validators
        [ValidationOptionsSym]: {   
            optionalProps: ['name']
        }
    }
    
    const CatParentSpec: ValidationSpec<CatParent> = {
        age: [
            [IsOfType('number'), TypeErrorFactory('number')],
        ],
        name: [
            [IsOfType('string'),  TypeErrorFactory('string')]
        ],
        weight: [
            [IsOfType('number'),  TypeErrorFactory('number')]
        ],
        childCat: CatSpec,          // just pass the SPECIFICATION for the 
                                    // nested object instead of an array of validators
        [ValidationOptionsSym]: {
            optionalProps: ['age']
        }
    }
    
    const cat: Cat = {
        // age: 1,              // missing
        color: 'grey',          // redundant
        name: 1,
        weight: 4,
        child: {                // nested child
            name: 'Tonny jr',
            age: '1',
        },
    } as any;
    
    const catParent: CatParent = {
        age: 'old',
        name: 'Tonny Sr',
        weight: 'overweight', 
        childCat: cat           // nested cat
    } as any
    
    const result = Validate(CatParentSpec, catParent);

    Result will be:

    {
      "valid": false,
      "errorCount": 6,
      "missingProperties": [
        "childCat.age"
      ],
      "redundantProperties": [
        "childCat.color"
      ],
      "errors": {
        "age": [
          "age must be of type number but was of type string"
        ],
        "weight": [
          "weight must be of type number but was of type string"
        ],
        "childCat.name": [
          "name must be of type string but was of type number"
        ],
        "childCat.child.age": [
          "age must be of type number but was of type string"
        ]
      }
    }
    

    Please note the way nested object errors are presented. They are presented using dot notation: "childCat.child.age"

    If you want to change format of the message you can try to use function to validate the nested object as usual:

    const CatSpec: ValidationSpec<Cat> = {
        child: [
            [
                (v, k, o) => v?.age > 0, 
                (v, k, o) => `${k} age must be greater than 0 but was ${v?.age}`
            ]
        ]
    }

    Extending specs

    As you can see above there's quite a lot of repetition when declaring specs for similar types. To only write necessary specification details for different types there's Extend method.

    Extend method takes two arguments:

    1. extension specification
    2. parent specification

    Extension specification

    Similar to validation specification but:

    1. Two options are required: omitKeys and optionalProps, this is done so that you don't forget to remove properties that don't exist on the new type and rewrite optional properties in accordance to the new type.
    2. Only keys that are NOT present on the parent type are required .

    Example

    type City      = { area?: number, name: string }
    type LocalArea = { area:  number, city: string }
    
    const CitySpec: ValidationSpec<City> = {
        area: [ [IsOfType('number'), TypeErrorFactory('number')] ],
        name: [ [IsOfType('string'), TypeErrorFactory('string')] ],
        [ValidationOptionsSym]: { optionalProps: ['area'] }
    }
    
    
    const LocalAreaSpec = Extend<City, LocalArea>({ // from city -> to -> localArea spec
        // 1. only add properties that don't exist on the parent spec
        city: [ [IsOfType('string'), TypeErrorFactory('string')] ],
        // area propertyRule will be inherited from the parent
        [ValidationOptionsSym]: {
            // 2. omit parent spec properties using omitkeys option
            omitKeys: ['name'],
            
            // 3. override optionalProperties option 
            // (in the parent spec it says that we have optional property 'name' 
            // which is not true for the current spec)
            optionalProps: [],
        }
    }, CitySpec);

    In the example above you can see how to easily add and remove properties from the parent specification. But you can also: Override parent options and property rules:

    const LocalAreaSpec = Extend<City, LocalArea>({
        // city property is not present on the parent spec, 
        // so it's required that you provide property rules for it
        city: [ [IsOfType('string'), TypeErrorFactory('string')] ],
        // 1. area property rule will be completely overriden
        // 2. area property is present on the parent so overriding it is optional
        area: [ 
            [IsOfType('number'), TypeErrorFactory('number')],
            [Gt(0), (v, k) => `${k} must be greater than 0 but was ${v}`],
        ],
        [ValidationOptionsSym]: {
            // these options are required when extending
            omitKeys: ['name'],
            optionalProps: [],
            
            // you can specify more options to substitute those on the parent options object
            // these options are optional when extending
            redundantIsError: true,
            stopAfterInvalid: false,
            errorHandler: ({key, ruleIndex}) => `Error while validating property "${key}" at rule index ${ruleIndex}`,
            isOptional: false
        }
    }, CitySpec);

    Speed comparison

    🔨 to be continued...

    Install

    npm i easyspec

    DownloadsWeekly Downloads

    27

    Version

    0.2.0

    License

    MIT

    Unpacked Size

    30.7 kB

    Total Files

    6

    Last publish

    Collaborators

    • francoisvong