├── .npmrc ├── .gitignore ├── .travis.yml ├── .npmignore ├── index.ts ├── tests ├── test_arrayWithNumber_success.ts ├── test_simpleObjectWithStringField_fail.ts ├── test_tupleWithNumberAndString_success.ts ├── test_objectWithStringIndex_successWithEmptyObj.ts ├── test_simpleObjectWithStringField_success.ts ├── test_tupleWithNumberAndString_failWithStringMissing.ts ├── test_simpleObjectWithOptionalStringField_successWithoutField.ts ├── test_tupleWithNumberAndString_failWithStringNumber.ts ├── test_objectWithNumberArray_failWithNull.ts ├── test_simpleObjectWithOptionalBooleanField_successWithoutField.ts ├── test_simpleObjectWithOptionalNumberField_successWithoutField.ts ├── test_simpleObjectWithOptionalStringField_failWithNumber.ts ├── test_simpleObjectWithStringOrNullField_failWithFieldMissing.ts ├── test_tupleWithNumberAndString_failWithThirdItem.ts ├── test_objectWithNumberArray_successWithEmptyArray.ts ├── test_simpleObjectWithNumberOrNullField_failWithFieldMissing.ts ├── test_simpleObjectWithOptionalStringField_successWithField.ts ├── test_simpleObjectWithStringFieldAndNonStrictNullChecks_failWithMissingField.ts ├── test_simpleObjectWithStringOrNullField_successWithNull.ts ├── test_objectWithNumberArray_success.ts ├── test_objectWithStringIndex_failWithNumberIndex.ts ├── test_simpleObjectWithBooleanOrNullField_failWithFieldMissing.ts ├── test_simpleObjectWithStringOrNullField_successWithString.ts ├── test_objectWithStringIndex_success.ts ├── test_simpleObjectWithOptionalNumberField_failWithString.ts ├── test_simpleObjectWithOptionalNumberField_successWithField.ts ├── test_simpleObjectWithStringFieldAndNonStrictNullChecks_successWithNull.ts ├── test_simpleObjectWithStringFieldAndNonStrictNullChecks_successWithString.ts ├── test_simpleObjectWithNumberOrNullField_successWithNull.ts ├── test_simpleObjectWithNumberOrNullField_successWithNumber.ts ├── test_simpleObjectWithOptionalBooleanField_successWithField.ts ├── test_objectWithNumberArray_failWithStringArray.ts ├── test_objectWithStringIndex_failWithBoolean.ts ├── test_simpleObjectWithBooleanOrNullField_successWithBoolean.ts ├── test_simpleObjectWithBooleanOrNullField_successWithNull.ts ├── test_simpleObjectWithOptionalBooleanField_failWithString.ts ├── test_objectWithStringIndexAndFields_successWithNoIndexFields.ts ├── test_hierachicalObjects_failWithChildMissing.ts ├── test_hierachicalObjects_failWithChildNull.ts ├── test_objectWithStringIndexAndFields_failWithFieldMissing.ts ├── test_objectWithStringIndexAndFields_success.ts ├── testEnum1Fail.ts ├── testEnum1Success.ts ├── testEnum3Fail1.ts ├── testEnum3Fail2.ts ├── test_objectWithStringIndexAndFields_failWithFieldWithString.ts ├── testEnum2Fail.ts ├── testEnum2Success.ts ├── testEnum3Success.ts ├── test_hierachicalObjects_success.ts ├── test_objectWithStringIndexAndFields_failWithIndexFieldWithString.ts ├── test_hierachicalObjects_failWithChildWithString.ts ├── test_simpleObjectWithStringNumberBooleanFields_success.ts ├── test_simpleObjectWithStringNumberBooleanFields_failBoolean.ts ├── test_simpleObjectWithStringNumberBooleanFields_failNumber.ts ├── test_simpleObjectWithStringNumberBooleanFields_failString.ts ├── test_customValidator_fail.ts ├── test_customValidator_success.ts └── transformer.test.js ├── webpack-example ├── index.html ├── package.json ├── index.ts ├── webpack.config.js └── tsconfig.json ├── .prettierrc ├── debug ├── debug-source2.ts ├── debug-source.ts └── debug.ts ├── ts-node-example ├── package.json ├── run.js ├── index.ts ├── tsconfig.json └── package-lock.json ├── .vscode └── launch.json ├── .github └── workflows │ └── build.yml ├── LICENSE ├── package.json ├── tsconfig.module.json ├── tsconfig.json ├── README.md ├── transformer.ts └── validator.ts /.npmrc: -------------------------------------------------------------------------------- 1 | message=":bookmark: %s" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | module 3 | node_modules 4 | debug/*.js -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 10 4 | - 11 5 | - 12 -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | !dist 3 | !module 4 | **/*tsbuildinfo 5 | **/*.tgz 6 | tests 7 | webpack-example 8 | .yarnrc 9 | tsconfig.module.json -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | export type CustomValidator = (x: unknown) => x is any; 2 | 3 | export declare function validate( 4 | jsonObj: any, 5 | customValidators?: CustomValidator[] 6 | ): T; 7 | -------------------------------------------------------------------------------- /tests/test_arrayWithNumber_success.ts: -------------------------------------------------------------------------------- 1 | import { validate } from "superstruct-ts-transformer"; 2 | 3 | type TestType = number[]; 4 | 5 | export const obj = validate(JSON.parse("[123, 321]")); 6 | -------------------------------------------------------------------------------- /tests/test_simpleObjectWithStringField_fail.ts: -------------------------------------------------------------------------------- 1 | import { validate } from "superstruct-ts-transformer"; 2 | 3 | type TestType = { name: string }; 4 | 5 | export const obj = validate(JSON.parse('{ "nama": "Me" }')); 6 | -------------------------------------------------------------------------------- /tests/test_tupleWithNumberAndString_success.ts: -------------------------------------------------------------------------------- 1 | import { validate } from "superstruct-ts-transformer"; 2 | 3 | type TestType = [number, string]; 4 | 5 | export const obj = validate(JSON.parse('[123, "testStr"]')); 6 | -------------------------------------------------------------------------------- /tests/test_objectWithStringIndex_successWithEmptyObj.ts: -------------------------------------------------------------------------------- 1 | import { validate } from "superstruct-ts-transformer"; 2 | 3 | type TestType = { [key: string]: number }; 4 | 5 | export const obj = validate(JSON.parse("{ }")); 6 | -------------------------------------------------------------------------------- /tests/test_simpleObjectWithStringField_success.ts: -------------------------------------------------------------------------------- 1 | import { validate } from "superstruct-ts-transformer"; 2 | 3 | type TestType = { name: string }; 4 | 5 | export const obj = validate(JSON.parse('{ "name": "Me" }')); 6 | -------------------------------------------------------------------------------- /tests/test_tupleWithNumberAndString_failWithStringMissing.ts: -------------------------------------------------------------------------------- 1 | import { validate } from "superstruct-ts-transformer"; 2 | 3 | type TestType = [number, string]; 4 | 5 | export const obj = validate(JSON.parse("[123]")); 6 | -------------------------------------------------------------------------------- /tests/test_simpleObjectWithOptionalStringField_successWithoutField.ts: -------------------------------------------------------------------------------- 1 | import { validate } from "superstruct-ts-transformer"; 2 | 3 | type TestType = { name?: string }; 4 | 5 | export const obj = validate(JSON.parse("{}")); 6 | -------------------------------------------------------------------------------- /tests/test_tupleWithNumberAndString_failWithStringNumber.ts: -------------------------------------------------------------------------------- 1 | import { validate } from "superstruct-ts-transformer"; 2 | 3 | type TestType = [number, string]; 4 | 5 | export const obj = validate(JSON.parse('["testStr", 123]')); 6 | -------------------------------------------------------------------------------- /webpack-example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Superstruct typescript transformer webpack example 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /tests/test_objectWithNumberArray_failWithNull.ts: -------------------------------------------------------------------------------- 1 | import { validate } from "superstruct-ts-transformer"; 2 | 3 | type TestType = { fieldArray: number[] }; 4 | 5 | export const obj = validate(JSON.parse('{ "fieldArray": null }')); 6 | -------------------------------------------------------------------------------- /tests/test_simpleObjectWithOptionalBooleanField_successWithoutField.ts: -------------------------------------------------------------------------------- 1 | import { validate } from "superstruct-ts-transformer"; 2 | 3 | type TestType = { fieldBoolean?: boolean }; 4 | 5 | export const obj = validate(JSON.parse("{}")); 6 | -------------------------------------------------------------------------------- /tests/test_simpleObjectWithOptionalNumberField_successWithoutField.ts: -------------------------------------------------------------------------------- 1 | import { validate } from "superstruct-ts-transformer"; 2 | 3 | type TestType = { fieldNumber?: number }; 4 | 5 | export const obj = validate(JSON.parse("{}")); 6 | -------------------------------------------------------------------------------- /tests/test_simpleObjectWithOptionalStringField_failWithNumber.ts: -------------------------------------------------------------------------------- 1 | import { validate } from "superstruct-ts-transformer"; 2 | 3 | type TestType = { name?: string }; 4 | 5 | export const obj = validate(JSON.parse('{ "name": 123 }')); 6 | -------------------------------------------------------------------------------- /tests/test_simpleObjectWithStringOrNullField_failWithFieldMissing.ts: -------------------------------------------------------------------------------- 1 | import { validate } from "superstruct-ts-transformer"; 2 | 3 | type TestType = { name: string | null }; 4 | 5 | export const obj = validate(JSON.parse("{ }")); 6 | -------------------------------------------------------------------------------- /tests/test_tupleWithNumberAndString_failWithThirdItem.ts: -------------------------------------------------------------------------------- 1 | import { validate } from "superstruct-ts-transformer"; 2 | 3 | type TestType = [number, string]; 4 | 5 | export const obj = validate(JSON.parse('[123, "testStr", true]')); 6 | -------------------------------------------------------------------------------- /tests/test_objectWithNumberArray_successWithEmptyArray.ts: -------------------------------------------------------------------------------- 1 | import { validate } from "superstruct-ts-transformer"; 2 | 3 | type TestType = { fieldArray: number[] }; 4 | 5 | export const obj = validate(JSON.parse('{ "fieldArray": [] }')); 6 | -------------------------------------------------------------------------------- /tests/test_simpleObjectWithNumberOrNullField_failWithFieldMissing.ts: -------------------------------------------------------------------------------- 1 | import { validate } from "superstruct-ts-transformer"; 2 | 3 | type TestType = { fieldNumber: number | null }; 4 | 5 | export const obj = validate(JSON.parse("{ }")); 6 | -------------------------------------------------------------------------------- /tests/test_simpleObjectWithOptionalStringField_successWithField.ts: -------------------------------------------------------------------------------- 1 | import { validate } from "superstruct-ts-transformer"; 2 | 3 | type TestType = { name?: string }; 4 | 5 | export const obj = validate(JSON.parse('{ "name": "Me" }')); 6 | -------------------------------------------------------------------------------- /tests/test_simpleObjectWithStringFieldAndNonStrictNullChecks_failWithMissingField.ts: -------------------------------------------------------------------------------- 1 | import { validate } from "superstruct-ts-transformer"; 2 | 3 | type TestType = { name: string }; 4 | 5 | export const obj = validate(JSON.parse("{ }")); 6 | -------------------------------------------------------------------------------- /tests/test_simpleObjectWithStringOrNullField_successWithNull.ts: -------------------------------------------------------------------------------- 1 | import { validate } from "superstruct-ts-transformer"; 2 | 3 | type TestType = { name: string | null }; 4 | 5 | export const obj = validate(JSON.parse('{ "name": null }')); 6 | -------------------------------------------------------------------------------- /tests/test_objectWithNumberArray_success.ts: -------------------------------------------------------------------------------- 1 | import { validate } from "superstruct-ts-transformer"; 2 | 3 | type TestType = { fieldArray: number[] }; 4 | 5 | export const obj = validate( 6 | JSON.parse('{ "fieldArray": [123, 321] }') 7 | ); 8 | -------------------------------------------------------------------------------- /tests/test_objectWithStringIndex_failWithNumberIndex.ts: -------------------------------------------------------------------------------- 1 | import { validate } from "superstruct-ts-transformer"; 2 | 3 | type TestType = { [key: string]: number }; 4 | 5 | export const obj = validate(JSON.parse('{ "field1": 123, 0: 321 }')); 6 | -------------------------------------------------------------------------------- /tests/test_simpleObjectWithBooleanOrNullField_failWithFieldMissing.ts: -------------------------------------------------------------------------------- 1 | import { validate } from "superstruct-ts-transformer"; 2 | 3 | type TestType = { fieldBoolean: boolean | null }; 4 | 5 | export const obj = validate(JSON.parse("{ }")); 6 | -------------------------------------------------------------------------------- /tests/test_simpleObjectWithStringOrNullField_successWithString.ts: -------------------------------------------------------------------------------- 1 | import { validate } from "superstruct-ts-transformer"; 2 | 3 | type TestType = { name: string | null }; 4 | 5 | export const obj = validate(JSON.parse('{ "name": "Me" }')); 6 | -------------------------------------------------------------------------------- /tests/test_objectWithStringIndex_success.ts: -------------------------------------------------------------------------------- 1 | import { validate } from "superstruct-ts-transformer"; 2 | 3 | type TestType = { [key: string]: number }; 4 | 5 | export const obj = validate( 6 | JSON.parse('{ "field1": 123, "field2": 321 }') 7 | ); 8 | -------------------------------------------------------------------------------- /tests/test_simpleObjectWithOptionalNumberField_failWithString.ts: -------------------------------------------------------------------------------- 1 | import { validate } from "superstruct-ts-transformer"; 2 | 3 | type TestType = { fieldNumber?: number }; 4 | 5 | export const obj = validate(JSON.parse('{ "fieldNumber": "123" }')); 6 | -------------------------------------------------------------------------------- /tests/test_simpleObjectWithOptionalNumberField_successWithField.ts: -------------------------------------------------------------------------------- 1 | import { validate } from "superstruct-ts-transformer"; 2 | 3 | type TestType = { fieldNumber?: number }; 4 | 5 | export const obj = validate(JSON.parse('{ "fieldNumber": 123 }')); 6 | -------------------------------------------------------------------------------- /tests/test_simpleObjectWithStringFieldAndNonStrictNullChecks_successWithNull.ts: -------------------------------------------------------------------------------- 1 | import { validate } from "superstruct-ts-transformer"; 2 | 3 | type TestType = { name: string }; 4 | 5 | export const obj = validate(JSON.parse('{ "name": null }')); 6 | -------------------------------------------------------------------------------- /tests/test_simpleObjectWithStringFieldAndNonStrictNullChecks_successWithString.ts: -------------------------------------------------------------------------------- 1 | import { validate } from "superstruct-ts-transformer"; 2 | 3 | type TestType = { name: string }; 4 | 5 | export const obj = validate(JSON.parse('{ "name": "Me" }')); 6 | -------------------------------------------------------------------------------- /tests/test_simpleObjectWithNumberOrNullField_successWithNull.ts: -------------------------------------------------------------------------------- 1 | import { validate } from "superstruct-ts-transformer"; 2 | 3 | type TestType = { fieldNumber: number | null }; 4 | 5 | export const obj = validate(JSON.parse('{ "fieldNumber": null }')); 6 | -------------------------------------------------------------------------------- /tests/test_simpleObjectWithNumberOrNullField_successWithNumber.ts: -------------------------------------------------------------------------------- 1 | import { validate } from "superstruct-ts-transformer"; 2 | 3 | type TestType = { fieldNumber: number | null }; 4 | 5 | export const obj = validate(JSON.parse('{ "fieldNumber": 123 }')); 6 | -------------------------------------------------------------------------------- /tests/test_simpleObjectWithOptionalBooleanField_successWithField.ts: -------------------------------------------------------------------------------- 1 | import { validate } from "superstruct-ts-transformer"; 2 | 3 | type TestType = { fieldBoolean?: boolean }; 4 | 5 | export const obj = validate(JSON.parse('{ "fieldBoolean": true }')); 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "bracketSpacing": true, 4 | "printWidth": 80, 5 | "quoteProps": "as-needed", 6 | "semi": true, 7 | "singleQuote": false, 8 | "tabWidth": 2, 9 | "trailingComma": "none", 10 | "useTabs": false 11 | } -------------------------------------------------------------------------------- /tests/test_objectWithNumberArray_failWithStringArray.ts: -------------------------------------------------------------------------------- 1 | import { validate } from "superstruct-ts-transformer"; 2 | 3 | type TestType = { fieldArray: number[] }; 4 | 5 | export const obj = validate( 6 | JSON.parse('{ "fieldArray": ["asd", "dsa"] }') 7 | ); 8 | -------------------------------------------------------------------------------- /tests/test_objectWithStringIndex_failWithBoolean.ts: -------------------------------------------------------------------------------- 1 | import { validate } from "superstruct-ts-transformer"; 2 | 3 | type TestType = { [key: string]: number }; 4 | 5 | export const obj = validate( 6 | JSON.parse('{ "field1": true, "field2": 321 }') 7 | ); 8 | -------------------------------------------------------------------------------- /tests/test_simpleObjectWithBooleanOrNullField_successWithBoolean.ts: -------------------------------------------------------------------------------- 1 | import { validate } from "superstruct-ts-transformer"; 2 | 3 | type TestType = { fieldBoolean: boolean | null }; 4 | 5 | export const obj = validate(JSON.parse('{ "fieldBoolean": true }')); 6 | -------------------------------------------------------------------------------- /tests/test_simpleObjectWithBooleanOrNullField_successWithNull.ts: -------------------------------------------------------------------------------- 1 | import { validate } from "superstruct-ts-transformer"; 2 | 3 | type TestType = { fieldBoolean: boolean | null }; 4 | 5 | export const obj = validate(JSON.parse('{ "fieldBoolean": null }')); 6 | -------------------------------------------------------------------------------- /tests/test_simpleObjectWithOptionalBooleanField_failWithString.ts: -------------------------------------------------------------------------------- 1 | import { validate } from "superstruct-ts-transformer"; 2 | 3 | type TestType = { fieldBoolean?: boolean }; 4 | 5 | export const obj = validate( 6 | JSON.parse('{ "fieldBoolean": "false" }') 7 | ); 8 | -------------------------------------------------------------------------------- /tests/test_objectWithStringIndexAndFields_successWithNoIndexFields.ts: -------------------------------------------------------------------------------- 1 | import { validate } from "superstruct-ts-transformer"; 2 | 3 | type TestType = { fieldNumber: number; [key: string]: number }; 4 | 5 | export const obj = validate(JSON.parse('{ "fieldNumber": 123 }')); 6 | -------------------------------------------------------------------------------- /tests/test_hierachicalObjects_failWithChildMissing.ts: -------------------------------------------------------------------------------- 1 | import { validate } from "superstruct-ts-transformer"; 2 | 3 | type TestType = { fieldChild: TestChildType }; 4 | 5 | type TestChildType = { fieldNumber: number }; 6 | 7 | export const obj = validate(JSON.parse("{ }")); 8 | -------------------------------------------------------------------------------- /tests/test_hierachicalObjects_failWithChildNull.ts: -------------------------------------------------------------------------------- 1 | import { validate } from "superstruct-ts-transformer"; 2 | 3 | type TestType = { fieldChild: TestChildType }; 4 | 5 | type TestChildType = { fieldNumber: number }; 6 | 7 | export const obj = validate(JSON.parse('{ "fieldChild": null }')); 8 | -------------------------------------------------------------------------------- /tests/test_objectWithStringIndexAndFields_failWithFieldMissing.ts: -------------------------------------------------------------------------------- 1 | import { validate } from "superstruct-ts-transformer"; 2 | 3 | type TestType = { fieldNumber: number; [key: string]: number }; 4 | 5 | export const obj = validate( 6 | JSON.parse('{ "field1": 123, "field2": 321 }') 7 | ); 8 | -------------------------------------------------------------------------------- /tests/test_objectWithStringIndexAndFields_success.ts: -------------------------------------------------------------------------------- 1 | import { validate } from "superstruct-ts-transformer"; 2 | 3 | type TestType = { fieldNumber: number; [key: string]: number }; 4 | 5 | export const obj = validate( 6 | JSON.parse('{ "fieldNumber": 123, "field1": 123, "field2": 321 }') 7 | ); 8 | -------------------------------------------------------------------------------- /tests/testEnum1Fail.ts: -------------------------------------------------------------------------------- 1 | import { validate } from "superstruct-ts-transformer"; 2 | 3 | enum TestEnum { 4 | EnumField0, 5 | EnumField1, 6 | EnumField2 7 | } 8 | 9 | type TestType = { 10 | fieldEnum: TestEnum; 11 | }; 12 | 13 | export const obj = validate(JSON.parse('{ "fieldEnum": 30 }')); 14 | -------------------------------------------------------------------------------- /tests/testEnum1Success.ts: -------------------------------------------------------------------------------- 1 | import { validate } from "superstruct-ts-transformer"; 2 | 3 | enum TestEnum { 4 | EnumField0, 5 | EnumField1, 6 | EnumField2 7 | } 8 | 9 | type TestType = { 10 | fieldEnum: TestEnum; 11 | }; 12 | 13 | export const obj = validate(JSON.parse('{ "fieldEnum": 2 }')); 14 | -------------------------------------------------------------------------------- /debug/debug-source2.ts: -------------------------------------------------------------------------------- 1 | export type Uuid = string & { readonly __brand: unique symbol }; 2 | 3 | const uuidRegExp = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; 4 | 5 | export const isUuid = (value: unknown): value is Uuid => 6 | typeof value === "string" && !!value && uuidRegExp.test(value); 7 | -------------------------------------------------------------------------------- /tests/testEnum3Fail1.ts: -------------------------------------------------------------------------------- 1 | import { validate } from "superstruct-ts-transformer"; 2 | 3 | enum TestEnum { 4 | Foo = "foo", 5 | Bar = "bar", 6 | Xyzzy = "xyzzy" 7 | } 8 | 9 | type TestType = { 10 | fieldEnum: TestEnum; 11 | }; 12 | 13 | export const obj = validate(JSON.parse('{ "fieldEnum": 0 }')); 14 | -------------------------------------------------------------------------------- /tests/testEnum3Fail2.ts: -------------------------------------------------------------------------------- 1 | import { validate } from "superstruct-ts-transformer"; 2 | 3 | enum TestEnum { 4 | Foo = "foo", 5 | Bar = "bar", 6 | Xyzzy = "xyzzy" 7 | } 8 | 9 | type TestType = { 10 | fieldEnum: TestEnum; 11 | }; 12 | 13 | export const obj = validate(JSON.parse('{ "fieldEnum": "eh" }')); 14 | -------------------------------------------------------------------------------- /tests/test_objectWithStringIndexAndFields_failWithFieldWithString.ts: -------------------------------------------------------------------------------- 1 | import { validate } from "superstruct-ts-transformer"; 2 | 3 | type TestType = { fieldNumber: number; [key: string]: number }; 4 | 5 | export const obj = validate( 6 | JSON.parse('{ "fieldNumber": "str", "field1": 123, "field2": 321 }') 7 | ); 8 | -------------------------------------------------------------------------------- /tests/testEnum2Fail.ts: -------------------------------------------------------------------------------- 1 | import { validate } from "superstruct-ts-transformer"; 2 | 3 | enum TestEnum { 4 | EnumField0 = 1, 5 | EnumField1 = 4, 6 | EnumField2 = 9 7 | } 8 | 9 | type TestType = { 10 | fieldEnum: TestEnum; 11 | }; 12 | 13 | export const obj = validate(JSON.parse('{ "fieldEnum": 2 }')); 14 | -------------------------------------------------------------------------------- /tests/testEnum2Success.ts: -------------------------------------------------------------------------------- 1 | import { validate } from "superstruct-ts-transformer"; 2 | 3 | enum TestEnum { 4 | EnumField0 = 1, 5 | EnumField1 = 4, 6 | EnumField2 = 9 7 | } 8 | 9 | type TestType = { 10 | fieldEnum: TestEnum; 11 | }; 12 | 13 | export const obj = validate(JSON.parse('{ "fieldEnum": 4 }')); 14 | -------------------------------------------------------------------------------- /tests/testEnum3Success.ts: -------------------------------------------------------------------------------- 1 | import { validate } from "superstruct-ts-transformer"; 2 | 3 | enum TestEnum { 4 | Foo = "foo", 5 | Bar = "bar", 6 | Xyzzy = "xyzzy" 7 | } 8 | 9 | type TestType = { 10 | fieldEnum: TestEnum; 11 | }; 12 | 13 | export const obj = validate(JSON.parse('{ "fieldEnum": "foo" }')); 14 | -------------------------------------------------------------------------------- /tests/test_hierachicalObjects_success.ts: -------------------------------------------------------------------------------- 1 | import { validate } from "superstruct-ts-transformer"; 2 | 3 | type TestType = { fieldChild: TestChildType }; 4 | 5 | type TestChildType = { fieldNumber: number }; 6 | 7 | export const obj = validate( 8 | JSON.parse('{ "fieldChild": { "fieldNumber": 123 } }') 9 | ); 10 | -------------------------------------------------------------------------------- /tests/test_objectWithStringIndexAndFields_failWithIndexFieldWithString.ts: -------------------------------------------------------------------------------- 1 | import { validate } from "superstruct-ts-transformer"; 2 | 3 | type TestType = { fieldNumber: number; [key: string]: number }; 4 | 5 | export const obj = validate( 6 | JSON.parse('{ "fieldNumber": 123, "field1": "asd", "field2": 321 }') 7 | ); 8 | -------------------------------------------------------------------------------- /tests/test_hierachicalObjects_failWithChildWithString.ts: -------------------------------------------------------------------------------- 1 | import { validate } from "superstruct-ts-transformer"; 2 | 3 | type TestType = { fieldChild: TestChildType }; 4 | 5 | type TestChildType = { fieldNumber: number }; 6 | 7 | export const obj = validate( 8 | JSON.parse('{ "fieldChild": { "fieldNumber": "asd" } }') 9 | ); 10 | -------------------------------------------------------------------------------- /ts-node-example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ts-node-example", 3 | "private": true, 4 | "version": "1.0.0", 5 | "description": "", 6 | "main": "index.js", 7 | "scripts": { 8 | "start": "node run.js" 9 | }, 10 | "devDependencies": { 11 | "ts-node": "^8.3.0" 12 | }, 13 | "dependencies": { 14 | "superstruct-ts-transformer": "^0.1.1" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/test_simpleObjectWithStringNumberBooleanFields_success.ts: -------------------------------------------------------------------------------- 1 | import { validate } from "superstruct-ts-transformer"; 2 | 3 | type TestType = { 4 | fieldStr: string; 5 | fieldNumber: number; 6 | fieldBoolean: boolean; 7 | }; 8 | 9 | export const obj = validate( 10 | JSON.parse( 11 | '{ "fieldStr": "test str", "fieldNumber": 123, "fieldBoolean": true }' 12 | ) 13 | ); 14 | -------------------------------------------------------------------------------- /tests/test_simpleObjectWithStringNumberBooleanFields_failBoolean.ts: -------------------------------------------------------------------------------- 1 | import { validate } from "superstruct-ts-transformer"; 2 | 3 | type TestType = { 4 | fieldStr: string; 5 | fieldNumber: number; 6 | fieldBoolean: boolean; 7 | }; 8 | 9 | export const obj = validate( 10 | JSON.parse( 11 | '{ "fieldStr": "test str", "fieldNumber": 123, "_fieldBoolean": true }' 12 | ) 13 | ); 14 | -------------------------------------------------------------------------------- /tests/test_simpleObjectWithStringNumberBooleanFields_failNumber.ts: -------------------------------------------------------------------------------- 1 | import { validate } from "superstruct-ts-transformer"; 2 | 3 | type TestType = { 4 | fieldStr: string; 5 | fieldNumber: number; 6 | fieldBoolean: boolean; 7 | }; 8 | 9 | export const obj = validate( 10 | JSON.parse( 11 | '{ "fieldStr": "test str", "_fieldNumber": 123, "fieldBoolean": true }' 12 | ) 13 | ); 14 | -------------------------------------------------------------------------------- /tests/test_simpleObjectWithStringNumberBooleanFields_failString.ts: -------------------------------------------------------------------------------- 1 | import { validate } from "superstruct-ts-transformer"; 2 | 3 | type TestType = { 4 | fieldStr: string; 5 | fieldNumber: number; 6 | fieldBoolean: boolean; 7 | }; 8 | 9 | export const obj = validate( 10 | JSON.parse( 11 | '{ "_fieldStr": "test str", "fieldNumber": 123, "fieldBoolean": true }' 12 | ) 13 | ); 14 | -------------------------------------------------------------------------------- /tests/test_customValidator_fail.ts: -------------------------------------------------------------------------------- 1 | import { validate } from "superstruct-ts-transformer"; 2 | 3 | type Uuid = string & { readonly __brand: unique symbol }; 4 | 5 | const uuidRegExp = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; 6 | 7 | const isUuid = (value: unknown): value is Uuid => 8 | typeof value === "string" && !!value && uuidRegExp.test(value); 9 | 10 | type TestType = Uuid; 11 | 12 | export const obj = validate( 13 | JSON.parse('"thisisnotuuid"'), 14 | [isUuid] 15 | ); 16 | -------------------------------------------------------------------------------- /tests/test_customValidator_success.ts: -------------------------------------------------------------------------------- 1 | import { validate } from "superstruct-ts-transformer"; 2 | 3 | type Uuid = string & { readonly __brand: unique symbol }; 4 | 5 | const uuidRegExp = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; 6 | 7 | const isUuid = (value: unknown): value is Uuid => 8 | typeof value === "string" && !!value && uuidRegExp.test(value); 9 | 10 | type TestType = Uuid; 11 | 12 | export const obj = validate( 13 | JSON.parse('"a4e1b0cf-2a08-4297-83f3-4db896d7e0fb"'), 14 | [isUuid] 15 | ); 16 | -------------------------------------------------------------------------------- /webpack-example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "superstruct-ts-transformer-webpack-example", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "webpack-cli serve --mode development", 7 | "build": "webpack --mode development" 8 | }, 9 | "devDependencies": { 10 | "@webpack-cli/serve": "^0.1.8", 11 | "ts-loader": "^6.0.4", 12 | "typescript": "^3.5.2", 13 | "webpack": "^4.39.1", 14 | "webpack-cli": "^3.3.6" 15 | }, 16 | "dependencies": { 17 | "superstruct-ts-transformer": "^0.1.0" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Debug", 11 | "runtimeArgs": ["-r", "ts-node/register"], 12 | "args": ["${workspaceFolder}/debug/debug.ts"], 13 | "env": {"SUPERSTRUCT_TS_TRANSFORMER_ENV": "debug"} 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /ts-node-example/run.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const { 3 | createValidatorTransformer 4 | } = require("superstruct-ts-transformer/dist/transformer"); 5 | 6 | // This is the almost the same at using `ts-node` or `node -r ts-node/register` 7 | // However it allows us to pass transformers 8 | // This should be executed before you start requiring ts and tsx files 9 | require("ts-node").register({ 10 | programTransformers: program => ({ 11 | before: [createValidatorTransformer(program)] // <-- custom transfomer configuration 12 | // don't forget that it's an array 13 | }) 14 | }); 15 | 16 | require("./index.ts"); 17 | -------------------------------------------------------------------------------- /webpack-example/index.ts: -------------------------------------------------------------------------------- 1 | import { validate } from "superstruct-ts-transformer"; 2 | 3 | type HttpBinGetResponse = { 4 | args: { [key: string]: any }; 5 | headers: { 6 | [key: string]: string; 7 | }; 8 | origin: string; 9 | url: string; 10 | }; 11 | 12 | fetch("https://httpbin.org/get") 13 | .then(response => response.json()) 14 | .then(json => validate(json)) // <-- validate call! 15 | .then(httpBinResponse => { 16 | const responseStr = JSON.stringify(httpBinResponse, undefined, " "); 17 | document.body.innerHTML = `
${responseStr}
`; 18 | }) 19 | .catch(error => { 20 | const errorStr = error.message; 21 | document.body.innerHTML = `
${errorStr}
`; 22 | }); 23 | -------------------------------------------------------------------------------- /debug/debug-source.ts: -------------------------------------------------------------------------------- 1 | import { validate } from "../index"; 2 | // import { isUuid, Uuid } from "./debug-source2"; 3 | 4 | // type TestType = { field: Uuid }; 5 | 6 | // export const obj = validate( 7 | // JSON.parse('{ "field": "a4e1b0cf-2a08-4297-83f3-4db896d7e0fb" }'), 8 | // [isUuid] 9 | // ); 10 | export type Uuid = string & { readonly __brand: unique symbol }; 11 | 12 | const uuidRegExp = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; 13 | 14 | export const isUuid = (value: unknown): value is Uuid => 15 | typeof value === "string" && !!value && uuidRegExp.test(value); 16 | 17 | type TestType = Uuid; 18 | 19 | export const obj = validate( 20 | JSON.parse("a4e1b0cf-2a08-4297-83f3-4db896d7e0fb"), 21 | [isUuid] 22 | ); 23 | -------------------------------------------------------------------------------- /webpack-example/webpack.config.js: -------------------------------------------------------------------------------- 1 | const { 2 | createValidatorTransformer 3 | } = require("superstruct-ts-transformer/dist/transformer"); 4 | 5 | module.exports = { 6 | entry: "./index.ts", 7 | output: { 8 | filename: "bundle.js" 9 | }, 10 | resolve: { 11 | extensions: [".ts", ".tsx", ".js"] 12 | }, 13 | module: { 14 | rules: [ 15 | { 16 | test: /\.tsx?$/, 17 | use: [ 18 | { 19 | loader: "ts-loader", 20 | options: { 21 | getCustomTransformers: program => ({ 22 | before: [createValidatorTransformer(program)] // <-- custom transfomer configuration 23 | // don't forget that it's an array 24 | }) 25 | } 26 | } 27 | ] 28 | } 29 | ] 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [12.x, 14.x] 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | - name: Use Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v1 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | - run: npm ci 28 | - run: npm run build --if-present 29 | - run: npm test 30 | env: 31 | CI: true 32 | -------------------------------------------------------------------------------- /ts-node-example/index.ts: -------------------------------------------------------------------------------- 1 | import https from 'https' 2 | import { validate } from "superstruct-ts-transformer"; 3 | 4 | type HttpBinGetResponse = { 5 | args: { [key: string]: any }; 6 | headers: { 7 | [key: string]: string; 8 | }; 9 | origin: string; 10 | url: string; 11 | }; 12 | 13 | https.get("https://httpbin.org/get", res => { 14 | const { statusCode } = res; 15 | 16 | if (statusCode && statusCode < 200 && statusCode >= 300) { 17 | console.error(`Status code is ${statusCode}`) 18 | res.resume() 19 | return 20 | } 21 | 22 | res.setEncoding('utf8'); 23 | let rawData = ''; 24 | res.on('data', (chunk) => { rawData += chunk; }); 25 | res.on('end', () => { 26 | try { 27 | const parsedData = JSON.parse(rawData); 28 | validate(parsedData) // <-- validate call! 29 | console.log(parsedData); 30 | } catch (e) { 31 | console.error(e.message); 32 | } 33 | }); 34 | }).on('error', (e) => { 35 | console.error(`Got error: ${e.message}`); 36 | }) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Mikhail Bashurov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /debug/debug.ts: -------------------------------------------------------------------------------- 1 | import ts from "typescript"; 2 | import path from "path"; 3 | import fs from "fs"; 4 | import { createValidatorTransformer } from "../transformer"; 5 | 6 | compile({ 7 | rootNames: [path.resolve(__dirname, "debug-source.ts")], 8 | options: { 9 | target: ts.ScriptTarget.ESNext, 10 | module: ts.ModuleKind.ESNext, 11 | strict: true 12 | // strictNullChecks: true, 13 | } 14 | }); 15 | 16 | function compile(createProgramOptions: ts.CreateProgramOptions) { 17 | const program = ts.createProgram(createProgramOptions); 18 | 19 | const result = program.emit(undefined, undefined, undefined, undefined, { 20 | before: [createValidatorTransformer(program)] 21 | }); 22 | // result. 23 | 24 | return; 25 | createProgramOptions.rootNames.forEach(filename => { 26 | const sourceText = fs.readFileSync(path.resolve(__dirname, filename), { 27 | encoding: "utf-8" 28 | }); 29 | 30 | const output = ts.transpileModule(sourceText, { 31 | compilerOptions: createProgramOptions.options, 32 | transformers: { 33 | before: [createValidatorTransformer(program)] 34 | } 35 | }); 36 | 37 | console.log(output.outputText); 38 | 39 | // const output2 = ts.transpileModule(sourceText, { 40 | // compilerOptions: options, 41 | // transformers: { 42 | // before: [createValidatorTransformer(program)] 43 | // } 44 | // }); 45 | 46 | // console.log(output2.outputText); 47 | }); 48 | } 49 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "superstruct-ts-transformer", 3 | "version": "0.2.3", 4 | "main": "dist/index.js", 5 | "module": "module/index.js", 6 | "author": "Saito Nakamura ", 7 | "license": "MIT", 8 | "scripts": { 9 | "test": "npm run lint && npm run test:raw | faucet", 10 | "test:raw": "tape -r ts-node/register tests/*.test.js", 11 | "watch": "tsc --watch --project ./tsconfig.json", 12 | "build": "npm run build:commonjs && npm run build:module", 13 | "build:commonjs": "tsc --project ./tsconfig.json", 14 | "build:module": "tsc --project ./tsconfig.module.json", 15 | "lint": "tsc --project ./tsconfig.json --noEmit", 16 | "prepublishOnly": "npm test && npm run build" 17 | }, 18 | "keywords": [ 19 | "typescript", 20 | "typescript-transformer", 21 | "typescript-compiler-api", 22 | "json-validation" 23 | ], 24 | "repository": { 25 | "type": "git", 26 | "url": "git+https://github.com/ts-type-makeup/superstruct-ts-transformer.git" 27 | }, 28 | "bugs": { 29 | "url": "https://github.com/ts-type-makeup/superstruct-ts-transformer/issues" 30 | }, 31 | "homepage": "https://github.com/ts-type-makeup/superstruct-ts-transformer#readme", 32 | "dependencies": { 33 | "superstruct": "^0.8.3", 34 | "ts-type-visitor": "^0.2.2" 35 | }, 36 | "peerDependencies": { 37 | "typescript": "^3.5.2 || ^4.0.0" 38 | }, 39 | "devDependencies": { 40 | "@types/tape": "^4.13.0", 41 | "faucet": "^0.0.1", 42 | "node-eval": "^2.0.0", 43 | "tape": "^4.13.2", 44 | "ts-node": "^8.8.2", 45 | "typescript": "^4.1.2" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tsconfig.module.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | /* Basic Options */ 5 | "module": "esnext" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, 6 | "outDir": "./module" /* Redirect output structure to the directory. */, 7 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 8 | /* Module Resolution Options */ 9 | "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */, 10 | // "baseUrl": "./" /* Base directory to resolve non-absolute module names. */, 11 | // "paths": { 12 | // "typescript": ["../node_modules/typescript"] 13 | // } /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */, 14 | // "rootDirs": ["./", "../"], /* List of root folders whose combined content represents the structure of the project at runtime. */ 15 | // "typeRoots": ["../node_modules/typescript/lib"], /* List of folders to include type definitions from. */ 16 | // "types": ["../node_modules/typescript/lib/typescript.d.ts"], /* Type declaration files to be included in compilation. */ 17 | "allowSyntheticDefaultImports": true /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */, 18 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 19 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 20 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 21 | 22 | /* Source Map Options */ 23 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 24 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 25 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 26 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | // "incremental": true /* Enable incremental compilation */, 5 | "target": "es5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */, 6 | "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, 7 | // "lib": [], /* Specify library files to be included in the compilation. */ 8 | // "allowJs": true /* Allow javascript files to be compiled. */, 9 | // "checkJs": true /* Report errors in .js files. */, 10 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 11 | "declaration": true /* Generates corresponding '.d.ts' file. */, 12 | "declarationMap": true /* Generates a sourcemap for each corresponding '.d.ts' file. */, 13 | "sourceMap": true /* Generates corresponding '.map' file. */, 14 | // "outFile": "./", /* Concatenate and emit output to single file. */ 15 | "outDir": "./dist" /* Redirect output structure to the directory. */, 16 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 17 | // "composite": true, /* Enable project compilation */ 18 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 19 | "removeComments": true, /* Do not emit comments to output. */ 20 | 21 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 22 | /* Strict Type-Checking Options */ 23 | "strict": true, /* Enable all strict type-checking options. */ 24 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 25 | // "strictNullChecks": true, /* Enable strict null checks. */ 26 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 27 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 28 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 29 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 30 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 31 | 32 | /* Additional Checks */ 33 | "noUnusedLocals": false, /* Report errors on unused locals. */ 34 | "noUnusedParameters": false, /* Report errors on unused parameters. */ 35 | "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 36 | "noFallthroughCasesInSwitch": false, /* Report errors for fallthrough cases in switch statement. */ 37 | 38 | /* Module Resolution Options */ 39 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 40 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 41 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 42 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 43 | // "typeRoots": [], /* List of folders to include type definitions from. */ 44 | // "types": [], /* Type declaration files to be included in compilation. */ 45 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 46 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 47 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 48 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 49 | 50 | /* Source Map Options */ 51 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 52 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 53 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 54 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 55 | }, 56 | "exclude": ["dist", "module", "tests", "node_modules", "webpack-example", "ts-node-example"], 57 | } 58 | -------------------------------------------------------------------------------- /ts-node-example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | // "incremental": true, /* Enable incremental compilation */ 5 | "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ 6 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 7 | // "lib": [], /* Specify library files to be included in the compilation. */ 8 | "allowJs": true, /* Allow javascript files to be compiled. */ 9 | "checkJs": true, /* Report errors in .js files. */ 10 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 11 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 12 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 13 | "sourceMap": true, /* Generates corresponding '.map' file. */ 14 | // "outFile": "./", /* Concatenate and emit output to single file. */ 15 | // "outDir": "./", /* Redirect output structure to the directory. */ 16 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 17 | // "composite": true, /* Enable project compilation */ 18 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 19 | // "removeComments": true, /* Do not emit comments to output. */ 20 | // "noEmit": true, /* Do not emit outputs. */ 21 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 22 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 23 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 24 | 25 | /* Strict Type-Checking Options */ 26 | "strict": true, /* Enable all strict type-checking options. */ 27 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 28 | // "strictNullChecks": true, /* Enable strict null checks. */ 29 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 30 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 31 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 32 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 33 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 34 | 35 | /* Additional Checks */ 36 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 37 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 38 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 39 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 40 | 41 | /* Module Resolution Options */ 42 | "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 43 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 44 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 45 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 46 | // "typeRoots": [], /* List of folders to include type definitions from. */ 47 | // "types": [], /* Type declaration files to be included in compilation. */ 48 | "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 49 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 50 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 51 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 52 | 53 | /* Source Map Options */ 54 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 55 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 56 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 57 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 58 | 59 | /* Experimental Options */ 60 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 61 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 62 | }, 63 | "exclude": ["dist", "node_modules"] 64 | } 65 | -------------------------------------------------------------------------------- /webpack-example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | // "incremental": true, /* Enable incremental compilation */ 5 | "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ 6 | "module": "esnext", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 7 | // "lib": [], /* Specify library files to be included in the compilation. */ 8 | "allowJs": true, /* Allow javascript files to be compiled. */ 9 | "checkJs": true, /* Report errors in .js files. */ 10 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 11 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 12 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 13 | "sourceMap": true, /* Generates corresponding '.map' file. */ 14 | // "outFile": "./", /* Concatenate and emit output to single file. */ 15 | // "outDir": "./", /* Redirect output structure to the directory. */ 16 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 17 | // "composite": true, /* Enable project compilation */ 18 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 19 | // "removeComments": true, /* Do not emit comments to output. */ 20 | // "noEmit": true, /* Do not emit outputs. */ 21 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 22 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 23 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 24 | 25 | /* Strict Type-Checking Options */ 26 | "strict": true, /* Enable all strict type-checking options. */ 27 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 28 | // "strictNullChecks": true, /* Enable strict null checks. */ 29 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 30 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 31 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 32 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 33 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 34 | 35 | /* Additional Checks */ 36 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 37 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 38 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 39 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 40 | 41 | /* Module Resolution Options */ 42 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 43 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 44 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 45 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 46 | // "typeRoots": [], /* List of folders to include type definitions from. */ 47 | // "types": [], /* Type declaration files to be included in compilation. */ 48 | "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 49 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 50 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 51 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 52 | 53 | /* Source Map Options */ 54 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 55 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 56 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 57 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 58 | 59 | /* Experimental Options */ 60 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 61 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 62 | }, 63 | "exclude": ["dist", "node_modules"] 64 | } 65 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Superstruct Typescript transformer 2 | 3 |

