Skip to content

Latest commit

 

History

History
637 lines (498 loc) · 16.5 KB

README.md

File metadata and controls

637 lines (498 loc) · 16.5 KB

Object-validator

Validate input data based on a defined schema with automatic compile time type interference and excellent performance, removing any reason to not always do validation on input:

Features:

  • Detailed validation errors
  • Precise validation with sub types for number like floats and integers with min/max value
  • Optimized code generation (Similar performance to hand optimized code)
  • Compile time type interference (Without using compiler plugins or parsers, just pure typescript)
  • Easy to implement own validators
  • Zero dependencies

Purpose

There exists a lot of validation libraries but most of them are slow, limit what you can validate, are tied to a specific build system, does not allow extension or has horrible syntax. ObjectValidator lets you construct simple schemas where you can validate input in detail, fx. limiting the size of a integer and give full feedback on what failed validation to the end user. While doing this you also construct your typescript types without having to define separate interfaces or types for what you are validating.

When running in optimized mode it generates a custom function that should give the fastest and least overhead validation that is possible often speeding up validating by more than 10 times.

Installation

npm install @connectedcars/object-validator

Features

The following validators are supported:

  • Array: Validate that a value is an array of a specific type using other validators and min/max length of array
  • Boolean: Validate that a value is a boolean
  • Date: Validate that a value is a JavaScript Date object
  • DateTime: Validate that a value is an ISO 8601 date/time string
  • ExactString: Validate that a value is a string that matches an exact string
  • FloatString: Validate that a value is float string within a min and max limit
  • Float: Validate that a value is a float within a min and max limit
  • IntegerString: Validate that a value is a integer string within a min and max limit
  • Integer: Validate that a value is a integer within a min and max limit
  • Null: Validate that a value is null
  • Object: Validate that a value is an object using other validators
  • RegexMatch: Validate that a value is a string that matches a regular expression
  • Sample: Validate that a value matches a sample
  • String: Validate that a value is a string within a min and max limit
  • Undefined: Validate that a value is undefined
  • Union: Validate that a value validates at least one validator out of a number of validators
  • Enum: Validate that a value validates is a string and matches at least one exact string
  • Unknown: Validate that a value is not undefined or or unset on a object

Validation functions:

  • .validate(value) : Returns list of errors found in the validation
  • .isType(value, listOfErrors) : Type guard for the type to avoid doing validation twice
  • .isValid(value) : Type guard for the type
  • .cast(value) : Returns type or throws ValidationError
  • .toString() : Return validator code or TypeScript interface for the validator
  • .AssertType<OwnType, true | false> : No op function to validate custom types match a validator

Common options:

  • optimize: Create optimized function and use this for validation (default: true)
  • earlyFail: Return on first error (default: false)

How to use

Simple

The sample validator enables building very quick validators where that you slowly can add more validation as you go along.

import { RequiredSample } from "@connectedcars/object-validator";

const tripValidator = new RequiredSample(
  {
    type: new RequiredExactString("trip"),
    unitId: "1234567",
    messageId: "MSG-12345",
    recordedAt: "2018-08-06T13:37:00Z",
    tripId: 1337,
    value: 500,
    position: {
      latitude: 55.332131,
      longitude: 12.54454,
      accuracy: 21,
    },
    positions: [
      {
        latitude: 55.332131,
        longitude: 12.54454,
        accuracy: 21,
      },
    ],
  },
  { optimize: true, earlyFail: true },
);

// Returns true if unknownValue validates
if (objectValidator.isValid(unknownValue)) {
  // unknownValue has been cast to the following type
  // {
  //  type: 'gps_odometer_km'
  //  unitId: string
  //  recordedAt: string
  //  tripId: number
  //  value: number
  //  position: {
  //    latitude: number
  //    longitude: number
  //    accuracy: number
  //  }
  //  positions: Array<{
  //    latitude: number
  //    longitude: number
  //    accuracy: number
  //  }>
  // }
  console.log(unknownValue.tripId);
}

