From 4b319cc189851f877ad6bf5681c19bf0a633cf03 Mon Sep 17 00:00:00 2001 From: Martin Tichovsky Date: Mon, 18 Jan 2021 10:44:36 +0100 Subject: [PATCH 01/12] feat: add require feature --- package.json | 2 +- test/programs/annotation-required/examples.ts | 15 ++++ test/programs/annotation-required/main.ts | 40 +++++++++ test/programs/annotation-required/schema.json | 90 +++++++++++++++++++ test/schema.test.ts | 2 + typescript-json-schema.ts | 41 +++++++-- 6 files changed, 184 insertions(+), 6 deletions(-) create mode 100644 test/programs/annotation-required/examples.ts create mode 100644 test/programs/annotation-required/main.ts create mode 100644 test/programs/annotation-required/schema.json diff --git a/package.json b/package.json index deb7fa5c..333a1939 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "typescript-json-schema", - "version": "0.47.0", + "version": "0.48.0", "description": "typescript-json-schema generates JSON Schema files from your Typescript sources", "main": "dist/typescript-json-schema.js", "typings": "dist/typescript-json-schema.d.ts", diff --git a/test/programs/annotation-required/examples.ts b/test/programs/annotation-required/examples.ts new file mode 100644 index 00000000..0f99769d --- /dev/null +++ b/test/programs/annotation-required/examples.ts @@ -0,0 +1,15 @@ +import { MyDefaultObject, MySubObject2 } from "./main"; + +export const mySubObject2Example: MySubObject2[] = [{ + bool: true, + string: "string", + object: { prop: 1 } +}]; + +const myDefaultExample: MyDefaultObject[] = [{ + age: 30, + name: "me", + free: true +}] + +export default myDefaultExample; \ No newline at end of file diff --git a/test/programs/annotation-required/main.ts b/test/programs/annotation-required/main.ts new file mode 100644 index 00000000..ab13e6f4 --- /dev/null +++ b/test/programs/annotation-required/main.ts @@ -0,0 +1,40 @@ +interface MySubObject { + bool: boolean; + string: string; + object: object | null; + /** + * @examples require('./examples.ts').mySubObject2Example + */ + subObject?: MySubObject2; +} + +export interface MySubObject2 { + bool: boolean; + string: string; + object: object; +} + +export interface MyDefaultObject { + age: number; + name: string; + free?: boolean; +} + +export interface MyObject { + /** + * @examples require(".").innerExample + */ + filled: MySubObject; + /** + * @examples require('./examples.ts') + */ + defaultObject: MyDefaultObject; +} + +export const innerExample: MySubObject[] = [ + { + bool: true, + string: "string", + object: {} + }, +]; diff --git a/test/programs/annotation-required/schema.json b/test/programs/annotation-required/schema.json new file mode 100644 index 00000000..ca051b8e --- /dev/null +++ b/test/programs/annotation-required/schema.json @@ -0,0 +1,90 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "MyDefaultObject": { + "properties": { + "age": { + "type": "number" + }, + "free": { + "type": "boolean" + }, + "name": { + "type": "string" + } + }, + "required": ["age", "name"], + "type": "object" + }, + "MySubObject": { + "properties": { + "bool": { + "type": "boolean" + }, + "object": { + "additionalProperties": true, + "properties": {}, + "type": "object" + }, + "string": { + "type": "string" + }, + "subObject": { + "$ref": "#/definitions/MySubObject2", + "examples": [ + { + "bool": true, + "object": { + "prop": 1 + }, + "string": "string" + } + ] + } + }, + "required": ["bool", "object", "string"], + "type": "object" + }, + "MySubObject2": { + "properties": { + "bool": { + "type": "boolean" + }, + "object": { + "additionalProperties": true, + "properties": {}, + "type": "object" + }, + "string": { + "type": "string" + } + }, + "required": ["bool", "object", "string"], + "type": "object" + } + }, + "properties": { + "defaultObject": { + "$ref": "#/definitions/MyDefaultObject", + "examples": [ + { + "age": 30, + "free": true, + "name": "me" + } + ] + }, + "filled": { + "$ref": "#/definitions/MySubObject", + "examples": [ + { + "bool": true, + "object": {}, + "string": "string" + } + ] + } + }, + "required": ["defaultObject", "filled"], + "type": "object" +} diff --git a/test/schema.test.ts b/test/schema.test.ts index 7aafe9ef..02c5e103 100644 --- a/test/schema.test.ts +++ b/test/schema.test.ts @@ -274,6 +274,8 @@ describe("schema", () => { }); assertSchema("annotation-items", "MyObject"); + assertSchema("annotation-required", "MyObject"); + assertSchema("typeof-keyword", "MyObject", { typeOfKeyword: true }); assertSchema("user-validation-keywords", "MyObject", { diff --git a/typescript-json-schema.ts b/typescript-json-schema.ts index a5c84606..c6f334ec 100644 --- a/typescript-json-schema.ts +++ b/typescript-json-schema.ts @@ -7,11 +7,13 @@ import { JSONSchema7 } from "json-schema"; export { Program, CompilerOptions, Symbol } from "typescript"; const vm = require("vm"); +require("ts-node/register"); const REGEX_FILE_NAME_OR_SPACE = /(\bimport\(".*?"\)|".*?")\.| /g; const REGEX_TSCONFIG_NAME = /^.*\.json$/; const REGEX_TJS_JSDOC = /^-([\w]+)\s+(\S|\S[\s\S]*\S)\s*$/g; const REGEX_GROUP_JSDOC = /^[.]?([\w]+)\s+(\S|\S[\s\S]*\S)\s*$/g; +const REGEX_REQUIRE = /^(\s+)?require\((\'[a-zA-Z0-9.\/_-]+\'|\"[a-zA-Z0-9.\/_-]+\")\)(\.([a-zA-Z0-9_-]+))?(\s+)?/; const NUMERIC_INDEX_PATTERN = "^[0-9]+$"; export function getDefaultArgs(): Args { @@ -169,10 +171,36 @@ function unique(arr: string[]): string[] { return r; } +/** + * Resolve required file / object + */ +function resolveRequiredFile(symbol: ts.Symbol, key: string, fileName: string, objectName: string): any { + var sourceFile = getSourceFile(symbol); + var requiredFileFullPath = + fileName === "." + ? path.resolve(sourceFile.fileName) + : path.resolve(path.dirname(sourceFile.fileName), fileName); + var requiredFile = require(requiredFileFullPath); + if (!requiredFile) { + throw Error("File couldn't be loaded"); + } + var requiredObject = objectName ? requiredFile[objectName] : requiredFile.default; + if (key === "examples" && !Array.isArray(requiredObject)) { + throw Error("Required object isn't an array"); + } + return requiredObject; +} + /** * Try to parse a value and returns the string if it fails. */ -function parseValue(value: string): any { +function parseValue(symbol: ts.Symbol, key: string, value: string): any { + const match = REGEX_REQUIRE.exec(value); + if (match) { + const fileName = match[2].substr(1, match[2].length - 2); + const objectName = match[4]; + return resolveRequiredFile(symbol, key, fileName, objectName); + } try { return JSON.parse(value); } catch (error) { @@ -371,7 +399,7 @@ const annotationKeywords: { [k in keyof typeof validationKeywords]?: true } = { default: true, examples: true, // A JSDoc $ref annotation can appear as a $ref. - $ref: true + $ref: true, }; const subDefinitions = { @@ -505,7 +533,7 @@ export class JsonSchemaGenerator { if (match) { const k = match[1]; const v = match[2]; - definition[name] = { ...definition[name], [k]: v ? parseValue(v) : true }; + definition[name] = { ...definition[name], [k]: v ? parseValue(symbol, k, v) : true }; return; } } @@ -514,12 +542,15 @@ export class JsonSchemaGenerator { if (name.includes(".")) { const parts = name.split("."); if (parts.length === 2 && subDefinitions[parts[0]]) { - definition[parts[0]] = { ...definition[parts[0]], [parts[1]]: text ? parseValue(text) : true }; + definition[parts[0]] = { + ...definition[parts[0]], + [parts[1]]: text ? parseValue(symbol, name, text) : true, + }; } } if (validationKeywords[name] || this.userValidationKeywords[name]) { - definition[name] = text === undefined ? "" : parseValue(text); + definition[name] = text === undefined ? "" : parseValue(symbol, name, text); } else { // special annotations otherAnnotations[doc.name] = true; From 0b8ea69e88feb1229d31ff6b4d9529146f8870d7 Mon Sep 17 00:00:00 2001 From: Martin Tichovsky Date: Mon, 18 Jan 2021 17:58:45 +0100 Subject: [PATCH 02/12] feat: readme + exceptions --- README.md | 66 +++++++++++++++++++++++++++++++++++++++ typescript-json-schema.ts | 8 ++++- 2 files changed, 73 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 558cfbd2..706adc74 100644 --- a/README.md +++ b/README.md @@ -238,6 +238,72 @@ interface MyObject { Note: this feature doesn't work for generic types & array types, it mainly works in very simple cases. +### `require` a variable from a file + +When you want to import for example an object or an array into your property defined in annotation, you can use `require`. + +Example: + +```ts +export interface InnerData { + age: number; + name: string; + free: boolean; +} + +export interface UserData { + /** + * Specify required object + * + * @examples require("./example.ts").example + */ + data: InnerData; +} +``` + +file `example.ts` + +```ts +export const example: InnerData[] = [{ + age: 30, + name: "Ben", + free: false +}] +``` + +Translation: + +```json +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "data": { + "description": "Specify required object", + "examples": [ + { + "age": 30, + "name": "Ben", + "free": false + } + ], + "type": "object", + "properties": { + "age": { "type": "number" }, + "name": { "type": "string" }, + "free": { "type": "boolean" } + }, + "required": ["age", "free", "name"] + } + }, + "required": ["data"], + "type": "object" +} +``` + +Also you can use `require(".").example`, which will try to find exported variable with name 'example' in current file. Or you can use `require("./someFile.ts")`, which will try to use default exported variable from 'someFile.ts'. + +Note: For `examples` a required variable must be an array. + ## Background Inspired and builds upon [Typson](https://github.com/lbovet/typson/), but typescript-json-schema is compatible with more recent Typescript versions. Also, since it uses the Typescript compiler internally, more advanced scenarios are possible. If you are looking for a library that uses the AST instead of the type hierarchy and therefore better support for type aliases, have a look at [vega/ts-json-schema-generator](https://github.com/vega/ts-json-schema-generator). diff --git a/typescript-json-schema.ts b/typescript-json-schema.ts index c6f334ec..8f8423e6 100644 --- a/typescript-json-schema.ts +++ b/typescript-json-schema.ts @@ -185,8 +185,14 @@ function resolveRequiredFile(symbol: ts.Symbol, key: string, fileName: string, o throw Error("File couldn't be loaded"); } var requiredObject = objectName ? requiredFile[objectName] : requiredFile.default; + if (requiredObject === undefined) { + throw Error("Required variable is undefined"); + } + if (typeof requiredObject === "function") { + throw Error("Can't use function as a variable"); + } if (key === "examples" && !Array.isArray(requiredObject)) { - throw Error("Required object isn't an array"); + throw Error("Required variable isn't an array"); } return requiredObject; } From 2cf9e53309bc2f90f715a373043ea0a2b409c4d0 Mon Sep 17 00:00:00 2001 From: Martin Tichovsky Date: Mon, 18 Jan 2021 20:48:21 +0100 Subject: [PATCH 03/12] feat: add possibility to require a module --- typescript-json-schema.ts | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/typescript-json-schema.ts b/typescript-json-schema.ts index 8f8423e6..36ba88ac 100644 --- a/typescript-json-schema.ts +++ b/typescript-json-schema.ts @@ -172,27 +172,28 @@ function unique(arr: string[]): string[] { } /** - * Resolve required file / object + * Resolve required file */ function resolveRequiredFile(symbol: ts.Symbol, key: string, fileName: string, objectName: string): any { - var sourceFile = getSourceFile(symbol); - var requiredFileFullPath = - fileName === "." + const sourceFile = getSourceFile(symbol); + const requiredFilePath = /[.\/]+/.test(fileName) + ? fileName === "." ? path.resolve(sourceFile.fileName) - : path.resolve(path.dirname(sourceFile.fileName), fileName); - var requiredFile = require(requiredFileFullPath); + : path.resolve(path.dirname(sourceFile.fileName), fileName) + : fileName; + const requiredFile = require(requiredFilePath); if (!requiredFile) { - throw Error("File couldn't be loaded"); + throw Error("Required: File couldn't be loaded"); } - var requiredObject = objectName ? requiredFile[objectName] : requiredFile.default; + const requiredObject = objectName ? requiredFile[objectName] : requiredFile.default; if (requiredObject === undefined) { - throw Error("Required variable is undefined"); + throw Error("Required: Variable is undefined"); } if (typeof requiredObject === "function") { - throw Error("Can't use function as a variable"); + throw Error("Required: Can't use function as a variable"); } if (key === "examples" && !Array.isArray(requiredObject)) { - throw Error("Required variable isn't an array"); + throw Error("Required: Variable isn't an array"); } return requiredObject; } From 0bb9fbfd3bd426ffb92e2ce915d09faac91d9e46 Mon Sep 17 00:00:00 2001 From: Martin Tichovsky Date: Mon, 18 Jan 2021 20:50:57 +0100 Subject: [PATCH 04/12] fix: relative path test --- typescript-json-schema.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/typescript-json-schema.ts b/typescript-json-schema.ts index 36ba88ac..725f60f8 100644 --- a/typescript-json-schema.ts +++ b/typescript-json-schema.ts @@ -176,7 +176,7 @@ function unique(arr: string[]): string[] { */ function resolveRequiredFile(symbol: ts.Symbol, key: string, fileName: string, objectName: string): any { const sourceFile = getSourceFile(symbol); - const requiredFilePath = /[.\/]+/.test(fileName) + const requiredFilePath = /^[.\/]+/.test(fileName) ? fileName === "." ? path.resolve(sourceFile.fileName) : path.resolve(path.dirname(sourceFile.fileName), fileName) @@ -204,7 +204,7 @@ function resolveRequiredFile(symbol: ts.Symbol, key: string, fileName: string, o function parseValue(symbol: ts.Symbol, key: string, value: string): any { const match = REGEX_REQUIRE.exec(value); if (match) { - const fileName = match[2].substr(1, match[2].length - 2); + const fileName = match[2].substr(1, match[2].length - 2).trim(); const objectName = match[4]; return resolveRequiredFile(symbol, key, fileName, objectName); } From ee05287321e6dd7a924ca5162a86f312646476c1 Mon Sep 17 00:00:00 2001 From: Martin Tichovsky Date: Tue, 19 Jan 2021 15:19:28 +0100 Subject: [PATCH 05/12] fix: update require regex --- typescript-json-schema.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typescript-json-schema.ts b/typescript-json-schema.ts index 725f60f8..ad1f5d17 100644 --- a/typescript-json-schema.ts +++ b/typescript-json-schema.ts @@ -13,7 +13,7 @@ const REGEX_FILE_NAME_OR_SPACE = /(\bimport\(".*?"\)|".*?")\.| /g; const REGEX_TSCONFIG_NAME = /^.*\.json$/; const REGEX_TJS_JSDOC = /^-([\w]+)\s+(\S|\S[\s\S]*\S)\s*$/g; const REGEX_GROUP_JSDOC = /^[.]?([\w]+)\s+(\S|\S[\s\S]*\S)\s*$/g; -const REGEX_REQUIRE = /^(\s+)?require\((\'[a-zA-Z0-9.\/_-]+\'|\"[a-zA-Z0-9.\/_-]+\")\)(\.([a-zA-Z0-9_-]+))?(\s+)?/; +const REGEX_REQUIRE = /^(\s+)?require\((\'@?[a-zA-Z0-9.\/_-]+\'|\"@?[a-zA-Z0-9.\/_-]+\")\)(\.([a-zA-Z0-9_$]+))?(\s+|$)/; const NUMERIC_INDEX_PATTERN = "^[0-9]+$"; export function getDefaultArgs(): Args { From bbe759b7a427d4c473719639cea8875d4e4f6e6d Mon Sep 17 00:00:00 2001 From: Martin Tichovsky Date: Wed, 20 Jan 2021 15:39:47 +0100 Subject: [PATCH 06/12] feat: add tests for REGEX, update version in package.json --- package.json | 2 +- test/require.test.ts | 120 ++++++++++++++++++++++++++++++++++++++ typescript-json-schema.ts | 25 +++++++- 3 files changed, 145 insertions(+), 2 deletions(-) create mode 100644 test/require.test.ts diff --git a/package.json b/package.json index 333a1939..deb7fa5c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "typescript-json-schema", - "version": "0.48.0", + "version": "0.47.0", "description": "typescript-json-schema generates JSON Schema files from your Typescript sources", "main": "dist/typescript-json-schema.js", "typings": "dist/typescript-json-schema.d.ts", diff --git a/test/require.test.ts b/test/require.test.ts new file mode 100644 index 00000000..b7348547 --- /dev/null +++ b/test/require.test.ts @@ -0,0 +1,120 @@ +import { assert } from "chai"; +import { regexRequire } from "../typescript-json-schema"; + +const basicFilePath = "./file.ts"; +const paths = [ + basicFilePath, + ".", + "@some-module", + "@some-module/my_123", + "/some/absolute/path-to-file", + "../relative-path", + "../../../relative-path/to-file.ts", + "./relative-path/myFile123.js", +]; + +const objName = "objectName"; +const extendedObjName = "$object12_Name"; + +const getValues = (singleQuotation: boolean) => { + const quot = singleQuotation ? "'" : '"'; + return { + path: `${quot}${basicFilePath}${quot}`, + quot, + quotName: singleQuotation ? "single" : "double", + }; +}; + +const matchSimple = ( + match: RegExpExecArray | null, + singleQuotation: boolean, + filePath: string, + propertyName?: string +) => { + assert.isArray(match); + const quotation = singleQuotation ? "'" : '"'; + const expectedFileName = `${quotation}${filePath}${quotation}`; + assert(match![2] === expectedFileName, `File doesn't match, got: ${match![2]}, expected: ${expectedFileName}`); + assert(match![4] === propertyName, `Poperty has to be ${propertyName?.toString()}`); +}; + +const commonTests = (singleQuotation: boolean) => { + const { quotName, path } = getValues(singleQuotation); + it(`will not match, (${quotName} quotation mark)`, () => { + assert.isNull(regexRequire(`pre require(${path})`)); + assert.isNull(regexRequire(` e require(${path})`)); + assert.isNull(regexRequire(`require(${path})post`)); + assert.isNull(regexRequire(`requir(${path})`)); + assert.isNull(regexRequire(`require(${path}).e-r`)); + assert.isNull(regexRequire(`require(${path}`)); + assert.isNull(regexRequire(`require${path})`)); + assert.isNull(regexRequire(`require[${path}]`)); + assert.isNull(regexRequire(`REQUIRE[${path}]`)); + }); +}; + +const tests = (singleQuotation: boolean, objectName?: string) => { + const { quotName, path, quot } = getValues(singleQuotation); + const objNamePath = objectName ? `.${objectName}` : ""; + it(`basic path (${quotName} quotation mark)`, () => { + matchSimple(regexRequire(`require(${path})${objNamePath}`), singleQuotation, basicFilePath, objectName); + }); + it(`white spaces and basic path (${quotName} quotation mark)`, () => { + matchSimple(regexRequire(` require(${path})${objNamePath}`), singleQuotation, basicFilePath, objectName); + matchSimple(regexRequire(`require(${path})${objNamePath} `), singleQuotation, basicFilePath, objectName); + matchSimple( + regexRequire(` require(${path})${objNamePath} `), + singleQuotation, + basicFilePath, + objectName + ); + matchSimple( + regexRequire(` require(${path})${objNamePath} comment`), + singleQuotation, + basicFilePath, + objectName + ); + matchSimple( + regexRequire(` require(${path})${objNamePath} comment `), + singleQuotation, + basicFilePath, + objectName + ); + }); + it(`paths (${quotName} quotation mark)`, () => { + paths.forEach((pathName) => { + matchSimple( + regexRequire(`require(${quot}${pathName}${quot})${objNamePath}`), + singleQuotation, + pathName, + objectName + ); + }); + }); +}; + +describe("Double quotation", () => { + tests(false); + commonTests(false); +}); + +describe("Single quotation", () => { + tests(true); + commonTests(true); +}); + +describe("Double quotation + object", () => { + tests(false, objName); +}); + +describe("Single quotation + object", () => { + tests(true, objName); +}); + +describe("Double quotation + extended object name", () => { + tests(false, extendedObjName); +}); + +describe("Single quotation + extended object name", () => { + tests(true, extendedObjName); +}); diff --git a/typescript-json-schema.ts b/typescript-json-schema.ts index ad1f5d17..0d248fe3 100644 --- a/typescript-json-schema.ts +++ b/typescript-json-schema.ts @@ -13,6 +13,25 @@ const REGEX_FILE_NAME_OR_SPACE = /(\bimport\(".*?"\)|".*?")\.| /g; const REGEX_TSCONFIG_NAME = /^.*\.json$/; const REGEX_TJS_JSDOC = /^-([\w]+)\s+(\S|\S[\s\S]*\S)\s*$/g; const REGEX_GROUP_JSDOC = /^[.]?([\w]+)\s+(\S|\S[\s\S]*\S)\s*$/g; +/** + * Resolve required file, his path and a property name, + * pattern: require([file_path]).[property_name] + * + * the part ".[property_name]" is optional in the regex + * + * will match: + * + * require('./path.ts') + * require('./path.ts').objectName + * require("./path.ts") + * require("./path.ts").objectName + * require('@module-name') + * + * match[2] = file_path (a path to the file with quotes) + * match[3] = (optional) property_name (a property name, exported in the file) + * + * for more details, see tests/require.test.ts + */ const REGEX_REQUIRE = /^(\s+)?require\((\'@?[a-zA-Z0-9.\/_-]+\'|\"@?[a-zA-Z0-9.\/_-]+\")\)(\.([a-zA-Z0-9_$]+))?(\s+|$)/; const NUMERIC_INDEX_PATTERN = "^[0-9]+$"; @@ -198,11 +217,15 @@ function resolveRequiredFile(symbol: ts.Symbol, key: string, fileName: string, o return requiredObject; } +export function regexRequire(value: string) { + return REGEX_REQUIRE.exec(value); +} + /** * Try to parse a value and returns the string if it fails. */ function parseValue(symbol: ts.Symbol, key: string, value: string): any { - const match = REGEX_REQUIRE.exec(value); + const match = regexRequire(value); if (match) { const fileName = match[2].substr(1, match[2].length - 2).trim(); const objectName = match[4]; From 8e2cfe5b3105e64cbfa7120cce652093d6c9320d Mon Sep 17 00:00:00 2001 From: Martin Tichovsky Date: Wed, 20 Jan 2021 15:42:06 +0100 Subject: [PATCH 07/12] refactor: update test description --- test/require.test.ts | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/test/require.test.ts b/test/require.test.ts index b7348547..8c522b1b 100644 --- a/test/require.test.ts +++ b/test/require.test.ts @@ -93,28 +93,13 @@ const tests = (singleQuotation: boolean, objectName?: string) => { }); }; -describe("Double quotation", () => { +describe("Require regex pattern", () => { tests(false); commonTests(false); -}); - -describe("Single quotation", () => { tests(true); commonTests(true); -}); - -describe("Double quotation + object", () => { tests(false, objName); -}); - -describe("Single quotation + object", () => { tests(true, objName); -}); - -describe("Double quotation + extended object name", () => { tests(false, extendedObjName); -}); - -describe("Single quotation + extended object name", () => { tests(true, extendedObjName); }); From c15417e18fcd9ab02000cc7cf30e384cb4f9abde Mon Sep 17 00:00:00 2001 From: Martin Tichovsky Date: Wed, 20 Jan 2021 16:12:24 +0100 Subject: [PATCH 08/12] fix: remove ts-node/registed must be used before using calling generateSchema --- typescript-json-schema.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/typescript-json-schema.ts b/typescript-json-schema.ts index 0d248fe3..446caab5 100644 --- a/typescript-json-schema.ts +++ b/typescript-json-schema.ts @@ -7,7 +7,6 @@ import { JSONSchema7 } from "json-schema"; export { Program, CompilerOptions, Symbol } from "typescript"; const vm = require("vm"); -require("ts-node/register"); const REGEX_FILE_NAME_OR_SPACE = /(\bimport\(".*?"\)|".*?")\.| /g; const REGEX_TSCONFIG_NAME = /^.*\.json$/; From 8bc0981bcec81ab6d15e2ab47f4c63436768d03e Mon Sep 17 00:00:00 2001 From: Martin Tichovsky Date: Wed, 20 Jan 2021 16:19:45 +0100 Subject: [PATCH 09/12] fix: tests --- test/schema.test.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/test/schema.test.ts b/test/schema.test.ts index 02c5e103..5951d81e 100644 --- a/test/schema.test.ts +++ b/test/schema.test.ts @@ -274,8 +274,6 @@ describe("schema", () => { }); assertSchema("annotation-items", "MyObject"); - assertSchema("annotation-required", "MyObject"); - assertSchema("typeof-keyword", "MyObject", { typeOfKeyword: true }); assertSchema("user-validation-keywords", "MyObject", { @@ -460,3 +458,9 @@ describe("tsconfig.json", () => { } }); }); + +describe("Required", () => { + // this part is needed to resolve ts script internaly + require("ts-node/register"); + assertSchema("annotation-required", "MyObject"); +}); From 25fcd20c61e8e41f1bfe4aef29a29aa0faf2114e Mon Sep 17 00:00:00 2001 From: Martin Tichovsky Date: Wed, 20 Jan 2021 19:06:10 +0100 Subject: [PATCH 10/12] feat: add tsNodeRegister option --- README.md | 191 +++++++++++++++++----------------- test/schema.test.ts | 8 +- typescript-json-schema-cli.ts | 7 +- typescript-json-schema.ts | 6 ++ 4 files changed, 112 insertions(+), 100 deletions(-) diff --git a/README.md b/README.md index 706adc74..890e2f59 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,7 @@ Options: --rejectDateType Rejects Date fields in type definitions. [boolean] [default: false] --id Set schema id. [string] [default: ""] --defaultNumberType Default number type. [choices: "number", "integer"] [default: "number"] + --tsNodeRegister Use ts-node/register (needed for require typescript files). [boolean] [default: false] ``` ### Programmatic use @@ -59,12 +60,12 @@ import * as TJS from "typescript-json-schema"; // optionally pass argument to schema generator const settings: TJS.PartialArgs = { - required: true, + required: true, }; // optionally pass ts compiler options const compilerOptions: TJS.CompilerOptions = { - strictNullChecks: true, + strictNullChecks: true, }; // optionally pass a base path @@ -95,7 +96,7 @@ generator.getSchemaForSymbol("AnotherType"); // In larger projects type names may not be unique, // while unique names may be enabled. const settings: TJS.PartialArgs = { - uniqueNames: true, + uniqueNames: true, }; const generator = TJS.buildGenerator(program, settings); @@ -114,10 +115,10 @@ const fullSymbolList = generator.getSymbols(); ```ts type SymbolRef = { - name: string; - typeName: string; - fullyQualifiedName: string; - symbol: ts.Symbol; + name: string; + typeName: string; + fullyQualifiedName: string; + symbol: ts.Symbol; }; ``` @@ -131,13 +132,13 @@ For example ```ts export interface Shape { - /** - * The size of the shape. - * - * @minimum 0 - * @TJS-type integer - */ - size: number; + /** + * The size of the shape. + * + * @minimum 0 + * @TJS-type integer + */ + size: number; } ``` @@ -145,20 +146,20 @@ will be translated to ```json { - "$ref": "#/definitions/Shape", - "$schema": "http://json-schema.org/draft-07/schema#", - "definitions": { - "Shape": { - "properties": { - "size": { - "description": "The size of the shape.", - "minimum": 0, - "type": "integer" + "$ref": "#/definitions/Shape", + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "Shape": { + "properties": { + "size": { + "description": "The size of the shape.", + "minimum": 0, + "type": "integer" + } + }, + "type": "object" } - }, - "type": "object" } - } } ``` @@ -172,20 +173,20 @@ Example: ```ts export interface ShapesData { - /** - * Specify individual fields in items. - * - * @items.type integer - * @items.minimum 0 - */ - sizes: number[]; - - /** - * Or specify a JSON spec: - * - * @items {"type":"string","format":"email"} - */ - emails: string[]; + /** + * Specify individual fields in items. + * + * @items.type integer + * @items.minimum 0 + */ + sizes: number[]; + + /** + * Or specify a JSON spec: + * + * @items {"type":"string","format":"email"} + */ + emails: string[]; } ``` @@ -193,31 +194,31 @@ Translation: ```json { - "$ref": "#/definitions/ShapesData", - "$schema": "http://json-schema.org/draft-07/schema#", - "definitions": { - "Shape": { - "properties": { - "sizes": { - "description": "Specify individual fields in items.", - "items": { - "minimum": 0, - "type": "integer" - }, - "type": "array" - }, - "emails": { - "description": "Or specify a JSON spec:", - "items": { - "format": "email", - "type": "string" - }, - "type": "array" + "$ref": "#/definitions/ShapesData", + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "Shape": { + "properties": { + "sizes": { + "description": "Specify individual fields in items.", + "items": { + "minimum": 0, + "type": "integer" + }, + "type": "array" + }, + "emails": { + "description": "Or specify a JSON spec:", + "items": { + "format": "email", + "type": "string" + }, + "type": "array" + } + }, + "type": "object" } - }, - "type": "object" } - } } ``` @@ -232,7 +233,7 @@ Example: ```typescript type integer = number; interface MyObject { - n: integer; + n: integer; } ``` @@ -240,24 +241,26 @@ Note: this feature doesn't work for generic types & array types, it mainly works ### `require` a variable from a file +(for requiring typescript files is needed to set argument `tsNodeRegister` to true) + When you want to import for example an object or an array into your property defined in annotation, you can use `require`. Example: ```ts export interface InnerData { - age: number; - name: string; - free: boolean; + age: number; + name: string; + free: boolean; } export interface UserData { - /** - * Specify required object - * - * @examples require("./example.ts").example - */ - data: InnerData; + /** + * Specify required object + * + * @examples require("./example.ts").example + */ + data: InnerData; } ``` @@ -275,28 +278,28 @@ Translation: ```json { - "$schema": "http://json-schema.org/draft-07/schema#", - "properties": { - "data": { - "description": "Specify required object", - "examples": [ - { - "age": 30, - "name": "Ben", - "free": false + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "data": { + "description": "Specify required object", + "examples": [ + { + "age": 30, + "name": "Ben", + "free": false + } + ], + "type": "object", + "properties": { + "age": { "type": "number" }, + "name": { "type": "string" }, + "free": { "type": "boolean" } + }, + "required": ["age", "free", "name"] } - ], - "type": "object", - "properties": { - "age": { "type": "number" }, - "name": { "type": "string" }, - "free": { "type": "boolean" } - }, - "required": ["age", "free", "name"] - } - }, - "required": ["data"], - "type": "object" + }, + "required": ["data"], + "type": "object" } ``` diff --git a/test/schema.test.ts b/test/schema.test.ts index 5951d81e..ea076df3 100644 --- a/test/schema.test.ts +++ b/test/schema.test.ts @@ -459,8 +459,8 @@ describe("tsconfig.json", () => { }); }); -describe("Required", () => { - // this part is needed to resolve ts script internaly - require("ts-node/register"); - assertSchema("annotation-required", "MyObject"); +describe("Functionality 'required' in annotation", () => { + assertSchema("annotation-required", "MyObject", { + tsNodeRegister: true + }); }); diff --git a/typescript-json-schema-cli.ts b/typescript-json-schema-cli.ts index 978e7013..7207b4b3 100644 --- a/typescript-json-schema-cli.ts +++ b/typescript-json-schema-cli.ts @@ -22,8 +22,8 @@ export function run() { .describe("noExtraProps", "Disable additional properties in objects by default.") .boolean("propOrder").default("propOrder", defaultArgs.propOrder) .describe("propOrder", "Create property order definitions.") - .boolean("typeOfKeyword").default("typeOfKeyword", defaultArgs.typeOfKeyword) - .describe("typeOfKeyword", "Use typeOf keyword (https://goo.gl/DC6sni) for functions.") + .boolean("useTypeOfKeyword").default("useTypeOfKeyword", defaultArgs.typeOfKeyword) + .describe("useTypeOfKeyword", "Use typeOf keyword (https://goo.gl/DC6sni) for functions.") .boolean("required").default("required", defaultArgs.required) .describe("required", "Create required array for non-optional properties.") .boolean("strictNullChecks").default("strictNullChecks", defaultArgs.strictNullChecks) @@ -47,6 +47,8 @@ export function run() { .option("defaultNumberType").choices("defaultNumberType", ["number", "integer"]) .default("defaultNumberType", defaultArgs.defaultNumberType) .describe("defaultNumberType", "Default number type.") + .boolean("tsNodeRegister").default("tsNodeRegister", defaultArgs.tsNodeRegister) + .describe("tsNodeRegister", "Use ts-node/register (needed for requiring typescript files).") .argv; exec(args._[0], args._[1], { @@ -69,6 +71,7 @@ export function run() { rejectDateType: args.rejectDateType, id: args.id, defaultNumberType: args.defaultNumberType, + tsNodeRegister: args.tsNodeRegister, }); } diff --git a/typescript-json-schema.ts b/typescript-json-schema.ts index 446caab5..610e02b4 100644 --- a/typescript-json-schema.ts +++ b/typescript-json-schema.ts @@ -55,6 +55,7 @@ export function getDefaultArgs(): Args { rejectDateType: false, id: "", defaultNumberType: "number", + tsNodeRegister: false, }; } @@ -82,6 +83,7 @@ export type Args = { rejectDateType: boolean; id: string; defaultNumberType: "number" | "integer"; + tsNodeRegister: boolean; }; export type PartialArgs = Partial; @@ -1496,6 +1498,10 @@ export function buildGenerator( } } + if (args.tsNodeRegister) { + require("ts-node/register"); + } + let diagnostics: ReadonlyArray = []; if (!args.ignoreErrors) { From 4b170706b71b5fbf79f3a88d38d6dc2fb5bfa9e0 Mon Sep 17 00:00:00 2001 From: Martin Tichovsky Date: Wed, 20 Jan 2021 20:28:17 +0100 Subject: [PATCH 11/12] feat: make ts-node as dependency --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index deb7fa5c..5406857d 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "@types/json-schema": "^7.0.6", "glob": "^7.1.6", "json-stable-stringify": "^1.0.1", + "ts-node": "^9.1.1", "typescript": "^4.1.3", "yargs": "^16.2.0" }, @@ -62,7 +63,6 @@ "mocha": "^8.2.1", "prettier": "^2.2.1", "source-map-support": "^0.5.19", - "ts-node": "^9.1.1", "tslint": "^6.1.3" }, "scripts": { From 0090c74254e5e4f8f0b86678aec9ea68ef974bf5 Mon Sep 17 00:00:00 2001 From: Martin Tichovsky Date: Thu, 11 Feb 2021 09:28:30 +0100 Subject: [PATCH 12/12] feat: title in annotation --- test/programs/annotation-title/main.ts | 15 ++++++++++++++ test/programs/annotation-title/schema.json | 23 ++++++++++++++++++++++ test/schema.test.ts | 12 ++++------- typescript-json-schema.ts | 3 ++- 4 files changed, 44 insertions(+), 9 deletions(-) create mode 100644 test/programs/annotation-title/main.ts create mode 100644 test/programs/annotation-title/schema.json diff --git a/test/programs/annotation-title/main.ts b/test/programs/annotation-title/main.ts new file mode 100644 index 00000000..af7b9d50 --- /dev/null +++ b/test/programs/annotation-title/main.ts @@ -0,0 +1,15 @@ +/** + * @title filled# + */ +interface MySubObject { + a: boolean; +} + +interface MyObject { + /** + * @title empty# + */ + empty; + + filled: MySubObject; +} diff --git a/test/programs/annotation-title/schema.json b/test/programs/annotation-title/schema.json new file mode 100644 index 00000000..7d11b17b --- /dev/null +++ b/test/programs/annotation-title/schema.json @@ -0,0 +1,23 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "MySubObject": { + "title": "filled#", + "type": "object", + "properties": { + "a": { "type": "boolean" } + }, + "required": ["a"] + } + }, + "properties": { + "empty": { + "title": "empty#" + }, + "filled": { + "$ref": "#/definitions/MySubObject" + } + }, + "required": ["empty", "filled"], + "type": "object" +} diff --git a/test/schema.test.ts b/test/schema.test.ts index ea076df3..26352eb6 100644 --- a/test/schema.test.ts +++ b/test/schema.test.ts @@ -259,19 +259,15 @@ describe("schema", () => { describe("annotations", () => { assertSchema("annotation-default", "MyObject"); assertSchema("annotation-ref", "MyObject", {}, undefined, undefined, { - skipCompile: true + skipCompile: true, }); assertSchema("annotation-tjs", "MyObject", { validationKeywords: ["hide"], }); assertSchema("annotation-id", "MyObject", {}, undefined, undefined, { - expectedWarnings: [ - "schema id ignored", - "schema id ignored", - "schema id ignored", - "schema id ignored" - ] + expectedWarnings: ["schema id ignored", "schema id ignored", "schema id ignored", "schema id ignored"], }); + assertSchema("annotation-title", "MyObject"); assertSchema("annotation-items", "MyObject"); assertSchema("typeof-keyword", "MyObject", { typeOfKeyword: true }); @@ -461,6 +457,6 @@ describe("tsconfig.json", () => { describe("Functionality 'required' in annotation", () => { assertSchema("annotation-required", "MyObject", { - tsNodeRegister: true + tsNodeRegister: true, }); }); diff --git a/typescript-json-schema.ts b/typescript-json-schema.ts index 610e02b4..87236ad4 100644 --- a/typescript-json-schema.ts +++ b/typescript-json-schema.ts @@ -416,7 +416,8 @@ const validationKeywords = { format: true, default: true, $ref: true, - id: true + id: true, + title: true }; /**