npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

easyspec

v0.2.0

Published

Data validation utility

Downloads

8

Readme

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...