console.log(tripValidator.toString()); // Dump validator code
console.log(tripValidator.toString({ type: true })); // Dump typescript interface for the validator

Advanced

import {
  RequiredObject,
  OptionalObject,
  RequiredArray,
  RequiredDateTime,
  RequiredFloat,
  RequiredInteger,
  RequiredObject,
  RequiredString,
  RequiredExactString
} from '@connectedcars/object-validator'

const tripValidator = new RequiredObject(
  {
    type: new RequiredExactString('trip'),
    unitId: new RequiredString(1, 32),
    recordedAt: new RequiredDateTime(),
    tripId: new RequiredInteger(0, 4294967295),
    value: new RequiredInteger(0, 999999),
    position: new OptionalObject({
      latitude: new RequiredFloat(-90, 90),
      longitude: new RequiredFloat(-180, 180),
      accuracy: new RequiredInteger(0, 20)
    }),
    positions: new RequiredArray(
      new RequiredObject({
        latitude: new RequiredFloat(-90, 90),
        longitude: new RequiredFloat(-180, 180),
        accuracy: new RequiredInteger(0, 20)
      }),
      0,
      10
    )
  },
  { optimize: true }
)

let unknownValue = {
  type: 'trip',
  unitId: '1234567',
  messageId: 'MSG-12345',
  recordedAt: '2018-08-06T13:37:00Z',
  tripId: 1337,
  value: 500,
  position: {
    latitude: 55.332131,
    longitude: 12.54454,
    accuracy: 21
  },
  positions: []
}

// Returns errors found in object
let errors = gpsOdometerKm.validate(unknownValue)
if (errors.length > 0) {
  console.error(`${errors.length} errors found`)
}

// Returns true if errors is empty
if(gpsOdometerKm.isType(unknownValue, errors)) {
  console.log(unknownValue.unitId) // unknownValue.unitId has been type cast to string
}

// Returns true if unknownValue validates
if(objectValidator.isValid(unknownValue)) {
  console.log(unknownValue.tripId) // unknownValue.unitId has been type cast to number
}

// Throws ValidationsError if unknownValue does not validate
let knownValue = objectValidator.cast(unknownValue)
  console.log(knownValue.positions.length()) // unknownValue.unitId has been type cast to array
}

interface Trip {
  type: 'trip',
  unitId: string,
  recordedAt: string,
  tripId: number,
  value: number,
  position?: {
    latitude: number,
    longitude: number,
    accuracy: number
  },
  positions: [
    {
      latitude: number,
      longitude: number,
      accuracy: number
    }
  ]
}
// Throws compile error if type does not match validator
objectValidator.AssertType<Trip, true>()

// Throws compile error if type matches validator
objectValidator.AssertType<Trip, false>()

Validators

Array (validator, min, max, options)

Validators: RequiredArray, OptionalArray, NullableArray, OptionalNullableArray

Function: isArray

let arrayValidator = new RequiredArray(new RequiredInteger(0, 10), 0, 2);
let errors = arrayValidator.validate([1, 10]);
if (isArray(new RequiredInteger(0, 10), [1, 10] 0, 2))  {
  console.log('Is integer array')
}

Boolean

Validators: RequiredBoolean, OptionalBoolean, NullableBoolean, OptionalNullableBoolean

Function: isBoolean

let booleanValidator = new RequiredBoolean();
let errors = booleanValidator.validate(true);
if (isBoolean(true)) {
  console.log("Is boolean");
}

Date

Validators: RequiredDate, OptionalDate, NullableDate, OptionalNullableDate

Function: isDate

let dateValidator = new RequiredDate();
let errors = dateValidator.validate(new Date());
if (isDate(new Date())) {
  console.log("Is Date");
}

DateTime

Validators: RequiredDateTime, OptionalDateTime, NullableDateTime, OptionalNullableDateTime

Function: isDateTime

