UNPKG

tynder

Version:

TypeScript friendly Data validator for JavaScript.

1,467 lines (1,195 loc) 40.2 kB
# Tynder ![Tynder](https://raw.githubusercontent.com/shellyln/tynder/master/docs/tynder.svg?sanitize=true) TypeScript friendly Data validator for JavaScript. Validate data in browsers, node.js back-end servers, and various language platforms by simply writing the schema once in TypeScript with extended syntax. [![npm](https://img.shields.io/npm/v/tynder.svg)](https://www.npmjs.com/package/tynder) [![GitHub release](https://img.shields.io/github/release/shellyln/tynder.svg)](https://github.com/shellyln/tynder/releases) [![Travis](https://img.shields.io/travis/shellyln/tynder/master.svg)](https://travis-ci.org/shellyln/tynder) [![GitHub forks](https://img.shields.io/github/forks/shellyln/tynder.svg?style=social&label=Fork)](https://github.com/shellyln/tynder/fork) [![GitHub stars](https://img.shields.io/github/stars/shellyln/tynder.svg?style=social&label=Star)](https://github.com/shellyln/tynder) ## Features * Define the **schema with TypeScript-like DSL**. * **Validate** data against the defined schema. * End user friendly **custom validation error message**. * Create subset by **cherrypicking** fields from original data with the defined schema. * Apply the **patch** data to the original data. * Generate type definition or schema files using CLI / API. * TypeScript * JSON Schema * C# (experimental) * Protocol Buffers 3 (experimental) * GraphQL (experimental) ![write-once-use-anywhere](https://shellyln.github.io/tynder/assets/image/write-once-v3.svg) ------------ ## Table of contents * [Get started](#get-started) * [Playground](#playground) * [Install](#install) * [Define schema with TypeScript-like DSL](#define-schema-with-typescript-like-dsl) * [Load pre-compiled schema and type definitions](#load-pre-compiled-schema-and-type-definitions) * [Define schema with functional API](#define-schema-with-functional-api) * [DSL syntax](#dsl-syntax) * [Customize error messages](#customize-error-messages) * [CLI subcommands and options](#cli-subcommands-and-options) * [Limitations](#limitations) * [License](#license) ------------ ## Get started * [tynder-express-react-ts-esm-quickstart](https://github.com/shellyln/tynder-express-react-ts-esm-quickstart) * A boilerplate for React client + Express server project using Tynder data validation library. * [Tynder Schema Converter Chrome Extension](https://github.com/shellyln/tynder-chrome-extension) ## Playground * [TypeScript (Tynder DSL) → JSON Schema | C# | GraphQL | Protobuf Converter](https://shellyln.github.io/tynder/playground.html) * Convert schema from `Tynder DSL` to JSON Schema, C#, GraphQL and Protobuf. * [TypeScript (Tynder DSL) Schema Validator](https://shellyln.github.io/tynder/playground2.html) * Validate data against the schema. ## Install ```sh npm install --save tynder ``` > NOTICE: > Use with `webpack >= 5` > > If you get the error: > > ``` > Module not found: Error: Can't resolve '(importing/path/to/filename)' > in '(path/to/node_modules/path/to/dirname)' > Did you mean '(filename).js'?` > ``` > > Add following setting to your `webpack.config.js`. > > ```js > { > test: /\.m?js/, > resolve: { > fullySpecified: false, > }, > }, > ``` > > On `webpack >= 5`, the extension in the request is mandatory for it to be fully specified > if the origin is a '*.mjs' file or a '*.js' file where the package.json contains '"type": "module"'. > NOTICE: > To use without webpack on Node.js, enabling ES Modules. > * Add flags: > * ```bash > node --experimental-modules \ > --es-module-specifier-resolution=node \ > --experimental-json-modules \ > app.mjs > ``` > > * Use `import` statement: > * ```ts > import { ValidationContext } from 'tynder/modules/types'; > import { deserializeFromObject } from 'tynder/modules/serializer'; > import { validate, > getType } from 'tynder/modules/validator'; > ``` > * Add package.json `{ "type": "module" }` or `{ "type": "commonjs" }` to your source directories. > > See [tynder-express-react-ts-esm-quickstart](https://github.com/shellyln/tynder-express-react-ts-esm-quickstart) and > [Node.js Documentation - ECMAScript Modules](https://nodejs.org/api/esm.html). ## Define schema with TypeScript-like DSL ### Schema: ```ts /// @tynder-external RegExp, Date, Map, Set /** doc comment */ export type Foo = string | number; type Boo = @range(-1, 1) number; /** doc comment */ interface Bar { /** doc comment */ a?: string; // Optional field /** doc comment */ b: Foo[] | null; // Union type c: string[3..5]; // Repeated type (with quantity) d: (number | string)[..10]; // Complex repeated type (with quantity) e: Array<number | string, 4..>; // Complex repeated type (with quantity) f: Array<Array<Foo | string>>; // Complex repeated type (nested) g: [string, number], // Sequence type h: ['zzz', ...<string | 999, 3..5>, number], // Sequence type (with quantity) } interface Baz { i: {x: number, y: number, z: 'zzz'} | number; // Union type j: {x: number} & ({y: number} & {z: number}); // Intersection type k: ({x: number, y: number, z: 'zzz'} - {z: 'zzz'}) | number; // Subtraction type } /** doc comment */ @msgId('M1111') // Custom error message id export interface FooBar extends Bar, Baz { /** doc comment */ @range(-10, 10) l: number; // Ranged value (number) @minValue(-10) @maxValue(10) m: number; // Ranged value n: @range(-10, 10) number[]; // Array of ranged value @greaterThan(-10) @lessThan(10) o: number; // Ranged value p: integer; // Integer value @range('AAA', 'FFF') q: string; // Ranged value (string) @match(/^.+$/) r: string; // Pattern matched value s: Foo; // Refer a defined type @msgId('M1234') t: number; // Custom error message id @msg({ required: '"%{name}" of "%{parentType}" is required.', typeUnmatched: '"%{name}" of "%{parentType}" should be "%{expectedType}".', }) u: number; // Custom error message @msg('"%{name}" of "%{parentType}" is not valid.') v: number; // Custom error message } // line comment /* block comment */ ``` Default file extension is `*.tss`. ### Compile using CLI commands: ```sh # Compile schema and output as JSON files. tynder compile --indir path/to/schema/tynder --outdir path/to/schema/_compiled # Compile schema and output as JavaScript|TypeScript files. tynder compile-as-ts --indir path/to/schema/tynder --outdir path/to/schema/_compiled # Compile schema and generate TypeScript type definition files. tynder gen-ts --indir path/to/schema/tynder --outdir path/to/typescript-src # Compile schema and generate JSON Schema files. tynder gen-json-schema --indir path/to/schema/tynder --outdir path/to/schema/json-schema # Compile schema and generate JSON Schema as JavaScript|TypeScript files. tynder gen-json-schema-as-ts --indir path/to/schema/tynder --outdir path/to/schema/json-schema # Compile schema and generate C# type definition files. tynder gen-csharp --indir path/to/schema/tynder --outdir path/to/schema/csharp # Compile schema and generate Protocol Buffers 3 type definition files. tynder gen-proto3 --indir path/to/schema/tynder --outdir path/to/schema/proto3 # Compile schema and generate GraphQL type definition files. tynder gen-graphql --indir path/to/schema/tynder --outdir path/to/schema/graphql ``` ### Compile using API: ```ts import { compile } from 'tynder/modules/compiler'; export default const mySchema = compile(` type Foo = string; interface A { @maxLength(4) a: Foo; z?: boolean; } `); ``` ### Validating: ```ts import { validate, getType } from 'tynder/modules/validator'; import { ValidationContext } from 'tynder/modules/types'; import default as mySchema from './myschema'; const validated1 = validate({ a: 'x', b: 3, }, getType(mySchema, 'A')); // {value: {a: 'x', b: 3}} const validated2 = validate({ aa: 'x', b: 3, }, getType(mySchema, 'A')); // null const ctx3: Partial<ValidationContext> = { // To receive the error messages, define the context as a variable. checkAll: true, // (optional) Set to true to continue validation after the first error. noAdditionalProps: true, // (optional) Do not allow implicit additional properties. schema: mySchema, // (optional) Pass "schema" to check for recursive types. }; const validated3 = validate({ aa: 'x', b: 3, }, getType(mySchema, 'A'), ctx3); if (validated3 === null) { console.log(JSON.stringify( ctx3.errors, // error messages null, 2)); } ``` ### Cherrypicking and patching: ```ts import { getType } from 'tynder/modules/validator'; import { pick, patch } from 'tynder/modules/picker'; import { ValidationContext } from 'tynder/modules/types'; import * as op from 'tynder/modules/operators'; import default as mySchema from './myschema'; const original = { a: 'x', b: 3, }; const needleType = op.picked(getType(mySchema, 'A'), 'a'); try { const needle1 = pick(original, needleType); // {a: 'x'} const unknownInput1: unknown = { // Edit the needle data ...needle1, a: 'y', q: 1234, }; const changed1 = patch(original, unknownInput1, needleType); // {a: 'y', b: 3} } catch (e) { console.log(e.message); console.log(e.ctx?.errors); } try { const needle2 = pick(original, needleType); // {a: 'x'} const unknownInput2: unknown = { // Edit the needle data ...needle2, a: 'yyyyy', q: 1234, }; const changed1 = patch(original, unknownInput2, needleType); // Throws an error } catch (e) { console.log(e.message); console.log(e.ctx?.errors); } try { const ctx3: Partial<ValidationContext> = { // To receive the error messages, define the context as a variable. checkAll: true, // (optional) Set to true to continue validation after the first error. schema: mySchema, // (optional) Pass "schema" to check for recursive types. }; const needle3 = pick({ aa: 'x', b: 3, }, needleType, ctx3); // Throws an error } catch (e) { console.log(e.message); console.log(e.ctx?.errors); } ``` ## Load pre-compiled schema and type definitions ### From object (import) ```ts ... import { deserializeFromObject } from 'tynder/modules/lib/serializer'; import { Foo, A } from './path/to/schema-types/my-schema'; // type definitions (.d.ts) import mySchema_, { Schema as MySchema } from './path/to/schema-compiled/my-schema'; // pre-compiled schema (.ts) // `MySchema` is auto generated string const enum. const mySchema = deserializeFromObject(mySchema_); const unknownInput: unknown = {a: 'x'}; const validated = validate<A>(unknownInput, getType(mySchema, MySchema.A)); if (validated) { const validatedInput = validated.value; // validatedInput is type-safe ... } ``` ### From object (require JSON file) ```ts ... import { deserializeFromObject } from 'tynder/modules/lib/serializer'; import { Foo, A } from './path/to/schema-types/my-schema'; // type definitions (.d.ts) // import { createRequireFromPath } from 'module'; // import { fileURLToPath } from 'url'; // const require = createRequireFromPath(fileURLToPath(import.meta.url)); const mySchema = deserializeFromObject( require('./path/to/schema-compiled/my-schema.json')); // pre-compiled schema (.json) const unknownInput: unknown = {a: 'x'}; const validated = validate<A>(unknownInput, getType(mySchema, 'A')); if (validated) { const validatedInput = validated.value; // validatedInput is type-safe ... } ``` or ```ts ... import { deserializeFromObject } from 'tynder/modules/lib/serializer'; import { Foo, A } from './path/to/schema-types/my-schema'; // type definitions (.d.ts) import mySchemaJson from './path/to/schema-compiled/my-schema.json'; // pre-compiled schema (.json) const mySchema = deserializeFromObject(mySchemaJson); const unknownInput: unknown = {a: 'x'}; const validated = validate<A>(unknownInput, getType(mySchema, 'A')); if (validated) { const validatedInput = validated.value; // validatedInput is type-safe ... } ``` ### From text ```ts ... import { deserialize } from 'tynder/modules/lib/serializer'; import { Foo, A } from './path/to/schema-types/my-schema'; // type definitions (.d.ts) import * as fs from 'fs'; const mySchema = deserialize( fs.readFileSync('./path/to/compiled/my-schema.json', 'utf8')); // pre-compiled schema (.json) const unknownInput: unknown = {a: 'x'}; const validated = validate<A>(unknownInput, getType(mySchema, 'A')); if (validated) { const validatedInput = validated.value; // validatedInput is type-safe ... } ``` ### Type-safe Cherrypicking and patching: ```ts // Load pre-compiled schema and type definitions ... interface Store { baz: A; } const store: Store = { baz: { a: 'x', z: false, } }; const needleType = op.picked(getType(mySchema, 'A'), 'a'); try { const needle = pick(store.baz, needleType); // {a: 'x'} // `needle` is RecursivePartial<A> const unknownInput: unknown = { // Edit the needle data ...needle, a: 'y', q: 1234, }; store.baz = patch(store.baz, unknownInput, needleType); // {a: 'y', z: false} } catch (e) { console.log(e.message); console.log(e.ctx?.errors); } ``` ### Type guards ```ts import { isType, getType } from 'tynder/modules/validator'; ... const unknownInput: unknown = {a: 'x'}; if (isType<A>(unknownInput, getType(mySchema, 'A'), ctx) && unknownInput.a.length > 0) { console.log(`ok: ${unknownInput.a.length}`); } else { console.log('ng'); } ``` ```ts import { assertType, getType } from 'tynder/modules/validator'; ... const unknownInput: unknown = {a: 'x'}; try { assertType<A>(unknownInput, getType(mySchema, 'A'), ctx); console.log(`ok: ${unknownInput.a.length}`); } catch (e) { console.log('ng'); } ``` ## Define schema with functional API ```ts import { picked, omit, partial, intersect, oneOf, subtract, primitive, regexpPatternStringType, primitiveValue, optional, repeated, sequenceOf, spread, enumType, objectType, derived, symlinkType, withName, withTypeName, withDocComment, withRange, withMinValue, withMaxValue, withGreaterThan, withLessThan, withMinLength, withMaxLength, withMatch, withStereotype, withStereotype, withForceCast, withRecordType, withMeta, withMsg as $$, withMsgId as $ } from 'tynder/modules/operators'; const myType = oneOf( derived( objectType( ['a', 10], ['b', optional(20)], ['c', $('MyType-c')( optional('aaa'))], ['d', sequenceOf( 10, 20, spread(primitive('string'), {min: 3, max: 10}), 50)], ), objectType( ['e', optional(primitive('string'))], ['f', primitive('string?')], ['g', repeated('string', {min: 3, max: 10})], [[/^[a-z][0-9]$/], optional(primitive('string'))], ), intersect( objectType( ['x', 10], ['y', 10], ['p', 10], ), objectType( ['x', 10], ['y', 10], ['q', 10], )), subtract( objectType( ['w', 10], ['z', 10], ), objectType( ['w', 10], ))), 10, 20, 30, primitive('string'), primitiveValue(50), ); /* Equivalent to following type definition: interface P { e?: string; f?: string; g: string[3..10]; [propName: /^[a-z][0-9]$/]?: string; } type Q = { x: 10, y: 10, p: 10, } & { x: 10, y: 10, q: 10, }; type R = { w: 10, z: 10, } - { w: 10, }; interface S extends P, Q, R { a: 10; b?: 20; @msgId('MyType-c') c: 'aaa'; d: [10, 20, ...<string, 3..10>, 50]; } type MyType = S | 10 | 20 | 30 | string | 50; */ const validated1 = validate({...}, myType); ``` ## DSL syntax ### Type ```ts type Foo = string; type Bar = string[] | 10 | {a: boolean} | [number, string]; ``` ### Interface #### Named interface ```ts interface Foo { a: string; // Separators `;` and `,` are both allowed. b?: number; } interface Bar { c: boolean; } interface Baz extends Foo, Bar { d: string[]; } ``` #### Unnamed literal interface ```ts type A = { a: string, // Separators `;` and `,` are both allowed. b?: number, }; ``` #### Optional member ```ts interface A { b?: number; // optional member }; ``` #### Additional properties ```ts type X = {a: string, b: number}; interface A { // Additional properties (Error if `propName` is unmatched) [propName: string | number | /^[a-z][0-9]+$/]: number; }; interface B { // Optional additional properties (Check type if propName matches) // -> Implicit additional properties are allowed // even if `ctx.noAdditionalProps` is true. [propName: string | number | /^[a-z][0-9]+$/]?: number; }; interface C { // `propName` can be any name [p: string]: X; }; interface D { // Error if app `propName`s are unmatched [propName1: /^[a-z][0-9]+$/]: number; [propName2: number]: number; }; interface E { // If optional additional properties definition(s) exist, // implicit additional properties are allowed // even if `ctx.noAdditionalProps` is true. [propName1: /^[a-z][0-9]+$/]: number; [propName2: number]: number; [propName3: /^[A-F]+$/]?: number; }; ``` Only `string`, `number`, and `RegExp` are allowed for the `propName` type. ### Type decoration #### Decorate to interface member ```ts interface A { @range(-10, 10) @msgId('M1234') a: number; } ``` #### Decorate to type component ```ts type A = @range(-10, -1) number | @range(1, 10) number; interface B { b: @range(-10, -1) number | @range(1, 10) number; } ``` * `@range(minValue: number | string, maxValue: number | string)` * Check value range. * minValue <= data <= maxValue * `@minValue(minValue: number | string)` * Check value range. * minValue <= data * `@maxValue(maxValue: number | string)` * Check value range. * data <= maxValue * `@greaterThan(minValue: number | string)` * Check value range. * minValue < data * `@lessThan(maxValue: number | string)` * Check value range. * data < maxValue * `@minLength(minLength: number)` * Check value range. * minLength <= data.length * `@maxLength(maxLength: number)` * Check value range. * data.length <= maxLength * `@match(pattern: RegExp)` * Check value text pattern. * RegExp flags are allowed. * e.g.: `/^[\u{3000}-\u{301C}]+$/u` * pattern.test(data) * `@stereotype(stereotype: string)` * Perform custom validation. * > **WARNING**: In the JSON schema output, this is stripped. * `@constraint(constraintName: string, args: any)` * Perform custom constraint. * > **WARNING**: In the JSON schema output, this is stripped. * `@constraint('unique', fields?: string[])` * Check unique. * `@constraint('unique-non-null', fields?: string[])` * Check unique (null field is always unique). ```ts interface A { @constraint('unique') a: string[]; } interface B { @constraint('unique', ['p', 'r']) b: {p: string, q: string, r: string}[]; } ``` * `@forceCast` * Validate after forcibly casting to the assertion's type. * > **WARNING**: In the JSON schema output, this is stripped. * `@recordType` * If the decorated member field of object is validated, the union type is determined. * Use to receive reasonable validation error messages. ```ts interface Foo { @recordType kind: 'foo'; ... } interface Bar { @recordType kind: 'bar'; ... } type FooBar = Foo | Bar; // If data {kind: 'foo', ...} is passed, // the union type will be determined as `Foo`. ``` * `@meta` * User defined custom properties (meta informations). * Output to the compiled schema. ```ts @meta({ objectId: '0ffc31e6-f534-4e49-b6d7-a3ec21f49637' }) interface A { @meta({ fieldId: '82bd5832-c399-4d4c-8bc4-b76a95823ebf', fieldType: 'checkbox', }) a: ('foo' | 'bar' | 'baz')[]; } ``` * `@msg(messages: string | ErrorMessages)` * Set custom error message. * `@msgId(messageId: string)` * Set custom error message id. ##### Date / Datetime stereotypes ```ts ... import { stereotypes as dateStereotypes } from 'tynder/modules/stereotypes/date'; const schema = compile(` interface Foo { @stereotype('date') @range('=today first-date-of-mo', '=today last-date-of-mo') a: string; @stereotype('date') @range('2020-01-01', '2030-12-31') b: string; @stereotype('date') @range('2020-01-01', '=today +2yr @12mo @31day') c: string; } `); const ty = getType(schema, 'Foo'); const ctx: Partial<ValidationContext> = { checkAll: true, stereotypes: new Map([ ...dateStereotypes, ]), }; const d = (new Date()).toISOString().slice(0, 10); const z = validate<any>({ a: d, b: '2020-01-01', c: d, }, ty, ctx); ``` ###### Stereotypes * `date` * date (UTC timezone) * `lcdate` * date (local timezone) * `datetime` * datetime (UTC timezone) * `lcdatetime` * datetime (local timezone) ###### Formula syntax ``` Expression = ISODateAndDatetime | ("=" , DateTimeFormula , {whitespace, DateTimeFormula}) ; DateTimeFormula = ISODateAndDatetime | ("current" | "now") | "today" ("@" | "+" | "-") , NaturalNumber , ("yr" | "mo" | ("days" | "day") | "hr" | "min" | "sec" | "ms") | "first-date-of-yr" | "last-date-of-yr" | "first-date-of-mo" | "last-date-of-mo" | "first-date-of-fy", "(", NaturalNumber1To12, ")" ; ``` ###### Formula examples * This month (date) * `@range('=today first-date-of-mo', '=today last-date-of-mo')` * This month (datetime) * `@minValue('=today first-date-of-mo') @lessThan('=today last-date-of-mo +1day')` * Next month (date) * `@range('=today first-date-of-mo +1mo', '=today @1day +1mo last-date-of-mo')` * Next month (datetime) * `@minValue('=today first-date-of-mo +1mo') @lessThan('=today @1day +1mo last-date-of-mo +1day')` * This year (date) * `@range('=today first-date-of-yr', '=today last-date-of-yr')` * This year (datetime) * `@minValue('=today first-date-of-yr') @lessThan('=today last-date-of-yr +1day')` * Next year (date) * `@range('=today first-date-of-yr +1yr', '=today @1day +1yr last-date-of-yr')` * Next year (datetime) * `@minValue('=today first-date-of-yr +1yr') @lessThan('=today @1day +1yr last-date-of-yr +1day')` * This fiscal year (date) * `@range('=today first-date-of-fy(9)', '=today first-date-of-fy(9) +1yr -1day')` * Fiscal year beginning in September * This fiscal year (datetime) * `@minValue('=today first-date-of-fy(9)') @lessThan('=today first-date-of-fy(9) +1yr')` * Fiscal year beginning in September * Next fiscal year (date) * `@range('=today first-date-of-fy(9) +1yr', '=today first-date-of-fy(9) +2yr -1day')` * Fiscal year beginning in September * Next fiscal year (datetime) * `@minValue('=today first-date-of-fy(9) +1yr') @lessThan('=today first-date-of-fy(9) +2yr')` * Fiscal year beginning in September ##### Unique constraint ```ts ... import { constraints as uniqueConstraints } from 'tynder/modules/constraints/unique'; const schema = compile(` interface A { @constraint('unique') a: string[]; } interface B { @constraint('unique', ['p', 'r']) b: {p: string, q: string, r: string}[]; } `); { const ty = getType(schema, 'A'); const ctx: Partial<ValidationContext> = { checkAll: true, customConstraints: new Map([ ...uniqueConstraints, ]), }; const z = validate<any>({a: [ 'x', 'y', 'x', // duplicated ]}, ty, ctx); } { const ty = getType(schema, 'B'); const ctx: Partial<ValidationContext> = { checkAll: true, customConstraints: new Map([ ...uniqueConstraints, ]), }; const z = validate<any>({a: [ {p: '1', q: '2', r: '3'}, {p: '2', q: '3', r: '4'}, {p: '1', q: '4', r: '3'}, // duplicated ]}, ty, ctx); } ``` ### Enum ```ts enum Foo { A, // 0 B, // 1 C, // 2 } enum Bar { A = 1, // 1 B, // 2 C = 100, // 100 } enum Baz { A = 'AAA', B = 'BBB', C = 'CCC', } const enum Qux { A, } ``` ### Primitive types ```ts /** Primitive types */ type A = number | integer | bigint | string | boolean; /** Null-like types */ type B = null | undefined; /** Placeholder types */ type C = any | unknown | never; ``` ### Value types See `Literals > Type literals` section. ### Array type component (Repeated type component) #### Simple array type ```ts type A = string[]; ``` #### Complex array type ```ts type A = Array<boolean|number|boolean[]|{a: string}|'a'>; ``` #### Simple array type with quantity assertion ```ts type A = string[10..20]; // 10 <= data.length <= 20 type B = string[10..]; // 10 <= data.length type C = string[..20]; // data.length <= 20 type D = string[10]; // data.length === 10 ``` #### Complex array type with quantity assertion ```ts type A = Array<boolean, 10..20>; // 10 <= data.length <= 20 type B = Array<boolean, 10..>; // 10 <= data.length type C = Array<boolean, ..20>; // data.length <= 20 type D = Array<boolean, 10>; // data.length === 10 ``` ### Sequence type component (Tuple type component) #### Fixed length ```ts type A = [string, number, 10, 20, 'a']; ``` #### Flex length ```ts type A = [string, number?, boolean?, string?]; // Zero or once type B = [string, ...<number>, ...<boolean>, ...<string>]; // Zero or more type C = [string, ...<number, 10..20>, ...<boolean, 10..>, ...<string, ..20>]; // With quantity assertion ``` > **WARNING**: In the JSON schema output, this translates into a simplified array assertion. ### Referencing other interface members ```ts interface Foo { @match(/^[A-Za-z]+$/) name: string; @match(/^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/) email: string; } interface Bar { foo: Foo } interface User { userName: Foo.name; primaryEmail: Foo.email; primaryAliasName: Bar.foo.name; aliasNames: Bar.foo.name[]; } ``` > NOTE: > * This syntax is incompatible with TypeScript. > * Generated TypeScript type definition is `userName: Foo['name'];`. > * Tynder compiler does not allow `userName: Foo['name'];`. ### Type operators * `P & Q` * Intersection type * Result type has all the members of P and Q. * `P | Q` * Union type * Match to P or Q type. * `P - Q` * Subtraction type * Result type has the members of P that is NOT exist in Q. * `Pick<T,K>` * e.g. `Pick<Foo, 'a' | 'b' | 'c'>` * Picked type * Result type has the members of T that is exist in K. * `Omit<T,K>` * e.g. `Omit<Foo, 'a' | 'b' | 'c'>` * Picked type * Result type has the members of T that is NOT exist in K. * `Partial<T>` * All the member of result type are optioonal. * `Partial<{a: string}>` is equivalent to `{a?: string}`. ### Export ```ts export type Foo = string; export interface Bar { a: string; } export enum Baz { A, } export const enum Qux { A, } ``` ### Import This statement is passed through to the generated codes. ```ts import from 'foo'; import * as foo from 'foo'; import {a, b as bb} from 'foo'; ``` ### Declared types ```ts declare type A = string; declare interface B {} declare enum C {} declare const enum D {} export declare type E = string; export declare interface F {} export declare enum G {} export declare const enum H {} ``` ### Declared variables This statement is passed through to the generated codes. ```ts declare var a: number; declare let b: number; declare const c: number; export declare var d: number; export declare let e: number; export declare const f: number; ``` ### External This statement is removed from the generated code. #### Untyped external statement Define the external (ambient) symbols as `any` type. ```ts external P, Q, R; ``` or ```ts /// @tynder-external P, Q, R ``` or ```ts /* @tynder-external P, Q, R */ ``` #### Typed external statement ```ts external P: string[], Q: P | string, R: {a: string}[]; ``` or ```ts /// @tynder-external P: string[], Q: P | string, R: {a: string}[] ``` or ```ts /* @tynder-external P: string[], Q: P | string, R: {a: string}[] */ ``` ### Pass-through code block This comment body is passed through to the generated codes. ```ts // Nominal type declare const phoneNumberString: unique symbol; /* @tynder-pass-through export type PhoneNumberString = string & { [phoneNumberString]: never }; */ external PhoneNumberString: @match(/^[0-9]{2,4}-[0-9]{1,4}-[0-9]{4}$/) string; ``` ### Comments ```ts // ↓↓↓ directive line comment ↓↓↓ // @tynder-external P, Q, R /// @tynder-external S, T // ↓↓↓ directive block comment ↓↓↓ /* @tynder-external U, V */ /** doc comment */ type Foo = string | number; /** doc comment */ interface Bar { /** doc comment */ a?: string; } /** doc comment */ enum Baz { /** doc comment */ A, } // line comment # line comment /* block comment */ /* block comment */ ``` Doc comments are preserved. ### Literals #### Type literals ```ts type A = 'a' | "b" | `c` | 20 | -10 | -0.12 | -9.3+8e | -10_000_000.999_999 | 0xff | 0o77 | 0b11 | +Infinity | -Infinity | -10n | 0n | 123n | true | false | null | undefined | {a: string, b: 'aaa'} | [10, string]; ``` #### Value literals ```ts type A = @match(/^.+$/) string; // RegExp type B = @range(10, 20) number; // number type C = @range('a', 'b') string; // string type D = @msg({ required: '...', typeUnmatched: '...' }) number; // object ``` ### Directives ```ts /// @tynder-external P, Q, R ``` * `@tynder-external` _type_ [, ...] * Declare external types as `any`. ```ts /* @tynder-pass-through export type PhoneNumberString = string & { [phoneNumberString]: never }; */ ``` * `@tynder-pass-through` _body_ * This comment body is passed through to the generated codes. ### Generics Generics actual parameters are removed. #### DSL: ```ts /// @tynder-external Map, Set interface Foo { a: Map<string, number>; // validator treats it as `any`. b: Set<string>; // validator treats it as `any`. } ``` #### TypeScript generated type definition: ```ts interface Foo { a: Map; // generics actual parameters are removed. b: Set; // generics actual parameters are removed. } ``` > NOTE: Generic interfaces and generic types cannot be defined. * e.g. ```ts interface Foo<T> { // It is not possible. a: T; } ``` ## Customize error messages ### Customize message of items ```ts @msgId('M1111') // Custom error message id export interface Foo { @msgId('M1234') s: number; // Custom error message id @msg({ required: '"%{name}" of "%{parentType}" is required.', typeUnmatched: '"%{name}" of "%{parentType}" should be "%{expectedType}".', }) t: number; // Custom error message @msg('"%{name}" of "%{parentType}" is not valid.') u: number; // Custom error message } ``` ### Default error messages ```ts export const defaultMessages: ErrorMessages = { invalidDefinition: '"%{name}" of "%{parentType}" type definition is invalid.', required: '"%{name}" of "%{parentType}" is required.', typeUnmatched: '"%{name}" of "%{parentType}" should be type "%{expectedType}".', additionalPropUnmatched: '"%{addtionalProps}" of "%{parentType}" are not matched to additional property patterns.', repeatQtyUnmatched: '"%{name}" of "%{parentType}" should repeat %{repeatQty} times.', sequenceUnmatched: '"%{name}" of "%{parentType}" sequence is not matched', valueRangeUnmatched: '"%{name}" of "%{parentType}" value should be in the range %{minValue} to %{maxValue}.', valuePatternUnmatched: '"%{name}" of "%{parentType}" value should be matched to pattern "%{pattern}"', valueLengthUnmatched: '"%{name}" of "%{parentType}" length should be in the range %{minLength} to %{maxLength}.', valueUnmatched: '"%{name}" of "%{parentType}" value should be "%{expectedValue}".', }; ``` ### Change default messages ```ts import { compile } from 'tynder/modules/compiler'; import { getType } from 'tynder/modules/validator'; import { pick, merge } from 'tynder/modules/picker'; import { ValidationContext } from 'tynder/modules/types'; export default const mySchema = compile(` interface A { @msg({ required: 'Don\'t forget "%{name}"!.', }) a: string; } `); const ctx: Partial<ValidationContext> = { checkAll: true, noAdditionalProps: true, schema: mySchema, errorMessages: { required: '%{name}" is requred!', }, }; const validated = validate({ aa: 'x', }, getType(mySchema, 'A'), ctx3); if (validated3 === null) { console.log(JSON.stringify( ctx3.errors, // error messages null, 2)); } ``` Precedence is "`Default messages` < `ctx.errorMessages` < `@msg()`". ### Keyword substitutions * `%{expectedType}` * `%{type}` * `%{expectedValue}` * `%{value}` * `%{repeatQty}` * `%{minValue}` * `%{maxValue}` * `%{pattern}` * `%{minLength}` * `%{maxLength}` * `%{name}` * `%{parentType}` * `%{dataPath}` * `%{addtionalProps}` ## CLI subcommands and options ``` Usage: tynder subcommand options... Subcommands: help Show this help. compile Compile schema and output as JSON files. * default input file extension is *.tss * default output file extension is *.json compile-as-ts Compile schema and output as JavaScript|TypeScript files. * default input file extension is *.tss * default output file extension is *.ts Generated code is: const schema = {...}; export default schema; gen-ts Compile schema and generate TypeScript type definition files. * default input file extension is *.tss * default output file extension is *.d.ts gen-json-schema Compile schema and generate 'JSON Schema' files. * default input file extension is *.tss * default output file extension is *.json gen-json-schema-as-ts Compile schema and generate 'JSON Schema' as JavaScript|TypeScript files. * default input file extension is *.tss * default output file extension is *.ts Generated code is: const schema = {...}; export default schema; gen-csharp Compile schema and generate 'C#' type definition files. * default input file extension is *.tss * default output file extension is *.cs gen-proto3 Compile schema and generate 'Protocol Buffers 3' type definition files. * default input file extension is *.tss * default output file extension is *.proto gen-graphql Compile schema and generate 'GraphQL' type definition files. * default input file extension is *.tss * default output file extension is *.graphql Options: --indir dirname Input directory --outdir dirname Output directory --inext fileExtensionName Input files' extension --outext fileExtensionName Output files' extension ``` Example: ```sh tynder compile --indir path/to/schema/tynder --outdir path/to/schema/_compiled ``` ## Limitations * Generics actual parameters are removed. * Except `Array<T,quantity?>`, [`Pick<T,K>`](https://www.typescriptlang.org/docs/handbook/utility-types.html#picktk), [`Omit<T,K>`](https://www.typescriptlang.org/docs/handbook/utility-types.html#omittk) and [`Partial<T>`](https://www.typescriptlang.org/docs/handbook/utility-types.html#partialt). ## License [ISC](https://github.com/shellyln/tynder/blob/master/LICENSE.md) Copyright (c) 2019-2020 Shellyl_N and Authors.