4 | 5 | Npm 6 | 7 | 8 | Travis CI build status 9 | 10 |

11 | 12 | It's a typescript transformer that will transforms `validate(JSON.parse("{}"))` calls to an actual [`superstruct` json validator](https://github.com/ianstormtaylor/superstruct) 13 | 14 | You write that code: 15 | 16 | ```typescript 17 | import { validate } from "superstruct-ts-transformer"; 18 | 19 | type User = { 20 | name: string; 21 | alive: boolean; 22 | }; 23 | 24 | const obj = validate(JSON.parse('{ "name": "Me", "alive": true }')); 25 | ``` 26 | 27 | and it will become when you'll compile it 28 | 29 | ```js 30 | import * as superstruct from "superstruct"; 31 | var obj = validate_User(JSON.parse('{ "name": "Me", "alive": true }')); 32 | 33 | function validate_User(jsonObj) { 34 | var validator = superstruct.struct({ 35 | name: "string", 36 | alive: "boolean" 37 | }); 38 | return validator(jsonObj); 39 | } 40 | ``` 41 | 42 | ## ⚠️ Current limitations ⚠️ 43 | 44 | Please read this carefully as you may miss it and be really disappointed afterward 45 | 46 | ### You can't use babel-only transpilation 47 | 48 | You can use babel, but you need to compile by typescript first. Babel plugin is in plans, but not the top priority right now. 49 | 50 | ### You can't use `tsc` 51 | 52 | You need to use [ttypescript](`https://github.com/cevek/ttypescript`), because `tsc` doesn't support custom transformers. 53 | 54 | ### Module target should be `CommonJS`, `ES2015` and `ESNext` 55 | 56 | You can't use other module targets. Also not a show stopper, haven't seen anyone using `UMD` or `AMD` for applications. 57 | 58 | ### It's only JSON validation 59 | 60 | No more, no less. Everything that's not representable in JSON or doesn't have a standard representation is out of the scope of this library, e.g. BigInts, functions, objects with a number indexer. You may want to keep an eye on a custom validators feature though. 61 | 62 | ## Installation 63 | 64 | ```bash 65 | npm i -D superstruct-ts-transformer 66 | # or 67 | yarn add --dev superstruct-ts-transformer 68 | ``` 69 | 70 | ## Usage 71 | 72 | ```typescript 73 | // you import validate function from "superstruct-ts-transformer" package 74 | import { validate } from "superstruct-ts-transformer"; 75 | 76 | // You define or use or import your own type 77 | type User = { 78 | name: string; 79 | alive: boolean; 80 | }; 81 | 82 | // You call this validate function passing your type as generic 83 | // and json parse result as an argument 84 | const obj = validate(JSON.parse('{ "name": "Me", "alive": true }')); 85 | ``` 86 | 87 | ### Custom validators 88 | 89 | You can pass custom validators array as second argument. This custom validator should comply with 2 rules 90 | 91 | * They must be [user-defined type guards](https://www.typescriptlang.org/docs/handbook/advanced-types.html#user-defined-type-guards) and for 92 | * For primitives (`string`, `number`, `bool`) they must be nominal (`type Uuid = string` won't do, typescript will see it as just `string`, rather than unique type). 93 | There's no official way to do this, but you [hack your way through](https://basarat.gitbook.io/typescript/main-1/nominaltyping) 94 | 95 | ```typescript 96 | // Import validate as usual 97 | import { validate } from "superstruct-ts-transformer"; 98 | 99 | // Define a brand type 100 | // Right here I'm using a different kind of brand hack, feel free to use any 101 | type Uuid = string & { readonly __brand: unique symbol }; 102 | 103 | const uuidRegExp = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; 104 | 105 | // Define user-defined type guard that returns `value is Uuid` (value is name of an argument, Uuid is your brand type) 106 | const isUuid = (value: unknown): value is Uuid => 107 | typeof value === "string" && !!value && uuidRegExp.test(value); 108 | 109 | // You can also wrap imported function as user-defined type guards, e.g. 110 | // import _isUuid from 'is-uuid' 111 | // const isUuid = (value: unknown): value is Uuid => _isUuid 112 | 113 | // Use this brand type 114 | type TestType = { id: Uuid }; 115 | 116 | // Call validate function passing isUuis as second parameter array element 117 | export const obj = validate( w 118 | JSON.parse('{ "id": "a4e1b0cf-2a08-4297-83f3-4db896d7e0fb" }'), 119 | [isUuid] 120 | ); 121 | // Transformer will use isUuid function when it will encounter Uuid type 122 | ``` 123 | 124 | ### ⚠️ Usage limitations ⚠️ 125 | 126 | #### Don't wrap or curry `validate` function and don't reexport it 127 | 128 | Don't do this 129 | 130 | ```ts 131 | const myValidate = (jsonStr: string) => validate(JSON.parse(jsonStr)); 132 | ``` 133 | 134 | And don't do this 135 | 136 | ```ts 137 | import { validate } from "superstruct-ts-transformer"; 138 | export default validate; 139 | ``` 140 | 141 | Basically just don't be fancy with this function, just import and use it 142 | 143 | ## How to integrate custom transformer 144 | 145 | The usage itself is really concise, injecting custom transformer can be trickier 146 | 147 | ### Webpack with ts-loader integration 148 | 149 | 1. Import the transformer 150 | ```js 151 | // Mind the destructuring 152 | const { 153 | createValidatorTransformer 154 | } = require("superstruct-ts-transformer/dist/transformer"); 155 | ``` 156 | 2. Add the transformer to the ts-loader config, so it'll look like this 157 | ```js 158 | { 159 | test: /\.tsx?$/, 160 | use: [{ 161 | loader: "ts-loader", 162 | options: { 163 | // provide your options here if you need it 164 | getCustomTransformers: program => ({ 165 | before: [createValidatorTransformer(program)] // <-- custom transfomer configuration 166 | }) 167 | } 168 | }] 169 | } 170 | ``` 171 | Take a look at [`ts-loader` docs](https://github.com/TypeStrong/ts-loader#options) if in hesitation. 172 | Also take a look at [tiny webpack example](/webpack-example) 173 | 174 | ### Webpack with `awesome-typescript-loader` integration 175 | 176 | To be written 177 | 178 | ### `ts-node` integration 179 | 180 | 1. Create a `run.js` wrapper to pass the transformer programmatically 181 | ``` 182 | const { 183 | createValidatorTransformer 184 | } = require("superstruct-ts-transformer/dist/transformer"); 185 | 186 | // This is the almost the same at using `ts-node` or `node -r ts-node/register` 187 | // However it allows us to pass transformers 188 | // This should be executed before you start requiring ts and tsx files 189 | require("ts-node").register({ 190 | programTransformers: program => ({ 191 | before: [createValidatorTransformer(program)] // <-- custom transfomer configuration 192 | // don't forget that it's an array 193 | }) 194 | }); 195 | 196 | // require your real entrypoint 197 | require("./index.ts"); 198 | ``` 199 | 2. Make use of that script by directly passing it to node, instead of using `ts-node` 200 | If you need to pass specifically options to `ts-node` do it in `run.js` in `register` call 201 | ``` 202 | // package.json 203 | "scripts": { 204 | "start": "node run.js" 205 | } 206 | ``` 207 | 208 | In doubt take a look at [tiny ts-node example](/ts-node-example) 209 | -------------------------------------------------------------------------------- /ts-node-example/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ts-node-example", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@types/superstruct": { 8 | "version": "0.5.0", 9 | "resolved": "https://registry.npmjs.org/@types/superstruct/-/superstruct-0.5.0.tgz", 10 | "integrity": "sha512-ieqVuj0o0G+yVzjv4r7evT6tjIoESocymOenpcAbXKjTjsQT3M2RN7+pBILCZ/ymC8nqIe7apCTIGnPjqpnjKQ==" 11 | }, 12 | "arg": { 13 | "version": "4.1.1", 14 | "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.1.tgz", 15 | "integrity": "sha512-SlmP3fEA88MBv0PypnXZ8ZfJhwmDeIE3SP71j37AiXQBXYosPV0x6uISAaHYSlSVhmHOVkomen0tbGk6Anlebw==", 16 | "dev": true 17 | }, 18 | "buffer-from": { 19 | "version": "1.1.1", 20 | "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", 21 | "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", 22 | "dev": true 23 | }, 24 | "clone-deep": { 25 | "version": "2.0.2", 26 | "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-2.0.2.tgz", 27 | "integrity": "sha512-SZegPTKjCgpQH63E+eN6mVEEPdQBOUzjyJm5Pora4lrwWRFS8I0QAxV/KD6vV/i0WuijHZWQC1fMsPEdxfdVCQ==", 28 | "requires": { 29 | "for-own": "^1.0.0", 30 | "is-plain-object": "^2.0.4", 31 | "kind-of": "^6.0.0", 32 | "shallow-clone": "^1.0.0" 33 | } 34 | }, 35 | "diff": { 36 | "version": "4.0.1", 37 | "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.1.tgz", 38 | "integrity": "sha512-s2+XdvhPCOF01LRQBC8hf4vhbVmI2CGS5aZnxLJlT5FtdhPCDFq80q++zK2KlrVorVDdL5BOGZ/VfLrVtYNF+Q==", 39 | "dev": true 40 | }, 41 | "for-in": { 42 | "version": "1.0.2", 43 | "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", 44 | "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=" 45 | }, 46 | "for-own": { 47 | "version": "1.0.0", 48 | "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz", 49 | "integrity": "sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs=", 50 | "requires": { 51 | "for-in": "^1.0.1" 52 | } 53 | }, 54 | "is-extendable": { 55 | "version": "0.1.1", 56 | "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", 57 | "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=" 58 | }, 59 | "is-plain-object": { 60 | "version": "2.0.4", 61 | "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", 62 | "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", 63 | "requires": { 64 | "isobject": "^3.0.1" 65 | } 66 | }, 67 | "isobject": { 68 | "version": "3.0.1", 69 | "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", 70 | "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" 71 | }, 72 | "kind-of": { 73 | "version": "6.0.2", 74 | "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", 75 | "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" 76 | }, 77 | "make-error": { 78 | "version": "1.3.5", 79 | "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.5.tgz", 80 | "integrity": "sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g==", 81 | "dev": true 82 | }, 83 | "mixin-object": { 84 | "version": "2.0.1", 85 | "resolved": "https://registry.npmjs.org/mixin-object/-/mixin-object-2.0.1.tgz", 86 | "integrity": "sha1-T7lJRB2rGCVA8f4DW6YOGUel5X4=", 87 | "requires": { 88 | "for-in": "^0.1.3", 89 | "is-extendable": "^0.1.1" 90 | }, 91 | "dependencies": { 92 | "for-in": { 93 | "version": "0.1.8", 94 | "resolved": "https://registry.npmjs.org/for-in/-/for-in-0.1.8.tgz", 95 | "integrity": "sha1-2Hc5COMSVhCZUrH9ubP6hn0ndeE=" 96 | } 97 | } 98 | }, 99 | "shallow-clone": { 100 | "version": "1.0.0", 101 | "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-1.0.0.tgz", 102 | "integrity": "sha512-oeXreoKR/SyNJtRJMAKPDSvd28OqEwG4eR/xc856cRGBII7gX9lvAqDxusPm0846z/w/hWYjI1NpKwJ00NHzRA==", 103 | "requires": { 104 | "is-extendable": "^0.1.1", 105 | "kind-of": "^5.0.0", 106 | "mixin-object": "^2.0.1" 107 | }, 108 | "dependencies": { 109 | "kind-of": { 110 | "version": "5.1.0", 111 | "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", 112 | "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" 113 | } 114 | } 115 | }, 116 | "source-map": { 117 | "version": "0.6.1", 118 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", 119 | "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", 120 | "dev": true 121 | }, 122 | "source-map-support": { 123 | "version": "0.5.13", 124 | "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", 125 | "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", 126 | "dev": true, 127 | "requires": { 128 | "buffer-from": "^1.0.0", 129 | "source-map": "^0.6.0" 130 | } 131 | }, 132 | "superstruct": { 133 | "version": "0.6.2", 134 | "resolved": "https://registry.npmjs.org/superstruct/-/superstruct-0.6.2.tgz", 135 | "integrity": "sha512-lvA97MFAJng3rfjcafT/zGTSWm6Tbpk++DP6It4Qg7oNaeM+2tdJMuVgGje21/bIpBEs6iQql1PJH6dKTjl4Ig==", 136 | "requires": { 137 | "clone-deep": "^2.0.1", 138 | "kind-of": "^6.0.1" 139 | } 140 | }, 141 | "superstruct-ts-transformer": { 142 | "version": "0.1.1", 143 | "resolved": "https://registry.npmjs.org/superstruct-ts-transformer/-/superstruct-ts-transformer-0.1.1.tgz", 144 | "integrity": "sha512-d6ha06UdAjZhsngau4ZmD0XFSNCwSm8o16O1I0HcLVogmttDpzrugIOzA0/+zhnRmH61HygiQH/FPNwcSyw2Jg==", 145 | "requires": { 146 | "@types/superstruct": "^0.5.0", 147 | "superstruct": "^0.6.1", 148 | "ts-type-visitor": "^0.1.1" 149 | } 150 | }, 151 | "ts-node": { 152 | "version": "8.3.0", 153 | "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.3.0.tgz", 154 | "integrity": "sha512-dyNS/RqyVTDcmNM4NIBAeDMpsAdaQ+ojdf0GOLqE6nwJOgzEkdRNzJywhDfwnuvB10oa6NLVG1rUJQCpRN7qoQ==", 155 | "dev": true, 156 | "requires": { 157 | "arg": "^4.1.0", 158 | "diff": "^4.0.1", 159 | "make-error": "^1.1.1", 160 | "source-map-support": "^0.5.6", 161 | "yn": "^3.0.0" 162 | } 163 | }, 164 | "ts-type-visitor": { 165 | "version": "0.1.3", 166 | "resolved": "https://registry.npmjs.org/ts-type-visitor/-/ts-type-visitor-0.1.3.tgz", 167 | "integrity": "sha512-CKhzWxBFGysqYn84LAy6HGIBkmeCKi+luKH4SGVPRHYURJsWgy2SQkzy8Wm19CegXj1jLfA+7faCECmbRF8scA==" 168 | }, 169 | "yn": { 170 | "version": "3.1.1", 171 | "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", 172 | "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", 173 | "dev": true 174 | } 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /transformer.ts: -------------------------------------------------------------------------------- 1 | import ts, { SignatureKind } from "typescript"; 2 | import { TypeModel, typeVisitor, TypeModelObject } from "ts-type-visitor"; 3 | import { 4 | createSuperStructDotSuperstructPropAccess, 5 | createSuperStructValidatorForm 6 | } from "./validator"; 7 | 8 | const flatten = (arr: T[][]) => 9 | arr.reduce((acc, curr) => [...acc, ...curr], []); 10 | 11 | const createSuperStructValidator = ( 12 | typeModel: TypeModel, 13 | functionName: string, 14 | strictNullChecks: boolean, 15 | customValidators?: Map 16 | ) => { 17 | const customValidatorsObjectLiteralProps: ts.PropertyAssignment[] = []; 18 | 19 | if (customValidators && customValidators.size > 0) { 20 | customValidators?.forEach(funcName => { 21 | customValidatorsObjectLiteralProps.push( 22 | ts.createPropertyAssignment(funcName, ts.createIdentifier(funcName)) 23 | ); 24 | }); 25 | } 26 | 27 | const superstructStructVariable = ts.createVariableStatement( 28 | /* modifiers */ undefined, 29 | /* declarations */ [ 30 | ts.createVariableDeclaration( 31 | /* name */ "struct", 32 | /* type */ undefined, 33 | /* initializer */ ts.createCall( 34 | /* modifiers */ createSuperStructDotSuperstructPropAccess(), 35 | /* typeParameters */ undefined, 36 | /* arguments */ [ 37 | ts.createObjectLiteral([ 38 | ts.createPropertyAssignment( 39 | "types", 40 | ts.createObjectLiteral(customValidatorsObjectLiteralProps) 41 | ) 42 | ]) 43 | ] 44 | ) 45 | ) 46 | ] 47 | ); 48 | 49 | const superstructValidator = ts.createCall( 50 | /* expression */ ts.createIdentifier("struct"), 51 | /* typeParameters */ undefined, 52 | /* arguments */ [ 53 | createSuperStructValidatorForm( 54 | typeModel, 55 | /* optional */ false, 56 | /* strictNullChecks */ strictNullChecks, 57 | customValidators 58 | ) 59 | ] 60 | ); 61 | 62 | const superstructValidatorVariable = ts.createVariableStatement( 63 | /* modifiers */ undefined, 64 | /* declarations */ [ 65 | ts.createVariableDeclaration( 66 | /* name */ "validator", 67 | /* type */ undefined, 68 | /* initializer */ superstructValidator 69 | ) 70 | ] 71 | ); 72 | 73 | const validatorCall = ts.createCall( 74 | /* expression */ ts.createIdentifier("validator"), 75 | /* typeParameters */ undefined, 76 | /* arguments */ [ts.createIdentifier("jsonObj")] 77 | ); 78 | 79 | const body = ts.createBlock( 80 | [ 81 | superstructStructVariable, 82 | superstructValidatorVariable, 83 | ts.createReturn(validatorCall) 84 | ], 85 | /* multiline */ true 86 | ); 87 | 88 | const validateFunc = ts.createFunctionDeclaration( 89 | /* decorators */ undefined, 90 | /* modifiers */ undefined, 91 | /* asteriskToken */ undefined, 92 | /* name */ functionName, 93 | /* typeParameters */ undefined, 94 | /* parameters */ [ 95 | ts.createParameter( 96 | undefined, 97 | undefined, 98 | undefined, 99 | "jsonObj", 100 | undefined, 101 | undefined, 102 | undefined 103 | ) 104 | ], 105 | /* type */ undefined, 106 | /* body */ body 107 | ); 108 | 109 | return validateFunc; 110 | }; 111 | 112 | type CallToImplement = { 113 | typeModel: TypeModel; 114 | functionName: string; 115 | customValidators: Map; 116 | }; 117 | 118 | function isOurModule(moduleName: string) { 119 | if (process.env.SUPERSTRUCT_TS_TRANSFORMER_ENV === "debug") { 120 | return moduleName == "../index"; 121 | } 122 | return moduleName == "superstruct-ts-transformer"; 123 | } 124 | 125 | const createVisitor = ( 126 | ctx: ts.TransformationContext, 127 | sourceFile: ts.SourceFile, 128 | checker: ts.TypeChecker 129 | ) => { 130 | const typeModels = new Map(); 131 | 132 | const visitor: ts.Visitor = (node: ts.Node) => { 133 | const pass = () => ts.visitEachChild(node, visitor, ctx); 134 | 135 | if ( 136 | ts.isImportDeclaration(node) && 137 | ts.isStringLiteral(node.moduleSpecifier) && 138 | isOurModule(node.moduleSpecifier.text) 139 | ) { 140 | const moduleTarget = ctx.getCompilerOptions().module; 141 | 142 | if (moduleTarget === ts.ModuleKind.CommonJS) { 143 | return ts.createVariableStatement( 144 | /* modifiers */ [ts.createModifier(ts.SyntaxKind.ConstKeyword)], 145 | /* declarations */ [ 146 | ts.createVariableDeclaration( 147 | /* name */ "superstruct", 148 | /* type */ undefined, 149 | /* initializer */ ts.createCall( 150 | /* expression */ ts.createIdentifier("require"), 151 | /* type args */ undefined, 152 | /* args */ [ts.createLiteral("superstruct")] 153 | ) 154 | ) 155 | ] 156 | ); 157 | } else if ( 158 | moduleTarget === ts.ModuleKind.ES2015 || 159 | moduleTarget === ts.ModuleKind.ESNext 160 | ) { 161 | const superstructStructImportClause = ts.createImportClause( 162 | /* name */ undefined, 163 | /* named bindings */ ts.createNamespaceImport( 164 | /* name */ ts.createIdentifier("superstruct") 165 | ) 166 | ); 167 | 168 | return ts.createImportDeclaration( 169 | /* decorators */ undefined, 170 | /* modifiers */ node.modifiers, 171 | /* import clause */ superstructStructImportClause, 172 | /* module specifier */ ts.createStringLiteral("superstruct") 173 | ); 174 | } else { 175 | throw new Error( 176 | "superstruct-ts-transformer doesn't support module targets other than CommonJS and ES2015+" 177 | ); 178 | } 179 | } 180 | 181 | if (ts.isSourceFile(node)) { 182 | const newFileNode = ts.visitEachChild(node, visitor, ctx); 183 | const options = ctx.getCompilerOptions(); 184 | 185 | const newValidators = flatten( 186 | Array.from(typeModels.values()).map(callsToImplement => 187 | callsToImplement.map(callToImplement => 188 | createSuperStructValidator( 189 | callToImplement.typeModel, 190 | callToImplement.functionName, 191 | options.strict || options.strictNullChecks || false, 192 | callToImplement.customValidators 193 | ) 194 | ) 195 | ) 196 | ); 197 | 198 | const fileNodeWithValidators = ts.updateSourceFileNode(newFileNode, [ 199 | ...newFileNode.statements, 200 | ...newValidators 201 | ]); 202 | 203 | return fileNodeWithValidators; 204 | } 205 | 206 | if ( 207 | ts.isCallExpression(node) && 208 | ts.isIdentifier(node.expression) && 209 | node.typeArguments && 210 | node.typeArguments.length > 0 && 211 | node.arguments.length > 0 212 | ) { 213 | const sym = checker.getSymbolAtLocation(node.expression); 214 | 215 | if ( 216 | !!sym && 217 | sym.declarations.some( 218 | decl => 219 | ts.isNamedImports(decl.parent) && 220 | ts.isImportClause(decl.parent.parent) && 221 | ts.isImportDeclaration(decl.parent.parent.parent) && 222 | ts.isStringLiteral(decl.parent.parent.parent.moduleSpecifier) && 223 | isOurModule(decl.parent.parent.parent.moduleSpecifier.text) 224 | ) 225 | ) { 226 | const typeToValidateAgainst = checker.getTypeFromTypeNode( 227 | node.typeArguments[0] 228 | ); 229 | 230 | const typeModel = typeVisitor(checker, typeToValidateAgainst); 231 | const typeToValidateAgainstStr = checker 232 | .typeToString(typeToValidateAgainst) 233 | .replace(/\[/g, "ARRAY_") 234 | .replace(/\]/g, "_ENDARRAY") 235 | .replace(/\s/g, "_") 236 | .replace(/[\,]/g, ""); 237 | 238 | const functionName = node.expression.text; 239 | 240 | const newFunctionName = `${functionName}_${typeToValidateAgainstStr}`; 241 | 242 | let customValidators = new Map(); 243 | 244 | const customValidatorsArg = node.arguments[1]; 245 | if ( 246 | customValidatorsArg && 247 | ts.isArrayLiteralExpression(customValidatorsArg) 248 | ) { 249 | customValidatorsArg.elements.forEach(element => { 250 | const sym = checker.getSymbolAtLocation(element); 251 | const type = checker.getTypeAtLocation(element); 252 | const sigs = checker.getSignaturesOfType(type, SignatureKind.Call); 253 | // const sig = type.; 254 | // const name = sig. 255 | // const decl = sig?.declaration; 256 | // const decl = sym?.getDeclarations()?.[0] as ts.SignatureDeclaration | undefined; 257 | // const type2 = decl?.type; 258 | const decl = sigs[0]?.declaration; 259 | const typeNode = decl?.type; 260 | const typeNeeded = 261 | typeNode && ts.isTypeNode(typeNode) 262 | ? checker.getTypeFromTypeNode(typeNode) 263 | : undefined; 264 | 265 | // if (typeNeeded) { 266 | 267 | if (typeNode && ts.isTypePredicateNode(typeNode) && typeNode.type) { 268 | customValidators.set( 269 | checker.getTypeFromTypeNode(typeNode.type), 270 | element.getText() 271 | ); 272 | // customValidators.set(typeNeeded, element.getText()); 273 | } 274 | // decl?.getChildren().forEach(child => { 275 | // if (ts.isTypePredicateNode(child) && child.type) { 276 | // customValidators.set( 277 | // checker.getTypeFromTypeNode(child.type), 278 | // "isUuid" 279 | // ); 280 | // } 281 | // }); 282 | }); 283 | } 284 | 285 | const newCallToImplement: CallToImplement = { 286 | typeModel, 287 | functionName: newFunctionName, 288 | customValidators 289 | }; 290 | 291 | if (typeModels.has(sourceFile)) { 292 | typeModels.set(sourceFile, [ 293 | ...typeModels.get(sourceFile)!, 294 | newCallToImplement 295 | ]); 296 | } else { 297 | typeModels.set(sourceFile, [newCallToImplement]); 298 | } 299 | 300 | return ts.createCall( 301 | /* expression */ ts.createIdentifier(newFunctionName), 302 | /* type argmuents */ undefined, 303 | /* arguments */ node.arguments.slice(0, 1) 304 | ); 305 | } 306 | } 307 | 308 | return pass(); 309 | }; 310 | 311 | return visitor; 312 | }; 313 | 314 | export const createValidatorTransformer = (program: ts.Program) => ( 315 | ctx: ts.TransformationContext 316 | ): ts.Transformer => (sf: ts.SourceFile) => 317 | ts.visitNode(sf, createVisitor(ctx, sf, program.getTypeChecker())); 318 | -------------------------------------------------------------------------------- /validator.ts: -------------------------------------------------------------------------------- 1 | import { TypeModelObject, TypeModel } from "ts-type-visitor"; 2 | import ts from "typescript"; 3 | 4 | export const createSuperStructDotStructPropAccess = () => 5 | ts.createPropertyAccess(ts.createIdentifier("superstruct"), "struct"); 6 | 7 | export const createSuperStructDotSuperstructPropAccess = () => 8 | ts.createPropertyAccess(ts.createIdentifier("superstruct"), "superstruct"); 9 | 10 | type SuperstructType = 11 | | "any" 12 | | "arguments" 13 | | "array" 14 | | "boolean" 15 | | "buffer" 16 | | "date" 17 | | "error" 18 | | "function" 19 | | "generatorfunction" 20 | | "map" 21 | | "null" 22 | | "number" 23 | | "object" 24 | | "promise" 25 | | "regexp" 26 | | "set" 27 | | "string" 28 | | "symbol" 29 | | "undefined" 30 | | "weakmap" 31 | | "weakset"; 32 | 33 | type SuperstructFunc = 34 | | "array" 35 | | "enum" 36 | | "function" 37 | | "instance" 38 | | "interface" 39 | | "intersection" 40 | | "lazy" 41 | | "dynamic" 42 | | "literal" 43 | | "object" 44 | | "optional" 45 | | "pick" 46 | | "record" 47 | | "scalar" 48 | | "tuple" 49 | | "union"; 50 | 51 | const createSuperstructObjectLiteralFromProps = ({ 52 | props, 53 | strictNullChecks 54 | }: { 55 | props: TypeModelObject["props"]; 56 | strictNullChecks: boolean; 57 | }) => 58 | ts.createObjectLiteral( 59 | /* properties */ 60 | props.map(prop => 61 | ts.createPropertyAssignment( 62 | prop.name, 63 | createSuperStructValidatorForm(prop, prop.optional, strictNullChecks) 64 | ) 65 | ), 66 | /* multiline */ true 67 | ); 68 | 69 | const createSuperstructLiteral = ({ type: name }: { type: SuperstructType }) => 70 | ts.createStringLiteral(name); 71 | 72 | const wrapOptional = ({ 73 | exp, 74 | optional 75 | }: { 76 | exp: ts.Expression; 77 | optional: boolean; 78 | }) => 79 | optional ? createSuperstructCall({ func: "optional", args: [exp] }) : exp; 80 | 81 | const wrapNonStrictNullChecks = ({ 82 | exp, 83 | strictNullChecks 84 | }: { 85 | exp: ts.Expression; 86 | strictNullChecks: boolean; 87 | }) => 88 | !strictNullChecks 89 | ? createSuperstructCall({ 90 | func: "union", 91 | args: [ 92 | ts.createArrayLiteral([ 93 | createSuperstructLiteral({ type: "null" }), 94 | exp 95 | ]) 96 | ] 97 | }) 98 | : exp; 99 | 100 | const wrapOptionalOrNonStrictNullCheck = ({ 101 | exp, 102 | optional, 103 | strictNullChecks 104 | }: { 105 | exp: ts.Expression; 106 | optional: boolean; 107 | strictNullChecks: boolean; 108 | }) => 109 | wrapOptional({ 110 | exp: wrapNonStrictNullChecks({ exp, strictNullChecks }), 111 | optional 112 | }); 113 | 114 | const createSuperstructCall = ({ 115 | func, 116 | args 117 | }: { 118 | func: SuperstructFunc; 119 | args: ts.Expression[] | undefined; 120 | }) => 121 | ts.createCall( 122 | ts.createPropertyAccess(createSuperStructDotStructPropAccess(), func), 123 | /* type args */ undefined, 124 | /* args */ args 125 | ); 126 | 127 | export const createSuperStructValidatorForm = ( 128 | typeModel: TypeModel, 129 | optional: boolean, 130 | strictNullChecks: boolean, 131 | customValidators?: Map 132 | ): ts.Expression => { 133 | const funcName = 134 | typeModel.originalType && customValidators?.get(typeModel.originalType); 135 | 136 | if (funcName) { 137 | return ts.createStringLiteral(funcName); 138 | } 139 | 140 | switch (typeModel.kind) { 141 | case "any": 142 | case "unknown": 143 | case "esSymbol": // ES symbols can't be represented in json 144 | case "uniqueEsSymbol": 145 | case "void": // void can't be represented in json 146 | case "never": // never can't be represented in json 147 | case "typeParameter": // type parameter can't be represented in json 148 | case "bigintLiteral": // bigint doesn't have a consistent json representation 149 | case "bigint": // bigint doesn't have a consistent json representation 150 | case "bigintLiteral": // bigint doesn't have a consistent json representation 151 | return createSuperstructLiteral({ 152 | type: "any" 153 | }); 154 | 155 | case "enum": 156 | case "enumLiteral": 157 | return wrapOptionalOrNonStrictNullCheck({ 158 | exp: createSuperstructCall({ 159 | func: "union", 160 | args: [ 161 | ts.createArrayLiteral( 162 | typeModel.values.map(t => 163 | createSuperStructValidatorForm(t, false, strictNullChecks) 164 | ) 165 | ) 166 | ] 167 | }), 168 | optional, 169 | strictNullChecks 170 | }); 171 | 172 | case "string": 173 | return wrapOptionalOrNonStrictNullCheck({ 174 | exp: createSuperstructLiteral({ type: "string" }), 175 | optional, 176 | strictNullChecks 177 | }); 178 | 179 | case "number": 180 | return wrapOptionalOrNonStrictNullCheck({ 181 | exp: createSuperstructLiteral({ type: "number" }), 182 | optional, 183 | strictNullChecks 184 | }); 185 | 186 | case "boolean": 187 | return wrapOptionalOrNonStrictNullCheck({ 188 | exp: createSuperstructLiteral({ type: "boolean" }), 189 | optional, 190 | strictNullChecks 191 | }); 192 | 193 | case "stringLiteral": 194 | return wrapOptionalOrNonStrictNullCheck({ 195 | exp: createSuperstructCall({ 196 | func: "literal", 197 | args: [ts.createLiteral(typeModel.value)] 198 | }), 199 | optional, 200 | strictNullChecks 201 | }); 202 | 203 | case "numberLiteral": 204 | return wrapOptionalOrNonStrictNullCheck({ 205 | exp: createSuperstructCall({ 206 | func: "literal", 207 | args: [ts.createLiteral(typeModel.value)] 208 | }), 209 | optional, 210 | strictNullChecks 211 | }); 212 | 213 | case "booleanLiteral": 214 | return wrapOptionalOrNonStrictNullCheck({ 215 | exp: createSuperstructCall({ 216 | func: "literal", 217 | args: [ts.createLiteral(typeModel.value)] 218 | }), 219 | optional, 220 | strictNullChecks 221 | }); 222 | 223 | case "undefined": 224 | return wrapOptionalOrNonStrictNullCheck({ 225 | exp: createSuperstructLiteral({ type: "undefined" }), 226 | optional, 227 | strictNullChecks 228 | }); 229 | 230 | case "null": 231 | return wrapOptionalOrNonStrictNullCheck({ 232 | exp: createSuperstructLiteral({ type: "null" }), 233 | optional, 234 | strictNullChecks 235 | }); 236 | 237 | case "objectWithIndex": 238 | return wrapOptionalOrNonStrictNullCheck({ 239 | exp: createSuperstructCall({ 240 | func: "intersection", 241 | args: [ 242 | ts.createArrayLiteral([ 243 | createSuperstructCall({ 244 | func: "interface", 245 | args: [ 246 | createSuperstructObjectLiteralFromProps({ 247 | props: typeModel.props, 248 | strictNullChecks 249 | }) 250 | ] 251 | }), 252 | createSuperStructValidatorForm( 253 | typeModel.index, 254 | false, 255 | strictNullChecks 256 | ) 257 | ]) 258 | ] 259 | }), 260 | optional, 261 | strictNullChecks 262 | }); 263 | 264 | case "object": { 265 | return wrapOptionalOrNonStrictNullCheck({ 266 | exp: createSuperstructObjectLiteralFromProps({ 267 | props: typeModel.props, 268 | strictNullChecks 269 | }), 270 | optional, 271 | strictNullChecks 272 | }); 273 | } 274 | 275 | case "union": 276 | return wrapOptionalOrNonStrictNullCheck({ 277 | exp: createSuperstructCall({ 278 | func: "union", 279 | args: [ 280 | ts.createArrayLiteral( 281 | typeModel.types.map(t => 282 | createSuperStructValidatorForm(t, false, strictNullChecks) 283 | ) 284 | ) 285 | ] 286 | }), 287 | optional, 288 | strictNullChecks 289 | }); 290 | 291 | case "index": 292 | if (typeModel.keyType.kind === "number") { 293 | // number object keys can't represented in json 294 | return createSuperstructLiteral({ 295 | type: "any" 296 | }); 297 | } else { 298 | return wrapOptionalOrNonStrictNullCheck({ 299 | exp: createSuperstructCall({ 300 | func: "record", 301 | args: [ 302 | ts.createArrayLiteral([ 303 | createSuperStructValidatorForm(typeModel.keyType, false, true), 304 | createSuperStructValidatorForm( 305 | typeModel.valueType, 306 | false, 307 | strictNullChecks 308 | ) 309 | ]) 310 | ] 311 | }), 312 | optional, 313 | strictNullChecks 314 | }); 315 | } 316 | 317 | case "intersection": 318 | return wrapOptionalOrNonStrictNullCheck({ 319 | exp: createSuperstructCall({ 320 | func: "intersection", 321 | args: [ 322 | ts.createArrayLiteral( 323 | typeModel.types.map(t => 324 | createSuperStructValidatorForm(t, false, strictNullChecks) 325 | ) 326 | ) 327 | ] 328 | }), 329 | optional, 330 | strictNullChecks 331 | }); 332 | 333 | case "indexedAccess": 334 | // TODO implement indexedAccess superstruct 335 | throw new Error("implement indexedAccess superstruct"); 336 | 337 | case "conditional": 338 | // TODO implement conditional superstruct 339 | throw new Error("implement conditional superstruct"); 340 | 341 | case "substitution": 342 | // TODO implement substitution superstruct 343 | throw new Error("implement substitution superstruct"); 344 | 345 | case "nonPrimitive": 346 | // TODO implement nonPrimitive superstruct 347 | throw new Error("implement nonPrimitive superstruct"); 348 | 349 | case "unidentified": 350 | return ts.createStringLiteral("any"); 351 | 352 | case "array": 353 | return wrapOptionalOrNonStrictNullCheck({ 354 | exp: createSuperstructCall({ 355 | func: "array", 356 | args: [ 357 | ts.createArrayLiteral([ 358 | createSuperStructValidatorForm( 359 | typeModel.type, 360 | false, 361 | strictNullChecks 362 | ) 363 | ]) 364 | ] 365 | }), 366 | optional, 367 | strictNullChecks 368 | }); 369 | 370 | case "tuple": 371 | return wrapOptionalOrNonStrictNullCheck({ 372 | exp: createSuperstructCall({ 373 | func: "tuple", 374 | args: [ 375 | ts.createArrayLiteral( 376 | typeModel.types.map(t => 377 | createSuperStructValidatorForm(t, false, strictNullChecks) 378 | ) 379 | ) 380 | ] 381 | }), 382 | optional, 383 | strictNullChecks 384 | }); 385 | } 386 | 387 | const _exhaustiveCheck: never = typeModel; 388 | }; 389 | -------------------------------------------------------------------------------- /tests/transformer.test.js: -------------------------------------------------------------------------------- 1 | const tape = require("tape"); 2 | const nodeEval = require("node-eval"); 3 | const ts = require("typescript"); 4 | const fs = require("fs"); 5 | const { createValidatorTransformer } = require("../transformer"); 6 | 7 | tape("test simple object with string field success", t => 8 | shouldPassValidation(t, "test_simpleObjectWithStringField_success.ts", { 9 | name: "Me" 10 | }) 11 | ); 12 | 13 | tape("test simple object with string field fail", t => 14 | shouldThrowError(t, "test_simpleObjectWithStringField_fail.ts") 15 | ); 16 | 17 | tape("test simple object with string, number and boolean fields success", t => 18 | shouldPassValidation( 19 | t, 20 | "test_simpleObjectWithStringNumberBooleanFields_success.ts", 21 | { 22 | fieldStr: "test str", 23 | fieldNumber: 123, 24 | fieldBoolean: true 25 | } 26 | ) 27 | ); 28 | 29 | tape( 30 | "test simple object with string, number and boolean fields fail because of no string field", 31 | t => 32 | shouldThrowError( 33 | t, 34 | "test_simpleObjectWithStringNumberBooleanFields_failString.ts" 35 | ) 36 | ); 37 | 38 | tape( 39 | "test simple object with string, number and boolean fields fail because of no number field", 40 | t => 41 | shouldThrowError( 42 | t, 43 | "test_simpleObjectWithStringNumberBooleanFields_failNumber.ts" 44 | ) 45 | ); 46 | 47 | tape( 48 | "test simple object with string, number and boolean fields fail because of no boolean field", 49 | t => 50 | shouldThrowError( 51 | t, 52 | "test_simpleObjectWithStringNumberBooleanFields_failBoolean.ts" 53 | ) 54 | ); 55 | 56 | tape( 57 | "test simple object with optional string field success with field present", 58 | t => 59 | shouldPassValidation( 60 | t, 61 | "test_simpleObjectWithOptionalStringField_successWithField.ts", 62 | { name: "Me" } 63 | ) 64 | ); 65 | 66 | tape( 67 | "test simple object with optional string field success with field missing", 68 | t => 69 | shouldPassValidation( 70 | t, 71 | "test_simpleObjectWithOptionalStringField_successWithoutField.ts", 72 | {} 73 | ) 74 | ); 75 | 76 | tape("test simple object with optional string field, fail with number", t => 77 | shouldThrowError( 78 | t, 79 | "test_simpleObjectWithOptionalStringField_failWithNumber.ts" 80 | ) 81 | ); 82 | 83 | tape("test simple object with string or null field, success with string", t => 84 | shouldPassValidation( 85 | t, 86 | "test_simpleObjectWithStringOrNullField_successWithString.ts", 87 | { name: "Me" } 88 | ) 89 | ); 90 | 91 | tape("test simple object with string or null field, success with null", t => 92 | shouldPassValidation( 93 | t, 94 | "test_simpleObjectWithStringOrNullField_successWithNull.ts", 95 | { name: null } 96 | ) 97 | ); 98 | 99 | tape( 100 | "test simple object with string or null field, fail with field missig", 101 | t => 102 | shouldThrowError( 103 | t, 104 | "test_simpleObjectWithStringOrNullField_failWithFieldMissing.ts" 105 | ) 106 | ); 107 | 108 | tape("test simple object with number or null field, success with number", t => 109 | shouldPassValidation( 110 | t, 111 | "test_simpleObjectWithNumberOrNullField_successWithNumber.ts", 112 | { fieldNumber: 123 } 113 | ) 114 | ); 115 | 116 | tape("test simple object with number or null field, success with null", t => 117 | shouldPassValidation( 118 | t, 119 | "test_simpleObjectWithNumberOrNullField_successWithNull.ts", 120 | { fieldNumber: null } 121 | ) 122 | ); 123 | 124 | tape( 125 | "test simple object with number or null field, fail with field missing", 126 | t => 127 | shouldThrowError( 128 | t, 129 | "test_simpleObjectWithNumberOrNullField_failWithFieldMissing.ts" 130 | ) 131 | ); 132 | 133 | tape("test simple object with boolean or null field, success with boolean", t => 134 | shouldPassValidation( 135 | t, 136 | "test_simpleObjectWithBooleanOrNullField_successWithBoolean.ts", 137 | { fieldBoolean: true } 138 | ) 139 | ); 140 | 141 | tape("test simple object with boolean or null field, success with null", t => 142 | shouldPassValidation( 143 | t, 144 | "test_simpleObjectWithBooleanOrNullField_successWithNull.ts", 145 | { fieldBoolean: null } 146 | ) 147 | ); 148 | 149 | tape( 150 | "test simple object with boolean or null field, fail with field missing", 151 | t => 152 | shouldThrowError( 153 | t, 154 | "test_simpleObjectWithBooleanOrNullField_failWithFieldMissing.ts" 155 | ) 156 | ); 157 | 158 | tape( 159 | "test simple object with string field and non strict null checks, success with string", 160 | t => 161 | shouldPassValidation( 162 | t, 163 | "test_simpleObjectWithStringFieldAndNonStrictNullChecks_successWithString.ts", 164 | { name: "Me" }, 165 | false 166 | ) 167 | ); 168 | 169 | tape( 170 | "test simple object with string field and non strict null checks, success with null", 171 | t => 172 | shouldPassValidation( 173 | t, 174 | "test_simpleObjectWithStringFieldAndNonStrictNullChecks_successWithNull.ts", 175 | { name: null }, 176 | false 177 | ) 178 | ); 179 | 180 | tape( 181 | "test simple object with string field and non strict null checks, fail with field missing", 182 | t => 183 | shouldThrowError( 184 | t, 185 | "test_simpleObjectWithStringFieldAndNonStrictNullChecks_failWithMissingField.ts", 186 | false 187 | ) 188 | ); 189 | 190 | tape( 191 | "test simple object with optional number field, success with field present", 192 | t => 193 | shouldPassValidation( 194 | t, 195 | "test_simpleObjectWithOptionalNumberField_successWithField.ts", 196 | { fieldNumber: 123 } 197 | ) 198 | ); 199 | 200 | tape( 201 | "test simple object with optional number field, success with field missing", 202 | t => 203 | shouldPassValidation( 204 | t, 205 | "test_simpleObjectWithOptionalNumberField_successWithoutField.ts", 206 | {} 207 | ) 208 | ); 209 | 210 | tape("test simple object with optional number field, fail with string", t => 211 | shouldThrowError( 212 | t, 213 | "test_simpleObjectWithOptionalNumberField_failWithString.ts" 214 | ) 215 | ); 216 | 217 | tape( 218 | "test simple object with optional boolean field, success with field present", 219 | t => 220 | shouldPassValidation( 221 | t, 222 | "test_simpleObjectWithOptionalBooleanField_successWithField.ts", 223 | { fieldBoolean: true } 224 | ) 225 | ); 226 | 227 | tape( 228 | "test simple object with optional boolean field, success with field missing", 229 | t => 230 | shouldPassValidation( 231 | t, 232 | "test_simpleObjectWithOptionalBooleanField_successWithoutField.ts", 233 | {} 234 | ) 235 | ); 236 | 237 | tape("test simple object with optional boolean field, fail with string", t => 238 | shouldThrowError( 239 | t, 240 | "test_simpleObjectWithOptionalBooleanField_failWithString.ts" 241 | ) 242 | ); 243 | 244 | tape("test auto enum success", t => 245 | shouldPassValidation(t, "testEnum1Success.ts", { 246 | fieldEnum: 2 247 | }) 248 | ); 249 | 250 | tape("test auto enum failure", t => shouldThrowError(t, "testEnum1Fail.ts")); 251 | 252 | tape("test int-initialized enum success", t => 253 | shouldPassValidation(t, "testEnum2Success.ts", { 254 | fieldEnum: 4 255 | }) 256 | ); 257 | 258 | tape("test int-initialized enum failure", t => 259 | shouldThrowError(t, "testEnum2Fail.ts") 260 | ); 261 | 262 | tape("test string-initialized enum success", t => 263 | shouldPassValidation(t, "testEnum3Success.ts", { 264 | fieldEnum: "foo" 265 | }) 266 | ); 267 | 268 | tape("test string-initialized enum failure 1", t => 269 | shouldThrowError(t, "testEnum3Fail1.ts") 270 | ); 271 | 272 | tape("test string-initialized enum failure 2", t => 273 | shouldThrowError(t, "testEnum3Fail2.ts") 274 | ); 275 | 276 | tape("test hierarchical object, success", t => 277 | shouldPassValidation(t, "test_hierachicalObjects_success.ts", { 278 | fieldChild: { fieldNumber: 123 } 279 | }) 280 | ); 281 | 282 | tape("test hierarchical object, fail with child missing", t => 283 | shouldThrowError(t, "test_hierachicalObjects_failWithChildMissing.ts") 284 | ); 285 | 286 | tape("test hierarchical object, fail with child null", t => 287 | shouldThrowError(t, "test_hierachicalObjects_failWithChildNull.ts") 288 | ); 289 | 290 | tape("test hierarchical object, fail with child with string", t => 291 | shouldThrowError(t, "test_hierachicalObjects_failWithChildWithString.ts") 292 | ); 293 | 294 | tape("test object with array, success with number array", t => 295 | shouldPassValidation(t, "test_objectWithNumberArray_success.ts", { 296 | fieldArray: [123, 321] 297 | }) 298 | ); 299 | 300 | tape("test object with array, success with empty array", t => 301 | shouldPassValidation( 302 | t, 303 | "test_objectWithNumberArray_successWithEmptyArray.ts", 304 | { 305 | fieldArray: [] 306 | } 307 | ) 308 | ); 309 | 310 | tape("test object with array, fail with string array", t => 311 | shouldThrowError(t, "test_objectWithNumberArray_failWithStringArray.ts") 312 | ); 313 | 314 | tape("test object with array, fail with null", t => 315 | shouldThrowError(t, "test_objectWithNumberArray_failWithNull.ts") 316 | ); 317 | 318 | tape("test object with string index access, success", t => 319 | shouldPassValidation(t, "test_objectWithStringIndex_success.ts", { 320 | field1: 123, 321 | field2: 321 322 | }) 323 | ); 324 | 325 | tape("test object with string index access, success with empty object", t => 326 | shouldPassValidation( 327 | t, 328 | "test_objectWithStringIndex_successWithEmptyObj.ts", 329 | {} 330 | ) 331 | ); 332 | 333 | tape( 334 | "test object with string index access, fail with one of values boolean", 335 | t => shouldThrowError(t, "test_objectWithStringIndex_failWithBoolean.ts") 336 | ); 337 | 338 | tape("test object with string index access, fail with one of keys number", t => 339 | shouldThrowError(t, "test_objectWithStringIndex_failWithNumberIndex.ts") 340 | ); 341 | 342 | tape("test object with mandatory field and string index access, success", t => 343 | shouldPassValidation(t, "test_objectWithStringIndexAndFields_success.ts", { 344 | fieldNumber: 123, 345 | field1: 123, 346 | field2: 321 347 | }) 348 | ); 349 | 350 | tape( 351 | "test object with mandatory field and string index access, success with no index fields", 352 | t => 353 | shouldPassValidation( 354 | t, 355 | "test_objectWithStringIndexAndFields_successWithNoIndexFields.ts", 356 | { 357 | fieldNumber: 123 358 | } 359 | ) 360 | ); 361 | 362 | tape( 363 | "test object with mandatory field and string index access, fail with field missing", 364 | t => 365 | shouldThrowError( 366 | t, 367 | "test_objectWithStringIndexAndFields_failWithFieldMissing.ts" 368 | ) 369 | ); 370 | 371 | tape( 372 | "test object with mandatory number field and string index access, fail with mandatory field with string", 373 | t => 374 | shouldThrowError( 375 | t, 376 | "test_objectWithStringIndexAndFields_failWithFieldWithString.ts" 377 | ) 378 | ); 379 | 380 | tape( 381 | "test object with mandatory number field and string index access, fail with index field with string", 382 | t => 383 | shouldThrowError( 384 | t, 385 | "test_objectWithStringIndexAndFields_failWithIndexFieldWithString.ts" 386 | ) 387 | ); 388 | 389 | tape("test tuple with string and number, success", t => 390 | shouldPassValidation(t, "test_tupleWithNumberAndString_success.ts", [ 391 | 123, 392 | "testStr" 393 | ]) 394 | ); 395 | 396 | tape("test tuple with string and number, fail with third item", t => 397 | shouldThrowError(t, "test_tupleWithNumberAndString_failWithThirdItem.ts") 398 | ); 399 | 400 | tape( 401 | "test tuple with string and number, fail with messed order [string, number]", 402 | t => 403 | shouldThrowError(t, "test_tupleWithNumberAndString_failWithStringNumber.ts") 404 | ); 405 | 406 | tape( 407 | "test tuple with string and number, fail with string (second one) missing", 408 | t => 409 | shouldThrowError( 410 | t, 411 | "test_tupleWithNumberAndString_failWithStringMissing.ts" 412 | ) 413 | ); 414 | 415 | tape("test array with number, success", t => 416 | shouldPassValidation(t, "test_arrayWithNumber_success.ts", [123, 321]) 417 | ); 418 | 419 | tape("test custom validator, success", t => 420 | shouldPassValidation( 421 | t, 422 | "test_customValidator_success.ts", 423 | "a4e1b0cf-2a08-4297-83f3-4db896d7e0fb" 424 | ) 425 | ); 426 | 427 | tape("test custom validator, fail", t => 428 | shouldThrowError(t, "test_customValidator_fail.ts") 429 | ); 430 | 431 | /** 432 | * @param {string} filename 433 | * @param {boolean} strictNullChecks 434 | */ 435 | const compile = (filename, strictNullChecks = true) => { 436 | const compilerOptions = { 437 | target: ts.ScriptTarget.ES2015, 438 | module: ts.ModuleKind.CommonJS, 439 | strictNullChecks: strictNullChecks 440 | }; 441 | const program = ts.createProgram([filename], compilerOptions); 442 | const fileContent = fs.readFileSync(filename, { encoding: "utf-8" }); 443 | const transpiledSource = ts.transpileModule(fileContent, { 444 | transformers: { before: [createValidatorTransformer(program)] }, 445 | compilerOptions 446 | }); 447 | return transpiledSource.outputText; 448 | }; 449 | 450 | /** 451 | * @param {tape.Test} t 452 | * @param {string} filename 453 | * @param {*} expectedParsedObj 454 | * @param {boolean} strictNullChecks 455 | */ 456 | function shouldPassValidation( 457 | t, 458 | filename, 459 | expectedParsedObj, 460 | strictNullChecks 461 | ) { 462 | t.plan(1); 463 | 464 | const compiledTsWithTransform = compile( 465 | __dirname + "/" + filename, 466 | strictNullChecks 467 | ); 468 | let tsExports; 469 | 470 | try { 471 | tsExports = nodeEval(compiledTsWithTransform, __filename); 472 | } catch (error) { 473 | console.log( 474 | `The transpiled output: 475 | ======================= 476 | ${compiledTsWithTransform} 477 | =======================` 478 | ); 479 | t.fail(error); 480 | } 481 | 482 | t.deepEqual(tsExports.obj, expectedParsedObj); 483 | } 484 | 485 | /** 486 | * @param {tape.Test} t 487 | * @param {string} filename 488 | * @param {boolean} strictNullChecks 489 | */ 490 | function shouldThrowError(t, filename, strictNullChecks) { 491 | t.plan(1); 492 | 493 | const compiledTsWithTransform = compile( 494 | __dirname + "/" + filename, 495 | strictNullChecks 496 | ); 497 | let tsExports; 498 | 499 | try { 500 | tsExports = nodeEval(compiledTsWithTransform, __filename); 501 | } catch (error) { 502 | t.pass("validate throwed as expected"); 503 | return; 504 | } 505 | 506 | console.log( 507 | `The transpiled output: 508 | ======================= 509 | ${compiledTsWithTransform} 510 | =======================` 511 | ); 512 | 513 | t.fail("validate should throw"); 514 | } 515 | --------------------------------------------------------------------------------