let dateTimeValidator = new RequiredDateTime();
let errors = dateTimeValidator.validate("2018-08-06T13:37:00Z");
if (isDateTime("2018-08-06T13:37:00Z")) {
  console.log("Is ISO date");
}

ExactString (expected)

Validators: RequiredExactString, OptionalExactString, NullableExactString, OptionalNullableExactString

Function: isExactString

let exactStringValidator = new RequiredExactString("mystring");
let errors = exactStringValidator.validate("mystring");
if (isExactString("mystring", "mystring")) {
  console.log("Is mystring");
}

FloatString (min, max)

Validators: RequiredFloatString, OptionalFloatString, NullableFloatString, OptionalNullableFloatString

Function: isFloatString

let floatStringValidator = new RequiredFloatString();
let errors = floatStringValidator.validate("1.0");
if (isFloatString("1.1")) {
  console.log("Is float string");
}

Float (min, max)

Validators: RequiredFloat, OptionalFloat, NullableFloat, OptionalNullableFloat

Function:

let floatValidator = new RequiredFloat();
let errors = floatValidator.validate(1.0);
if (isFloat(1.1)) {
  console.log("Is float");
}

IntegerString (min, max)

Validators: RequiredIntegerString, OptionalIntegerString, NullableIntegerString, OptionalNullableIntegerString

Function: isIntegerString

let integerStringValidator = new RequiredIntegerString();
let errors = integerStringValidator.validate("10");
if (isIntegerString("10")) {
  console.log("Is integer string");
}

Integer (min, max)

Validators: RequiredInteger, OptionalInteger, NullableInteger, OptionalNullableInteger

Function: isInteger

let integerValidator = new RequiredInteger();
let errors = integerValidator.validate(10);
if (isInteger(10)) {
  console.log("Is integer");
}

Null

Validators: RequiredNull, OptionalNull, NullableNull, OptionalNullableNull

Function: isNull

let nullValidator = new RequiredNull();
let errors = nullValidator.validate(null);
if (isNull(10)) {
  console.log("Is null");
}

Object (schema)

Validators: RequiredObject, OptionalObject, NullableObject, OptionalNullableObject

Function: isObject

let objectValidator = new RequiredObject({
  int: new RequiredInteger(0, 10),
});
let errors = objectValidator.validate({
  int: 10,
});
if (isObject({ int: new RequiredInteger(0, 10) }, { int: 10 })) {
  console.log("Is object");
}

RegexMatch (regex)

Validators: RequiredRegexMatch, OptionalRegexMatch, NullableRegexMatch, OptionalNullableRegexMatch

Function: isRegexMatch

let regexMatchValidator = new RequiredRegexMatch(/^hello$/);
let errors = regexMatchValidator.validate("hello");
if (isRegexMatch(/hello/, "hello")) {
  console.log("Is regex match");
}

Sample (sample)

Validators: RequiredSample, OptionalSample, NullableSample, OptionalNullableSample

Function: isSample

The sample validator will convert a sample to a validator based on types with a few exceptions:

  • Integer numbers in the sample with be converted to a IntegerValidator, fx. 1, 10, etc.
  • Float numbers in the sample with be converted to a FloatValidator, fx. 1.1, 2.2, etc.
  • Strings that validates with the DateTime validator will be converted DateTimeValidator, fx. '2018-08-06T13:37:00Z'
let sampleValidator = new RequiredSample({
  date: '2018-08-06T13:37:00Z'
  key: new RequiredExactString('value')
})
let errors = sampleValidator.validate({
  date: '2018-08-06T13:37:00Z',
  key: 'value'
})
let sample = {
  date: '2018-08-06T13:37:00Z'
  key: new RequiredExactString('value')
}
let value = {
  date: '2018-08-06T13:37:00Z',
  key: 'value'
}
if (isSample(sample, value)) {
  console.log('Is sample match')
}

More complex example:

