Have ideas to improve npm?Join in the discussion! ¬Ľ

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

    10.2.1¬†‚Äʬ†Public¬†‚Äʬ†Published

    Quartet 10

    Build Status Coverage Status

    Size: 5.39 KB (minified and gzipped). No dependencies. Size Limit controls the size.

    It is a declarative and fast tool for data validation.

    Examples

    See examples here.

    Is there extra word in this list?

    • 3rd-party API
    • Typescript
    • Confidence
    • Simplicity
    • Performance

    In our opinion, there is no extra word. Let's take a look at the following situation.

    We request information about the user from the 3rd-party API.

    This data has a certain type, we write it in the TypeScript language in this way:

    interface Response {
      user: {
        id: number;
        name: string;
        age: number;
        gender: "Male" | "Female";
        friendsIds: number[];
      };
    }

    To achieve Confidence we will write a function that tells us whether the answer is of type Response.

    // More details about such functions google
    // "Typescript Custom Type Guards"
    function checkResponse(response: any): response is Response {
      if (response == null) return false;
     
      if (response.user == null) return false;
     
      if (typeof response.user.id !== "number") return false;
     
      if (typeof response.user.name !== "string") return false;
     
      if (typeof response.user.age !== "number") return false;
     
      if (VALID_GENDERS_DICT[response.user.gender] !== true) return false;
     
      if (!response.user.friendsIds || !Array.isArray(response.user.friendsIds))
        return false;
      for (let i = 0; i < response.user.friendsIds.length; i++) {
        const id = response.user.friendsIds[i];
        if (typeof id !== "number") return false;
      }
     
      return true;
    }
     
    const VALID_GENDERS_DICT = {
      Male: true,
      Female: true,
    };

    Now in the place where we make the request, we will check:

    // ...
    const userResponse = await GET("http://api.com/user/1");
    if (!checkResponse(userResponse)) {
      throw new Error("API response is invalid");
    }
    const { user } = userResponse; // has type Response
    // ...

    This approach, with a stretch, but can be called Simple.

    It's pretty hard to come up with a faster option to provide a type guarantee. Therefore, this code has sufficient Performance.

    We got everything we wanted!

    Objections

    You can say: How can you call the function checkResponse simple? We would agree if it were as declarative as the type of Response itself. Something like:

    const checkResponse = v<Response>({
      user: {
        id: v.number,
        name: v.string,
        age: v.number,
        gender: ["Male", "Female"],
        friendsIds: v.arrayOf(v.number),
      },
    });

    Yes! Anyone would agree with that. Such an approach would be extremely convenient. But only on condition that the performance remains at the same level as the imperative version.

    Confession

    This is exactly what this library provides you. I hope this example inspires you to read further and subsequently start using this library.

    How to use it?

    Here's what you need to do to use this library.:

    • Install
    npm i -S quartet
    
    • Import the "compiler" of schemas:
    import { v } from "quartet";
    • Describe the type of value you want to check.

    This step is optional, and if you do not use TypeScript, you can safely skip it. It just helps you write a validation scheme.

    type MyType = // ...
    • Create a validation scheme
    const myTypeSchema = // ...
    • Compile this schema into a validation function
    const checkMyType = v<MyType>(myTypeSchema);

    or the same without TypeScript type parameter:

    const checkMyType = v(myTypeSchema);
    • Use checkMyType on data that you are not sure about. It will return true if the data is valid. It will return false if the data is not valid.

    Primitives

    Each primitive Javascript value is its own validation scheme.

    I will give an example:

    const isNull = v(null);
    // same as
    const isNull = (x) => x === null;

    or

    const is42 = v(42);
    // same as
    const is42 = (x) => x === 42;

    Primitives are all Javascript values, with the exception of objects (including arrays) and functions. That is: undefined,null, false,true, numbers (NaN,Infinity, -Infinity including) and strings.

    Schemas out of the box

    Quartet provides pre-defined schemas for specific checks. They are in the properties of the v compiler function.

    v.any: Schema

    const checkBoolean = v(v.any);
    // same as
    const checkBoolean = () => true;

    v.array: Schema

    const checkBoolean = v(v.array);
    // same as
    const checkBoolean = (value) => Array.isArray(v);

    v.boolean: Schema

    const checkBoolean = v(v.boolean);
    // same as
    const checkBoolean = (x) => typeof x === "boolean";

    v.finite: Schema

    const checkFinite = v(v.finite);
    // same as
    const checkFinite = (x) => Number.isFinite(x);

    v.function: Schema

    const checkFunction = v(v.function);
    // same as
    const checkFunction = (x) => typeof x === "function";

    v.negative: Schema

    const checkNegative = v(v.negative);
    // same as
    const checkNegative = (x) => x < 0;

    v.never: Schema

    const checkNegative = v(v.never);
    // same as
    const checkNegative = () => false;

    v.number: Schema

    const checkNumber = v(v.number);
    // same as
    const checkNumber = (x) => typeof x === "number";

    v.positive: Schema

    const checkPositive = v(v.positive);
    // same as
    const checkPositive = (x) => x > 0;

    v.safeInteger: Schema

    const checkSafeInteger = v(v.safeInteger);
    // same as
    const checkSafeInteger = (x) => Number.isSafeInteger(x);

    v.string: Schema

    const checkString = v(v.string);
    // same as
    const checkString = (x) => typeof x === "string";

    v.symbol: Schema

    const checkSymbol = v(v.symbol);
    // same as
    const checkSymbol = (x) => typeof x === "symbol";

    Schemas created using quartet methods

    The compiler function also has methods that return schemas.

    v.and(...schemas: Schema[]): Schema

    It creates a kind of connection schemas using a logical AND (like the operator &&)

    const positiveNumberSchema = v.and(v.number, v.positive);
    const isPositiveNumber = v(positiveNumberSchema);
     
    // same as
     
    const isPositiveNumber = (x) => {
      if (typeof x !== "number") return false;
      if (x <= 0) return false;
      return true;
    };

    v.arrayOf(elemSchema: Schema): Schema

    According to the element scheme, it creates a validation scheme for an array of these elements:

    const elemSchema = v.and(v.number, v.positive);
    const arraySchema = v.arrayOf(elemSchema);
    const checkPositiveNumbersArray = v(arraySchema);
     
    // same as
     
    const checkPositiveNumbersArray = (x) => {
      if (!x || !Array.isArray(x)) return false;
      for (let i = 0; i < 0; i++) {
        const elem = x[i];
        if (typeof elem !== "number") return false;
        if (elem <= 0) return false;
      }
      return true;
    };

    v.custom(checkFunction: (x: any) => boolean, description?: string): Schema

    From the validation function, it creates a schema.

    function checkEven(x) {
      return x % 2 === 0;
    }
     
    const evenSchema = v.custom(checkEven);
    const checkPositiveEvenNumber = v(v.and(v.number, v.positive, evenSchema));
     
    // same as
     
    const checkPositiveEvenNumber = (x) => {
      if (typeof x !== "number") return false;
      if (x <= 0) return false;
      if (!checkEven(x)) return false;
      return true;
    };

    If description is passed it will be placed inside explanation if such is used.

    import { e } from "quartet";
    const isEven = (x) => x % 2 === 0;
    const evenNumbersValidator = e(
      e.arrayOf(
        e.custom(isEven, "should be even")
      )
    );
     
    evenNumbersValidator([]) // true
    evenNumbersValidator([1]) // false
    evenNumbersValidator.explanations
    // [
    //   {
    //     value: 1,
    //     schema: {
    //       type: 'Custom',
    //       description: 'should be even',
    //       innerExplanations: []
    //     },
    //     path: [ 0 ],
    //     innerExplanations: []
    //   }
    // ] 
     

    (See Advanced Quartet for more.)

    v.max(maxValue: number, isExclusive?: boolean): Schema

    By the maximum (or boundary) number returns the corresponding validation scheme.

    const checkLessOrEqualToFive = v(v.max(5));
    // same as
    const checkLessOrEqualToFive = (x) => x <= 5;
    const checkLessThanFive = v(v.max(5, true));
    // same as
    const checkLessThanFive = (x) => x < 5;

    v.maxLength(maxLength: number, isExclusive?: boolean): Schema

    By the maximum (or boundary) value of the length, returns the corresponding schema.

    const checkTwitterText = v(v.maxLength(140));
    // same as
    const checkTwitterText = (x) => x != null && x.length <= 140;
    const checkTwitterText = v({ length: v.max(140) });
    const checkSmallArray = v(v.maxLength(20, true));
    // same as
    const checkSmallArray = (x) => x != null && x.length < 140;
    const checkTwitterText = v({ length: v.max(20, true) });

    v.min(minValue: number, isExclusive?: boolean): Schema

    By the minimum (or boundary) number returns the corresponding validation scheme.

    const checkNonNegative = v(v.min(0));
    // same as
    const checkNonNegative = (x) => x >= 0;
    const checkPositive = v(v.min(0, true));
    // same as
    const checkPositive = (x) => x > 0;
    const checkPositive = v(v.positive);

    v.minLength(minLength: number, isExclusive?: number): Schema

    By the minimum (or boundary) value of the length, returns the corresponding schema.

    const checkLargeArrayOrString = v(v.minLength(1024));
    // same as
    const checkLargeArrayOrString = (x) => x != null && x.length >= 1024;
    const checkLargeArrayOrString = v({ length: v.min(1024) });
    const checkNotEmptyStringOrArray = v(v.minLength(0, true));
    // same as
    const checkNotEmptyStringOrArray = (x) => x != null && x.length > 0;
    const checkNotEmptyStringOrArray = v({ length: v.min(0, true) });

    v.not(schema: Schema): Schema

    Applies logical negation (like the ! Operator) to the passed schema. Returns the inverse schema to the passed one.

    const checkNonPositive = v(v.not(v.positive));
    const checkNot42 = v(v.not(42));
    const checkIsNotNullOrUndefined = v(v.and(v.not(null), v.not(undefined)));

    v.pair(keyValueSchema: Schema): Schema

    It's a method that returns a special kind of Schema that can be used only as a single parameter of v.arrayOf and [v.rest] prop in object schema.

    It is used to get access to index or prop name of validated value.

    keyValueSchema is a schema that should validate an object with two props key(in which index or prop name will be stored) and value(in which value will be stored)

    The main goal is to validate dictionaries.

    const validPersonsNames = ["Andrew", "Vasilina", "Bohdan", "TF"];
    const checkPhoneBook = v({
      [v.rest]: {
        key: validPersonsNames,
        value: v.string,
      },
    });
     
    checkPhoneBook({}); // will be true
    checkPhoneBook({
      Andrew: "0975017374",
      Vasilina: "23123123",
    }); // will be true
    checkPhoneBook({
      NotAnAndrew: "0975017374",
      Vasilina: "23123123",
    }); // will be false

    Example with v.arrayOf

    const isSquaresOfIndices = v.arrayOf(
      v.pair(v.custom(({ key, value }) => value === key * key))
    );
     
    isSquaresOfIndices([]); // true
    isSquaresOfIndices([0]); // true, because 0 = 0 * 0
    isSquaresOfIndices([0, 1]); // true, because 1 = 1 * 1
    isSquaresOfIndices([0, 1, 4]); // true, because 4 = 2 * 2
     
    isSquaresOfIndices([0, 1, 4, 10]); // false, because 10 !== 3 * 3
    const checkShortArrayOfStrings = v(v.and(v.arrayOf(v.string), v.minLength(10)));
     
    // the same as
     
    const checkShortArrayOfStrings = v(
      v.arrayOf(
        v.pair({
          key: v.max(9),
          value: v.string,
        })
      )
    );

    Good to mention that:

    const valueSchema = v.string;
     
    const checkPhoneBook = v({
      [v.rest]: valueSchema,
    });
     
    // is the same as
     
    const checkPhoneBook = v({
      [v.rest]: v.pair({
        value: valueSchema,
      }),
    });

    WARNING: there is only two ways to use v.pair according to its rules:

    const schemaOfArray = v.arrayOf(v.pair(...))
    const schemaWithRest = {
      // ...
      [v.rest]: v.pair(...)
    }

    Any other usage either will throw error or will have undefined behavior.

    v.test(tester: { test(x: any) => boolean }): Schema

    On an object with the test method, returns a schema that checks whether the given test method returns true on the checked value .

    Most commonly used with Regular Expressions.

    v.test(tester) === v.custom(x => tester.test(x))

    const checkIntegerNumberString = v(v.test(/[1-9]\d*/));
    // same as
    const checkIntegerNumberString = (x) => /[1-9]\d*/.test(x);

    Variant schemas

    An array of schemas acts as a connection of schemas using the logical operation OR (operator ||)

    const checkStringOrNull = v([v.string, null]);
    // same as
    const checkStringOrNull = (x) => {
      if (typeof x === "string") return true;
      if (x === null) return true;
      return false;
    };
    const checkGender = v(["male", "female"]);
    // same as
    const VALID_GENDERS = { male: true, female: true };
    const checkStringOrNull = (x) => {
      if (VALID_GENDERS[x] === true) return true;
      return false;
    };
    const checkRating = v([1, 2, 3, 4, 5]);
    // same as
    const checkRating = (x) => {
      if (x === 1) return true;
      if (x === 2) return true;
      if (x === 3) return true;
      if (x === 4) return true;
      if (x === 5) return true;
      return false;
    };

    The schema for an object is an object

    An object whose values are schemas is an object validation schema. Where the appropriate fields are validated by the appropriate schemas.

    const checkHelloWorld = v({ hello: "World" });
    // same as
    const checkHelloWorld = (x) => {
      if (x == null) return false;
      if (x.hello !== "World") return false;
      return true;
    };

    If you want to validate objects with previously unknown fields, use v.rest

    interface PhoneBook {
      [name: string]: string;
    }
    const checkPhoneBook = v({
      [v.rest]: v.string,
    });

    The scheme from the v.rest key will validate all unspecified fields.

    interface PhoneBookWithAuthorId {
      authorId: number;
      [name: string]: string;
    }
    const checkPhoneBookWithAuthorId = v({
      authorId: v.number,
      [v.rest]: v.string,
    });

    Conclusions

    Using these schemes and combining them, you can declaratively describe validation functions, and the v compiler function will create a function that imperatively checks the value against your scheme.

    Explanations

    If you need explanations of validation just use e instance instead of v instance.

    import { e as v } from "quartet";
     
    const checkPerson = v({
      name: v.string,
    });
     
    checkPerson({ name: 1 }); // false
    checkPerson.explanations;
    /*
    [
      {
        path: ["name"],
        schema: {
          type: "String",
        },
        value: 1,
      },
    ]
    */

    Predefined Instances

    There is two predefined instances of quartet:

    import { v } from "quartet"; // Zero-configured instance, without explanations
     
    import { e } from "quartet"; // Instance with explanations.

    Advanced Quartet

    // TODO: Write it!

    Ajv vs Quartet 10

    I wrote a benchmark in order to compare one of the fastest ajv validation libraries with my example from the introduction.

    const Benchmark = require("benchmark");
     
    const { v } = require("quartet");
    const validator = v({
      user: {
        id: v.number,
        name: v.string,
        age: v.number,
        gender: ["Male", "Female"],
        friendsIds: v.arrayOf(v.number),
      },
    });
     
    const Ajv = require("ajv");
    const ajv = new Ajv();
     
    const ajvValidator = ajv.compile({
      type: "object",
      required: ["user"],
      properties: {
        user: {
          type: "object",
          required: ["id", "name", "age", "gender", "friendsIds"],
          properties: {
            id: { type: "number" },
            name: { type: "string" },
            age: { type: "number" },
            gender: { type: "string", enum: ["Male", "Female"] },
            friendsIds: { type: "array", items: { type: "number" } },
          },
        },
      },
    });
     
    const positive = [
      {
        user: {
          id: 1,
          name: "Andrew",
          age: 23,
          gender: "Male",
          friendsIds: [2, 3],
        },
      },
      {
        user: {
          id: 2,
          name: "Vasilina",
          age: 20,
          gender: "Female",
          friendsIds: [1],
        },
      },
      { user: { id: 3, name: "Bohdan", age: 23, gender: "Male", friendsIds: [1] } },
      { user: { id: 4, name: "Siroja", age: 99, gender: "Male", friendsIds: [] } },
    ];
    const negative = [
      null,
      false,
      undefined,
      {},
      { user: null },
      { user: false },
      { user: undefined },
      {
        user: {
          id: "1",
          name: "Andrew",
          age: 23,
          gender: "Male",
          friendsIds: [2, 3],
        },
      },
      {
        user: {
          id: 1,
          name: undefined,
          age: 23,
          gender: "Male",
          friendsIds: [2, 3],
        },
      },
      {
        user: {
          id: 1,
          name: "Andrew",
          age: undefined,
          gender: "Male",
          friendsIds: [2, 3],
        },
      },
      {
        user: {
          id: 1,
          name: "Andrew",
          age: 23,
          gender: undefined,
          friendsIds: [2, 3],
        },
      },
      {
        user: {
          id: 1,
          name: "Andrew",
          age: 23,
          gender: "male",
          friendsIds: [2, 3],
        },
      },
      {
        user: {
          id: 1,
          name: "Andrew",
          age: 23,
          gender: "Male",
          friendsIds: undefined,
        },
      },
    ];
    const suite = new Benchmark.Suite();
    suite
      .add("ajv", function() {
        for (let i = 0; i < positive.length; i++) {
          ajvValidator(positive[i]);
        }
        for (let i = 0; i < negative.length; i++) {
          ajvValidator(negative[i]);
        }
      })
      .add("Quartet 10", function() {
        for (let i = 0; i < positive.length; i++) {
          validator(positive[i]);
        }
        for (let i = 0; i < negative.length; i++) {
          validator(negative[i]);
        }
      })
      .on("cycle", function(event) {
        console.log(String(event.target));
      })
      .on("complete", function() {
        console.log(
          this.filter("fastest")
            .map("name")
            .toString()
        );
      })
      .run();

    And the result is this:

    ajv                x 1,029,338 ops/sec ¬Ī0.79% (90 runs sampled)
    Quartet 9: Allegro x 3,727,212 ops/sec ¬Ī9.26% (66 runs sampled)
    

    Install

    npm i quartet

    DownloadsWeekly Downloads

    39

    Version

    10.2.1

    License

    MIT

    Unpacked Size

    109 kB

    Total Files

    112

    Last publish

    Collaborators

    • avatar