From 9a94ceb5cd148bc9d608dc782a9b9213f2a2d073 Mon Sep 17 00:00:00 2001 From: Matt Bishop Date: Tue, 15 Aug 2017 17:24:52 -0700 Subject: [PATCH] Add type definition for js.spec --- types/js.spec/index.d.ts | 330 +++++++++++++++++++++++++++++++++ types/js.spec/js.spec-tests.ts | 87 +++++++++ types/js.spec/tsconfig.json | 20 ++ types/js.spec/tslint.json | 1 + 4 files changed, 438 insertions(+) create mode 100644 types/js.spec/index.d.ts create mode 100644 types/js.spec/js.spec-tests.ts create mode 100644 types/js.spec/tsconfig.json create mode 100644 types/js.spec/tslint.json diff --git a/types/js.spec/index.d.ts b/types/js.spec/index.d.ts new file mode 100644 index 0000000000..b33a0fd475 --- /dev/null +++ b/types/js.spec/index.d.ts @@ -0,0 +1,330 @@ +// Type definitions for js.spec 1.0 +// Project: http://js-spec.online +// Definitions by: Matt Bishop +// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped +// TypeScript Version: 2.2 + +/** + * A Spec provides a predicate that can test a value for conformance. + */ +export interface Spec { + /** + * The name of the spec, displayed in explain() results. + */ + readonly name: string; + + /** + * Data necessary to check values for conformity. + */ + readonly options: object; + + /** + * Returns the conformed value to this spec. + * @param value the value to test for conformance + * @returns {symbol.invalid} if the value does not conform to the spec, or the value if it does. + */ + conform(value: any): any; + + /** + * Explain why a value does not conform to this spec. + * @param value the value to examine + * @returns {Problem[]} list of problems or null if none + */ + explain(value: any): Problem[]; +} + +/** + * A Spec with a boolean conform function. Test the value and return true if it conforms. + */ +export interface Predicate extends Spec { + (value: any): boolean; +} + +/** + * An explanation of why a part of a value does not conform to a spec. + */ +export interface Problem { + /** + * The path to the value. + */ + readonly path: string[]; + + /** + * Pth to he Spec that applies. + */ + readonly via: string[]; + + /** + * The value associated with the problem. + */ + readonly value: any; + + /** + * A predicate function to test new values for conformance. + */ + readonly predicate: Predicate; +} + +/** + * Given a Spec, tests the value for confomrance. If it passes, then returns true. + * @param {Spec} spec the spec to test with + * @param value the value to test + * @returns {boolean} true if valid + */ +export function valid(spec: Spec, value: any): boolean; + +/** + * Returns the conformed value to this spec. + * @param {Spec} spec the spec to test with + * @param value the value to test + * @returns {symbol.invalid} if the value does not conform to the spec, or the conformed value if it does. + */ +export function conform(spec: Spec, value: any): any; + +/** + * Like explain(), but returns Problems array. + * @param {Spec} spec the spec to test with + * @param value the value to test + * @returns {Problem[]} list of problems or null if none + */ +export function explainData(spec: Spec, value: any): Problem[]; + +/** + * Prints, to the console, reasons why the value did not conform to this spec. + * @param {Spec} spec the spec to test with + * @param value the value to test + */ +export function explain(spec: Spec, value: any): void; + +/** + * Returns a multiline string with reasons why the value did not conform to this spec. + * @param {Spec} spec the spec to test with + * @param value the value to test + */ +export function explainStr(spec: Spec, value: any): string; + +/** + * Tests if a value conforms to a spec, and if not, throws an Error. + * @param {Spec} spec the spec to test with + * @param value the value to test + */ +export function assert(spec: Spec, value: any): void; + +export namespace symbol { + /** + * Returned by conform() to indicate a value does not conform to a spec. + */ + const invalid: symbol; + + /** + * Used as an option in collection() to specify the size of a collection. + */ + const count: symbol; + + /** + * Used as an option in collection() to specify the maximum size of a collection. + */ + const maxCount: symbol; + + /** + * Used as an option in collection() to specify the minimum size of a collection. + */ + const minCount: symbol; + + /** + * Used as an option in map() to specify a key spec that is optional. + */ + const optional: symbol; +} + +export namespace spec { + /** + * Predicate function definition to describe non-spec predicate functions. + */ + type PredFn = (value: any) => boolean; + + /** + * Defins an input to a spec. Can be a Spec instance or a predicate function. + */ + type SpecInput = PredFn | Spec; + + /** + * Data must conform to every provided spec. + * @param {string} name the name of the spec + * @param {spec.SpecInput} specs the array of specs that must all match + * @returns {Spec} the constructed Spec + */ + function and(name: string, ...specs: SpecInput[]): Spec; + + /** + * Data must conform to at least one provided spec. The order in which they are validated is not defined. + * The conform() function returns matched branches along with input data. + * @param {string} name the name of the spec + * @param {object} alts map of alternative keys with their respective SpecInputs + * @returns {Spec} the constructed Spec + */ + function or(name: string, alts: {[key: string]: SpecInput}): Spec; + + /** + * By default no spec accepts null or undefined as valid input. Wrap your spec in nilable() to change this. + * @param {string} name the name of the spec + * @param {spec.SpecInput} spec the spec to apply if a value is non-nil + * @returns {Spec} the constructed spec + */ + function nilable(name: string, spec: SpecInput): Spec; + + // Cannot specify 'symbol' as a key type: https://github.com/Microsoft/TypeScript/issues/7660 + /** + * Used to define collections with items of the same type. Works with Arrays and Sets. + * Accepts an option map as optional second parameter. + * NOTE: the keys in this option map are symbols but Typescript will not allow 'symbol' to be specified + * as a key type but the TS compiler will allow it. + * @param {string} name the name of the spec + * @param {spec.SpecInput} spec the spec to apply to values in the collection + * @param {object} options symbol.count or symbol.minCount / symbol.maxCount + * @returns {Spec} + */ + function collection(name: string, spec: SpecInput, options?: {[option: string]: number}): Spec; + + /** + * Used to define collections with items of possibly different types. Works only with arrays as order is important. + * @param {string} name the name of the spec + * @param {spec.SpecInput} specs the specs to test the value array + * @returns {Spec} the constructed spec + */ + function tuple(name: string, ...specs: SpecInput[]): Spec; + + /** + * Used to define the shape of maps. By default all keys are required. Use {symbol.optional} key to define + * optional keys. Shape map can contain nested key specs. + * @param {string} name the name of the spec + * @param {Object} shape the shape map with keys and associated specs + * @returns {Spec} the constructed spec + */ + function map(name: string, shape: object): Spec; + + /** + * Used to define "one out of these values", like an enum. (It's called oneOf because enum is a reserved word.) + * @param {string} name the name of the spec + * @param values the emum of values + * @returns {Spec} the constructed spec + */ + function oneOf(name: string, ...values: any[]): Spec; + + // Predicates + /** + * Returns true if data is an integer. + */ + const int: Predicate; + + /** + * Returns true if data is an integer. + */ + const integer: Predicate; + + /** + * Returns true if data is a finite number. + */ + const finite: Predicate; + + /** + * Returns true if data is a number (can be double or integer). + */ + const number: Predicate; + + /** + * Returns true if data is an odd number. + */ + const odd: Predicate; + + /** + * Returns true if data is an even number. + */ + const even: Predicate; + + /** + * Returns true if data is a number greater than zero. + */ + const positive: Predicate; + + /** + * Returns true if data is a number smaller than zero. + */ + const negative: Predicate; + + /** + * Returns true if data is the number zero. + * Why: To easily construct specs for >= 0. + */ + const zero: Predicate; + + /** + * Returns true if data is a string. + */ + const str: Predicate; + + /** + * Returns true if data is a string. + */ + const string: Predicate; + + /** + * Returns true if data is a function. + */ + const fn: Predicate; + + /** + * Returns true if data is a Symbol. + */ + const sym: Predicate; + + /** + * Returns true if data is a Symbol. + */ + const symbol: Predicate; + + /** + * Returns true if data is null or undefined. + */ + const nil: Predicate; + + /** + * Returns true if data is a boolean. + */ + const bool: Predicate; + + /** + * Returns true if data is a boolean. + */ + const boolean: Predicate; + + /** + * Returns true if data is a Date. + */ + const date: Predicate; + + /** + * Returns true if data is a plain object. + */ + const obj: Predicate; + + /** + * Returns true if data is a plain object. + */ + const object: Predicate; + + /** + * Returns true if data is an Array. + */ + const array: Predicate; + + /** + * Returns true if data is a Set. + */ + const set: Predicate; + + /** + * Returns true if data is an Array or Set. + */ + const coll: Predicate; +} diff --git a/types/js.spec/js.spec-tests.ts b/types/js.spec/js.spec-tests.ts new file mode 100644 index 0000000000..8bb9611e87 --- /dev/null +++ b/types/js.spec/js.spec-tests.ts @@ -0,0 +1,87 @@ +import * as S from "js.spec"; + +const spec: S.Spec = S.spec.bool; +const c: symbol = spec.conform("water"); +const waterProblems: S.Problem[] = spec.explain("water"); +const name: string = spec.name; +const options: object = spec.options; + +const isValid: boolean = S.valid(S.spec.boolean, true); + +const result = S.conform(S.spec.map("dancing", {field: S.spec.string}), "not a map"); + +const problems: S.Problem[] = S.explainData(S.spec.int, "not a number"); + +const {path, via, value, predicate}: {path: string[], via: string[], value: any, predicate: S.Predicate} = problems[0]; + +const problemStr: string = S.explainStr(S.spec.even, 3); + +// $ExpectType void +S.explain(S.spec.positive, true); + +// $ExpectType void +S.assert(S.spec.string, "things"); + +const symbols: symbol[] = [S.symbol.count, S.symbol.invalid, S.symbol.maxCount, S.symbol.minCount, S.symbol.optional]; + +const orSpec: S.Spec = S.spec.or("or test", { ball: (value: any) => value === "whale", fish: S.spec.number }); + +const nilableSpec: S.Spec = S.spec.nilable("nilable test", (value: any) => false); + +const collectionSpec: S.Spec = S.spec.collection("collection test", S.spec.positive); + +const collection2Spec: S.Spec = S.spec.collection("collection test", S.spec.string, {[S.symbol.count]: 3}); + +const tupleSpec: S.Spec = S.spec.tuple("tuple test", S.spec.bool, S.spec.date, S.spec.array); + +const mapSpec: S.Spec = S.spec.map("map test", { email: S.spec.string, [S.symbol.optional]: { name: S.spec.string } }); + +const oneOfSpec: S.Spec = S.spec.oneOf("oneOf test", "a", "b", "c"); + +// Predicates + +const intPred: S.Predicate = S.spec.int; + +const integerPred: S.Predicate = S.spec.integer; + +const finitePred: S.Predicate = S.spec.finite; + +const numberPred: S.Predicate = S.spec.number; + +const oddPred: S.Predicate = S.spec.odd; + +const evenPred: S.Predicate = S.spec.even; + +const positivePred: S.Predicate = S.spec.positive; + +const negativePred: S.Predicate = S.spec.negative; + +const zeroPred: S.Predicate = S.spec.zero; + +const strPred: S.Predicate = S.spec.str; + +const stringPred: S.Predicate = S.spec.string; + +const fnPred: S.Predicate = S.spec.fn; + +const symPred: S.Predicate = S.spec.sym; + +const symbolPred: S.Predicate = S.spec.symbol; + +const nilPred: S.Predicate = S.spec.nil; + +const boolPred: S.Predicate = S.spec.bool; + +const booleanPred: S.Predicate = S.spec.boolean; + +const datePred: S.Predicate = S.spec.date; + +const objPred: S.Predicate = S.spec.obj; + +const objectPred: S.Predicate = S.spec.object; + +const arrayPred: S.Predicate = S.spec.array; + +const setPred: S.Predicate = S.spec.set; + +const collPred: S.Predicate = S.spec.coll; diff --git a/types/js.spec/tsconfig.json b/types/js.spec/tsconfig.json new file mode 100644 index 0000000000..b3afe65da2 --- /dev/null +++ b/types/js.spec/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "module": "commonjs", + "lib": ["es6"], + "noImplicitAny": true, + "noImplicitThis": true, + "strictNullChecks": true, + "baseUrl": "../", + "typeRoots": [ + "../" + ], + "types": [], + "noEmit": true, + "forceConsistentCasingInFileNames": true + }, + "files": [ + "index.d.ts", + "js.spec-tests.ts" + ] +} diff --git a/types/js.spec/tslint.json b/types/js.spec/tslint.json new file mode 100644 index 0000000000..2750cc0197 --- /dev/null +++ b/types/js.spec/tslint.json @@ -0,0 +1 @@ +{ "extends": "dtslint/dt.json" } \ No newline at end of file