const sample = {
  type: new RequiredExactString("gps_odometer_km"),
  unitId: "1234567",
  recordedAt: "2018-08-06T13:37:00Z",
  tripId: 1337,
  value: 500,
  position: {
    latitude: 55.332131,
    longitude: 12.54454,
    accuracy: 18,
    extra: {
      tag: "test",
      tagversion: 32,
      tagDepth: 3.1416,
    },
  },
  positions: [
    {
      latitude: 55.332131,
      longitude: 12.54454,
      accuracy: 18,
    },
  ],
};
const sampleValidator = new RequiredSample(sample, { optimize });
const errors = sampleValidator.validate(sample);

Converting to a normal validator structure:

console.log(`interface MyType ${sampleValidator.toString({ types: true })}`);
console.log(`let myTypeValidator = ${sampleValidator.toString()}`);

String (min, max)

Validators: RequiredString, OptionalString, NullableString, OptionalNullableString

Function: isString

let stringValidator = new RequiredString();
let errors = stringValidator.validate("hello1234");
if (isString("hello1234")) {
  console.log("Is string");
}

Undefined

Validators: RequiredUndefined, OptionalUndefined, NullableUndefined

Function: isUndefined

let undefinedValidator = new RequiredUndefined();
let errors = undefinedValidator.validate(undefined);
if (isUndefined(undefined)) {
  console.log("Is undefined");
}

Union ([validator, ...])

Validators: RequiredUnion, OptionalUnion, NullableUnion, OptionalNullableUnion

Function: isUnion

The union validator takes a union of validates and validates the value with each of them, it will stop when the first validates and return a positive result. It has a special optimization for a union of ObjectValidators with a shared required ExactString key, it will use the key to figure out what validator to use.

let unionValidator = new RequiredUnion([
  new RequiredInteger(),
  new RequiredString(),
]);
let errors1 = unionValidator.validate("hello");
let errors2 = unionValidator.validate(10);
let union = [new RequiredInteger(), new RequiredString()];
if (isSample(union, 10)) {
  console.log("Is union match");
}

Example of shared required ExactString key:

const numberMessageValidator = new RequiredObject({
  type: new RequiredExactString("number"),
  value: new RequiredFloat(),
});

const stringMessageValidator = new RequiredObject({
  type: new RequiredExactString("string"),
  value: new RequiredString(),
});

const errorMessageValidator = new RequiredObject({
  type: new RequiredExactString("error"),
  error: new RequiredString(),
});

const messageValidator = new UnionValidator([
  numberMessageValidator,
  stringMessageValidator,
  errorMessageValidator,
]);

let errors = messageValidator.validate({ type: "string", value: "hello" });

EnumValidator([string, ...])

Alias: RequiredEnum, OptionalEnum, NullableEnum, OptionalNullableEnum

The enum validator is a short hand instance of the union validator:

let unionValidator = new RequiredEnum(["hello", "more"]);
let errors1 = unionValidator.validate("hello");
let errors2 = unionValidator.validate(10);

Unknown

Validators: RequiredUnknown, OptionalUnknown

The unknown validator only makes sense to use with another validate like the object validator to require a key even when the type is not known.

let unknownValidator = new RequiredUnknown();
let errors1 = unknownValidator.validate("hello");
let errors2 = unknownValidator.validate(10);
let errors3 = unknownValidator.validate(null);

Benchmark

success in 1.937 s (516,262.261 ops/s)
success optimized in 0.195 s (5,128,205.128 ops/s)
failureEarly in 1.349 s (741,289.844 ops/s)
failureEarly optimized in 0.715 s (1,398,601.399 ops/s)
failureLate in 1.309 s (763,941.94 ops/s)
failureLate optimized in 0.351 s (2,849,002.849 ops/s)

Generating Rust Types

Checkout examples in ./test/rust-types.test.ts & ./util.test.ts

  • Import the generateRustTypes() function from util in your project.
  • Make an array of the top level Validators and pass it into the function
  • Take the result, either copy from the console into your rust file, or give it a file path as the second argument