diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..34fd8a53 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,23 @@ +{ + "version": "0.2.5", + "configurations": [ + { + // Name of configuration; appears in the launch configuration drop down menu. + "name": "Run mocha", + // Type of configuration. Possible values: "node", "mono". + "type": "node", + // Workspace relative or absolute path to the program. + "program": "${workspaceRoot}/node_modules/mocha/bin/_mocha", + // Automatically stop program after launch. + "stopOnEntry": false, + // Command line arguments passed to the program. + "args": ["out/test/test.js"], + // Workspace relative or absolute path to the working directory of the program being debugged. Default is the current workspace. + "cwd": "${workspaceRoot}", + // Workspace relative or absolute path to the runtime executable to be used. Default is the runtime executable on the PATH. + "runtimeExecutable": null, + // Environment variables passed to the program. + "env": { "NODE_ENV": "production"} +} + ] +} \ No newline at end of file diff --git a/dist/index.d.ts b/dist/index.d.ts index 026e43ca..1925578d 100644 --- a/dist/index.d.ts +++ b/dist/index.d.ts @@ -1,6 +1,6 @@ // Generated by dts-bundle v0.5.0 -export function compile(schema: JSONSchema.Schema, settings?: TsType.TsTypeSettings): string; +export function compile(schema: JSONSchema.Schema, path: string, settings?: TsType.TsTypeSettings): string; export function compileFromFile(inputFilename: string): Promise; export namespace JSONSchema { @@ -42,6 +42,7 @@ export namespace JSONSchema { [k: string]: HttpJsonSchemaOrgDraft04Schema | string[]; }; enum?: any[]; + tsEnumNames?: string[]; type?: SimpleTypes | SimpleTypes[]; allOf?: HttpJsonSchemaOrgDraft04Schema[]; anyOf?: HttpJsonSchemaOrgDraft04Schema[]; @@ -56,17 +57,18 @@ export namespace JSONSchema { export namespace TsType { interface TsTypeSettings { - declareSimpleType?: boolean; + declarationDescription?: boolean; declareReferenced?: boolean; - useFullReferencePathAsName?: boolean; - useInterfaceDeclaration?: boolean; - endTypeWithSemicolon?: boolean; + declareSimpleType?: boolean; endPropertyWithSemicolon?: boolean; - declarationDescription?: boolean; + endTypeWithSemicolon?: boolean; propertyDescription?: boolean; + useConstEnums?: boolean; + useFullReferencePathAsName?: boolean; + useInterfaceDeclaration?: boolean; } var DEFAULT_SETTINGS: TsTypeSettings; - abstract class TsType { + abstract class TsTypeBase { id?: string; description?: string; protected safeId(): string | undefined; @@ -82,37 +84,52 @@ export namespace TsType { interface TsProp { name: string; required: boolean; - type: TsType; + type: TsTypeBase; } - class Any extends TsType { + class Any extends TsTypeBase { _type(): string; } - class String extends TsType { + class String extends TsTypeBase { _type(): string; } - class Boolean extends TsType { + class Boolean extends TsTypeBase { _type(): string; } - class Number extends TsType { + class Number extends TsTypeBase { _type(): string; } - class Object extends TsType { + class Object extends TsTypeBase { _type(): string; } - class Void extends TsType { + class Void extends TsTypeBase { _type(): string; } - class Literal extends TsType { + class Literal extends TsTypeBase { constructor(value: any); _type(): any; } - class Array extends TsType { - constructor(type?: TsType); + class EnumValue { + identifier: string; + value: string; + constructor(enumValues: string[]); + toDeclaration(): string; + toString(): string; + } + class Enum extends TsTypeBase { + enumValues: EnumValue[]; + constructor(enumValues: EnumValue[]); + isSimpleType(): boolean; + _type(settings: TsTypeSettings): string; + toSafeType(settings: TsTypeSettings): string; + toDeclaration(settings: TsTypeSettings): string; + } + class Array extends TsTypeBase { + constructor(type?: TsTypeBase); _type(settings: TsTypeSettings): string; } - class Intersection extends TsType { - protected data: TsType[]; - constructor(data: TsType[]); + class Intersection extends TsTypeBase { + protected data: TsTypeBase[]; + constructor(data: TsTypeBase[]); isSimpleType(): boolean; _type(settings: TsTypeSettings): string; toSafeType(settings: TsTypeSettings): string; @@ -121,7 +138,7 @@ export namespace TsType { isSimpleType(): boolean; _type(settings: TsTypeSettings): string; } - class Interface extends TsType { + class Interface extends TsTypeBase { constructor(props: TsProp[]); static reference(id: string): Interface; protected _type(settings: TsTypeSettings, declaration?: boolean): string; diff --git a/dist/index.js b/dist/index.js index 4a4c78ca..b4e11b5a 100644 --- a/dist/index.js +++ b/dist/index.js @@ -7,46 +7,47 @@ var __extends = (this && this.__extends) || function (d, b) { }; var lodash_1 = require('lodash'); var TsType; -(function (TsType_1) { - TsType_1.DEFAULT_SETTINGS = { +(function (TsType) { + TsType.DEFAULT_SETTINGS = { declarationDescription: true, + // declareProperties: false, declareReferenced: true, declareSimpleType: false, endPropertyWithSemicolon: true, endTypeWithSemicolon: true, propertyDescription: true, + useConstEnums: true, useFullReferencePathAsName: false, - // declareProperties: false, useInterfaceDeclaration: true }; - var TsType = (function () { - function TsType() { + var TsTypeBase = (function () { + function TsTypeBase() { } - TsType.prototype.safeId = function () { + TsTypeBase.prototype.safeId = function () { return this.id && lodash_1.upperFirst(lodash_1.camelCase(this.id)); }; - TsType.prototype.toBlockComment = function (settings) { + TsTypeBase.prototype.toBlockComment = function (settings) { return this.description && settings.declarationDescription ? "/** " + this.description + " */\n" : ''; }; - TsType.prototype._toDeclaration = function (decl, settings) { + TsTypeBase.prototype._toDeclaration = function (decl, settings) { return this.toBlockComment(settings) + decl + (settings.endTypeWithSemicolon ? ';' : ''); }; - TsType.prototype.isSimpleType = function () { return true; }; - TsType.prototype.toDeclaration = function (settings) { - return this._toDeclaration("type " + this.safeId() + " = " + this._type(settings), settings); + TsTypeBase.prototype.isSimpleType = function () { return true; }; + TsTypeBase.prototype.toDeclaration = function (settings) { + return this._toDeclaration("export type " + this.safeId() + " = " + this._type(settings), settings); }; - TsType.prototype.toSafeType = function (settings) { + TsTypeBase.prototype.toSafeType = function (settings) { return this.toType(settings); }; - TsType.prototype.toType = function (settings) { + TsTypeBase.prototype.toType = function (settings) { return this.safeId() || this._type(settings); }; - TsType.prototype.toString = function () { - return this._type(TsType_1.DEFAULT_SETTINGS); + TsTypeBase.prototype.toString = function () { + return this._type(TsType.DEFAULT_SETTINGS); }; - return TsType; + return TsTypeBase; }()); - TsType_1.TsType = TsType; + TsType.TsTypeBase = TsTypeBase; var Any = (function (_super) { __extends(Any, _super); function Any() { @@ -56,8 +57,8 @@ var TsType; return 'any'; }; return Any; - }(TsType)); - TsType_1.Any = Any; + }(TsTypeBase)); + TsType.Any = Any; var String = (function (_super) { __extends(String, _super); function String() { @@ -67,8 +68,8 @@ var TsType; return 'string'; }; return String; - }(TsType)); - TsType_1.String = String; + }(TsTypeBase)); + TsType.String = String; var Boolean = (function (_super) { __extends(Boolean, _super); function Boolean() { @@ -78,8 +79,8 @@ var TsType; return 'boolean'; }; return Boolean; - }(TsType)); - TsType_1.Boolean = Boolean; + }(TsTypeBase)); + TsType.Boolean = Boolean; var Number = (function (_super) { __extends(Number, _super); function Number() { @@ -89,8 +90,8 @@ var TsType; return 'number'; }; return Number; - }(TsType)); - TsType_1.Number = Number; + }(TsTypeBase)); + TsType.Number = Number; var Object = (function (_super) { __extends(Object, _super); function Object() { @@ -100,8 +101,8 @@ var TsType; return 'Object'; }; return Object; - }(TsType)); - TsType_1.Object = Object; + }(TsTypeBase)); + TsType.Object = Object; var Void = (function (_super) { __extends(Void, _super); function Void() { @@ -111,8 +112,8 @@ var TsType; return 'void'; }; return Void; - }(TsType)); - TsType_1.Void = Void; + }(TsTypeBase)); + TsType.Void = Void; var Literal = (function (_super) { __extends(Literal, _super); function Literal(value) { @@ -123,8 +124,43 @@ var TsType; return this.value; }; return Literal; - }(TsType)); - TsType_1.Literal = Literal; + }(TsTypeBase)); + TsType.Literal = Literal; + var EnumValue = (function () { + function EnumValue(enumValues) { + this.identifier = enumValues[0]; + this.value = enumValues[1]; + } + EnumValue.prototype.toDeclaration = function () { + // if there is a value associated with the identifier, declare as identifier=value + // else declare as identifier + return "" + this.identifier + (this.value ? ('=' + this.value) : ''); + }; + EnumValue.prototype.toString = function () { + return "Enum" + this.identifier; + }; + return EnumValue; + }()); + TsType.EnumValue = EnumValue; + var Enum = (function (_super) { + __extends(Enum, _super); + function Enum(enumValues) { + _super.call(this); + this.enumValues = enumValues; + } + Enum.prototype.isSimpleType = function () { return false; }; + Enum.prototype._type = function (settings) { + return this.safeId() || 'SomeEnumType'; + }; + Enum.prototype.toSafeType = function (settings) { + return "" + this.toType(settings); + }; + Enum.prototype.toDeclaration = function (settings) { + return this.toBlockComment(settings) + "export " + (settings.useConstEnums ? 'const ' : '') + "enum " + this._type(settings) + "{\n " + this.enumValues.map(function (_) { return _.toDeclaration(); }).join(',\n') + "\n }"; + }; + return Enum; + }(TsTypeBase)); + TsType.Enum = Enum; var Array = (function (_super) { __extends(Array, _super); function Array(type) { @@ -135,8 +171,8 @@ var TsType; return (this.type || new Any()).toSafeType(settings) + "[]"; }; return Array; - }(TsType)); - TsType_1.Array = Array; + }(TsTypeBase)); + TsType.Array = Array; var Intersection = (function (_super) { __extends(Intersection, _super); function Intersection(data) { @@ -154,8 +190,8 @@ var TsType; return "" + this.toType(settings); }; return Intersection; - }(TsType)); - TsType_1.Intersection = Intersection; + }(TsTypeBase)); + TsType.Intersection = Intersection; var Union = (function (_super) { __extends(Union, _super); function Union() { @@ -169,7 +205,7 @@ var TsType; }; return Union; }(Intersection)); - TsType_1.Union = Union; + TsType.Union = Union; var Interface = (function (_super) { __extends(Interface, _super); function Interface(props) { @@ -185,29 +221,26 @@ var TsType; if (declaration === void 0) { declaration = false; } var id = this.safeId(); return declaration || !id ? "{\n " + this.props.map(function (_) { - var decl = _.name; + var decl = ' ' + _.name; if (!_.required) decl += '?'; decl += ': ' + _.type.toType(settings); if (settings.endPropertyWithSemicolon) decl += ';'; - if (settings.propertyDescription && _.type.description) + if (settings.propertyDescription && _.type.description && !_.type.id) decl += ' // ' + _.type.description; return decl; - }).join('\n') + "\n }" : id; + }).join('\n') + "\n}" : id; }; Interface.prototype.isSimpleType = function () { return false; }; Interface.prototype.toDeclaration = function (settings) { - if (settings.useInterfaceDeclaration) { - return this.toBlockComment(settings) + "interface " + this.safeId() + " " + this._type(settings, true); - } - else { - return this._toDeclaration("type " + this.safeId() + " = " + this._type(settings, true), settings); - } + if (settings.useInterfaceDeclaration) + return this.toBlockComment(settings) + "export interface " + this.safeId() + " " + this._type(settings, true); + return this._toDeclaration("export type " + this.safeId() + " = " + this._type(settings, true), settings); }; return Interface; - }(TsType)); - TsType_1.Interface = Interface; + }(TsTypeBase)); + TsType.Interface = Interface; })(TsType = exports.TsType || (exports.TsType = {})); },{"lodash":undefined}],2:[function(require,module,exports){ @@ -216,6 +249,7 @@ var pretty_printer_1 = require('./pretty-printer'); var TsTypes_1 = require('./TsTypes'); var fs_1 = require('fs'); var lodash_1 = require('lodash'); +var path_1 = require('path'); var RuleType; (function (RuleType) { RuleType[RuleType['Any'] = 0] = 'Any'; @@ -234,13 +268,20 @@ var RuleType; RuleType[RuleType['Boolean'] = 13] = 'Boolean'; RuleType[RuleType['Literal'] = 14] = 'Literal'; })(RuleType || (RuleType = {})); +var EnumType; +(function (EnumType) { + EnumType[EnumType["String"] = 0] = "String"; + EnumType[EnumType["Integer"] = 1] = "Integer"; +})(EnumType || (EnumType = {})); var Compiler = (function () { - function Compiler(schema, settings) { + function Compiler(schema, filePath, settings) { this.schema = schema; - this.id = schema.id || schema.title || 'Interface1'; + var path = path_1.resolve(filePath); + this.filePath = path_1.parse(path); this.declarations = new Map; + this.id = schema.id || schema.title || this.filePath.name || 'Interface1'; this.settings = Object.assign({}, Compiler.DEFAULT_SETTINGS, settings); - this.declareType(this.toTsType(this.schema), this.id, this.id); + this.declareType(this.toTsType(this.schema, '', true), this.id, this.id); } Compiler.prototype.toString = function () { var _this = this; @@ -258,6 +299,7 @@ var Compiler = (function () { if (rule.type === 'array' && rule.items) { return RuleType.TypedArray; } + // enum type vs enum constant? if (rule.enum) { return RuleType.Enum; } @@ -297,12 +339,39 @@ var Compiler = (function () { return /^[\d\.]+$/.test(a); }; // eg. "#/definitions/diskDevice" => ["definitions", "diskDevice"] - Compiler.prototype.resolveType = function (path) { - if (path[0] !== '#') - throw new Error('reference must start with #'); - if (path === '#' || path === '#/') + // only called in case of a $ref type + Compiler.prototype.resolveType = function (refPath, propName) { + if (refPath === '#' || refPath === '#/') { return TsTypes_1.TsType.Interface.reference(this.id); - var parts = path.slice(2).split('/'); + } + if (refPath[0] !== '#') { + var id = void 0; + var fullPath = path_1.resolve(path_1.join(this.filePath.dir, refPath)); + var file = void 0; + if (fullPath.startsWith('http')) { + throw new ReferenceError('Remote http references are not yet supported. Could not read ' + fullPath); + } + try { + file = fs_1.readFileSync(fullPath); + } + catch (err) { + throw new ReferenceError('Unable to find referenced file ' + fullPath); + } + var targetType = this.toTsType(JSON.parse(file.toString()), propName, false, true); + if (targetType.id) { + id = targetType.toSafeType(this.settings); + } + else { + var parsedNewFile = path_1.parse(fullPath); + id = parsedNewFile.name; + } + if (this.settings.declareReferenced) { + this.declareType(targetType, id, id); + } + return new TsTypes_1.TsType.Literal(id); + } + ; + var parts = refPath.slice(2).split('/'); var ret = this.settings.declareReferenced ? this.declarations.get(parts.join('/')) : undefined; if (!ret) { var cur = this.schema; @@ -315,9 +384,9 @@ var Compiler = (function () { } return ret; }; - Compiler.prototype.declareType = function (type, path, id) { + Compiler.prototype.declareType = function (type, refPath, id) { type.id = id; - this.declarations.set(path, type); + this.declarations.set(refPath, type); return type; }; Compiler.prototype.toStringLiteral = function (a) { @@ -335,14 +404,45 @@ var Compiler = (function () { })); } }; - Compiler.prototype.createTsType = function (rule) { + Compiler.prototype.createTsType = function (rule, propName, isTop, isReference) { var _this = this; + if (isTop === void 0) { isTop = false; } + if (isReference === void 0) { isReference = false; } switch (this.getRuleType(rule)) { case RuleType.AnonymousSchema: case RuleType.NamedSchema: return this.toTsDeclaration(rule); case RuleType.Enum: - return new TsTypes_1.TsType.Union(lodash_1.uniqBy(rule.enum.map(function (_) { return _this.toStringLiteral(_); }), function (_) { return _.toType(_this.settings); })); + // we honor the schema's "type" on the enum. if string, generate a union. + // if int, require the tsEnumNames + var enumType = this.validateEnumMembers(rule); + switch (enumType) { + case EnumType.Integer: + var enumValues = lodash_1.zip(rule.tsEnumNames || [], + // If we try to create a literal from an object, bad stuff can happen... so we have to toString it + rule.enum.map(function (_) { return new TsTypes_1.TsType.Literal(_).toType(_this.settings).toString(); })) + .map(function (_) { return new TsTypes_1.TsType.EnumValue(_); }); + // name our anonymous enum, if it doesn't have an ID, by the property name under + // which it was declared. Failing both of these things, it'll concat together the + // identifiers as EnumOneTwoThree for enum: ["One", "Two", "Three"]. Ugly, but + // practical. + var path = rule.id || propName || ('Enum' + enumValues.map(function (_) { return _.identifier; }).join('')); + var enm = new TsTypes_1.TsType.Enum(enumValues); + var retVal = enm; + // don't add this to the declarations map if this is the top-level type (already declared) + // or if it's a reference and we don't want to declare those. + if ((!isReference || this.settings.declareReferenced)) { + if (!isTop) { + retVal = this.declareType(retVal, path, path); + } + else { + retVal.id = path; + } + } + return retVal; + case EnumType.String: + return new TsTypes_1.TsType.Union(lodash_1.uniqBy(rule.enum.map(function (_) { return _this.toStringLiteral(_); }), function (_) { return _.toType(_this.settings); })); + } case RuleType.Any: return new TsTypes_1.TsType.Any; case RuleType.Literal: return new TsTypes_1.TsType.Literal(rule); case RuleType.TypedArray: return new TsTypes_1.TsType.Array(this.toTsType(rule.items)); @@ -357,13 +457,63 @@ var Compiler = (function () { case RuleType.AnyOf: return new TsTypes_1.TsType.Union(rule.anyOf.map(function (_) { return _this.toTsType(_); })); case RuleType.Reference: - return this.resolveType(rule.$ref); + return this.resolveType(rule.$ref, propName); } throw new Error('Unknown rule:' + rule.toString()); }; - Compiler.prototype.toTsType = function (rule) { - var type = this.createTsType(rule); - type.id = type.id || rule.id || rule.title; + Compiler.prototype.validateEnumMembers = function (rule) { + if (!rule.type) + rule.type = 'string'; + var isDeclaredStringEnum = rule.type === 'string'; + var isDeclaredIntegerEnum = rule.type === 'integer'; + if (!isDeclaredStringEnum && !isDeclaredIntegerEnum) { + throw TypeError('Enum type must be string or integer; default is string if undefined'); + } + if (rule.enum.some(function (_) { return _ instanceof Object; })) { + throw TypeError('Enum members must be a list of strings or a list of integers; instead, found an Object'); + } + var isActuallyStringEnum = rule.enum.every(function (_) { return typeof (_) === 'string'; }); + var isActuallyIntegerEnum = rule.enum.every(function (_) { return typeof (_) === 'number'; }); + var isIntegerEnumWithValidStringValues = isActuallyIntegerEnum + && rule.tsEnumNames + && rule.tsEnumNames.length === rule.enum.length + && rule.tsEnumNames.every(function (_) { return typeof (_) === 'string'; }); + if (isDeclaredStringEnum && !isActuallyStringEnum) { + throw TypeError('Enum was declared as a string type but found at least one non-string member'); + } + if (isDeclaredIntegerEnum && !isIntegerEnumWithValidStringValues) { + if (!isActuallyIntegerEnum) { + throw TypeError('Enum was declared as an integer type, but found at least one non-integer member'); + } + if (!rule.tsEnumNames) { + throw TypeError('Property tsEnumNames is required when enum is declared as an integer type'); + } + if (rule.tsEnumNames.length !== rule.enum.length) { + throw TypeError('Property enum and property tsEnumNames must be the same length'); + } + throw TypeError('Enum was declared as an integer type, but found at least one non-string tsEnumValue'); + } + // I don't think we should ever hit this case. + if (!isActuallyStringEnum && !isIntegerEnumWithValidStringValues) { + throw TypeError('Enum members must be a list of strings or a list of integers (with corresponding tsEnumNames)'); + } + if (isIntegerEnumWithValidStringValues) { + return EnumType.Integer; + } + else { + return EnumType.String; + } + }; + Compiler.prototype.toTsType = function (rule, propName, isTop, isReference) { + if (isTop === void 0) { isTop = false; } + if (isReference === void 0) { isReference = false; } + var type = this.createTsType(rule, propName, isTop, isReference); + if (!type.id) { + // the type is not declared, let's check if we should declare it or keep it inline + type.id = rule.id || rule.title; + if (type.id && !isReference) + this.declareType(type, type.id, type.id); + } type.description = type.description || rule.description; return type; }; @@ -374,7 +524,7 @@ var Compiler = (function () { return { name: k, required: _this.isRequired(k, copy), - type: _this.toTsType(v) + type: _this.toTsType(v, k) }; }); if (props.length === 0 && !('additionalProperties' in schema)) { @@ -403,8 +553,8 @@ var Compiler = (function () { }; return Compiler; }()); -function compile(schema, settings) { - return new Compiler(schema, settings).toString(); +function compile(schema, path, settings) { + return new Compiler(schema, path, settings).toString(); } exports.compile = compile; function compileFromFile(inputFilename) { @@ -414,14 +564,14 @@ function compileFromFile(inputFilename) { reject(err); } else { - resolve(compile(JSON.parse(data.toString()))); + resolve(compile(JSON.parse(data.toString()), inputFilename)); } }); }); } exports.compileFromFile = compileFromFile; -},{"./TsTypes":1,"./pretty-printer":3,"fs":undefined,"lodash":undefined}],3:[function(require,module,exports){ +},{"./TsTypes":1,"./pretty-printer":3,"fs":undefined,"lodash":undefined,"path":undefined}],3:[function(require,module,exports){ // from https://github.com/Microsoft/TypeScript/wiki/Using-the-Compiler-API#pretty-printer-using-the-ls-formatter "use strict"; var ts = require('typescript'); diff --git a/package.json b/package.json index 84b62eec..7607f9ce 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "json-schema-to-typescript", - "version": "1.2.2", + "version": "1.2.3", "description": "compile json schema to typescript typings", "main": "dist/index.js", "typings": "dist/index.d.ts", diff --git a/src/JSONSchema.ts b/src/JSONSchema.ts index 18751fc0..45d30649 100644 --- a/src/JSONSchema.ts +++ b/src/JSONSchema.ts @@ -38,6 +38,10 @@ export namespace JSONSchema { [k: string]: HttpJsonSchemaOrgDraft04Schema | string[]; }; enum?: any[]; + + // schema extension to support numeric enums + tsEnumNames?: string[]; + type?: SimpleTypes | SimpleTypes[]; allOf?: HttpJsonSchemaOrgDraft04Schema[]; anyOf?: HttpJsonSchemaOrgDraft04Schema[]; diff --git a/src/TsTypes.ts b/src/TsTypes.ts index d72d4391..fa4331c0 100644 --- a/src/TsTypes.ts +++ b/src/TsTypes.ts @@ -3,30 +3,32 @@ import { camelCase, upperFirst } from 'lodash' export namespace TsType { export interface TsTypeSettings { - declareSimpleType?: boolean - declareReferenced?: boolean - useFullReferencePathAsName?: boolean + declarationDescription?: boolean // TODO declareProperties?: boolean - useInterfaceDeclaration?: boolean - endTypeWithSemicolon?: boolean + declareReferenced?: boolean + declareSimpleType?: boolean endPropertyWithSemicolon?: boolean - declarationDescription?: boolean + endTypeWithSemicolon?: boolean propertyDescription?: boolean + useConstEnums?: boolean + useFullReferencePathAsName?: boolean + useInterfaceDeclaration?: boolean } export var DEFAULT_SETTINGS: TsTypeSettings = { declarationDescription: true, + // declareProperties: false, declareReferenced: true, declareSimpleType: false, endPropertyWithSemicolon: true, endTypeWithSemicolon: true, propertyDescription: true, + useConstEnums: true, useFullReferencePathAsName: false, - // declareProperties: false, useInterfaceDeclaration: true } - export abstract class TsType { + export abstract class TsTypeBase { id?: string description?: string @@ -42,7 +44,7 @@ export namespace TsType { protected abstract _type(settings: TsTypeSettings): string isSimpleType() { return true } toDeclaration(settings: TsTypeSettings): string { - return this._toDeclaration(`type ${this.safeId()} = ${this._type(settings)}`, settings) + return this._toDeclaration(`export type ${this.safeId()} = ${this._type(settings)}`, settings) } toSafeType(settings: TsTypeSettings): string { return this.toType(settings) @@ -58,54 +60,92 @@ export namespace TsType { export interface TsProp { name: string required: boolean - type: TsType + type: TsTypeBase } - export class Any extends TsType { + export class Any extends TsTypeBase { _type() { return 'any' } } - export class String extends TsType { + export class String extends TsTypeBase { _type() { return 'string' } } - export class Boolean extends TsType { + export class Boolean extends TsTypeBase { _type() { return 'boolean' } } - export class Number extends TsType { + export class Number extends TsTypeBase { _type() { return 'number' } } - export class Object extends TsType { + export class Object extends TsTypeBase { _type() { return 'Object' } } - export class Void extends TsType { + export class Void extends TsTypeBase { _type() { return 'void' } } - export class Literal extends TsType { + export class Literal extends TsTypeBase { constructor(private value: any) { super() } _type() { return this.value } } - export class Array extends TsType { - constructor(private type?: TsType) { super() } + export class EnumValue { + identifier: string + value: string + + constructor(enumValues: string[]) { + this.identifier = enumValues[0] + this.value = enumValues[1] + } + + toDeclaration(){ + // if there is a value associated with the identifier, declare as identifier=value + // else declare as identifier + return `${this.identifier}${this.value ? ('=' + this.value) : ''}` + } + + toString(){ + return `Enum${this.identifier}` + } + } + + export class Enum extends TsTypeBase { + constructor(public enumValues: EnumValue[]) { + super() + } + isSimpleType() { return false } + _type(settings: TsTypeSettings) { + return this.safeId() || 'SomeEnumType' + } + toSafeType(settings: TsTypeSettings) { + return `${this.toType(settings)}` + } + toDeclaration(settings: TsTypeSettings): string { + return `${this.toBlockComment(settings)}export ${settings.useConstEnums ? 'const ' : ''}enum ${this._type(settings)}{ + ${this.enumValues.map(_ => _.toDeclaration()).join(',\n')} + }` + } + } + + export class Array extends TsTypeBase { + constructor(private type?: TsTypeBase) { super() } _type(settings: TsTypeSettings) { return `${(this.type || new Any()).toSafeType(settings)}[]` } } - export class Intersection extends TsType { - constructor(protected data: TsType[]) { + export class Intersection extends TsTypeBase { + constructor(protected data: TsTypeBase[]) { super() } isSimpleType() { return this.data.filter(_ => !(_ instanceof Void)).length <= 1 } @@ -128,7 +168,7 @@ export namespace TsType { } } - export class Interface extends TsType { + export class Interface extends TsTypeBase { constructor(private props: TsProp[]) { super() } @@ -141,7 +181,7 @@ export namespace TsType { let id = this.safeId() return declaration || !id ? `{ ${this.props.map(_ => { - let decl = _.name + let decl = ' ' + _.name if (!_.required) decl += '?' decl += ': ' + _.type.toType(settings) @@ -151,15 +191,13 @@ export namespace TsType { decl += ' // ' + _.type.description return decl }).join('\n')} - }` : id +}` : id } isSimpleType() { return false } toDeclaration(settings: TsTypeSettings): string { - if (settings.useInterfaceDeclaration) { - return `${this.toBlockComment(settings)}interface ${this.safeId()} ${this._type(settings, true)}` - } else { - return this._toDeclaration(`type ${this.safeId()} = ${this._type(settings, true)}`, settings) - } + if (settings.useInterfaceDeclaration) + return `${this.toBlockComment(settings)}export interface ${this.safeId()} ${this._type(settings, true)}` + return this._toDeclaration(`export type ${this.safeId()} = ${this._type(settings, true)}`, settings) } } diff --git a/src/index.ts b/src/index.ts index 4f5aacf1..f08ebb43 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,14 +1,20 @@ import { JSONSchema } from './JSONSchema' import { format } from './pretty-printer' import { TsType } from './TsTypes' -import { readFile } from 'fs' -import { isPlainObject, last, map, merge, uniqBy } from 'lodash' +import { readFile, readFileSync } from 'fs' +import { isPlainObject, last, map, merge, uniqBy, zip } from 'lodash' +import { join, parse, ParsedPath, resolve } from 'path' enum RuleType { 'Any', 'TypedArray', 'Enum', 'AllOf', 'AnyOf', 'Reference', 'NamedSchema', 'AnonymousSchema', 'String', 'Number', 'Void', 'Object', 'Array', 'Boolean', 'Literal' } +enum EnumType { + String, + Integer +} + class Compiler { static DEFAULT_SETTINGS = TsType.DEFAULT_SETTINGS @@ -19,11 +25,13 @@ class Compiler { type: 'object' } - constructor(private schema: JSONSchema.Schema, settings?: TsType.TsTypeSettings) { - this.id = schema.id || schema.title || 'Interface1' + constructor(private schema: JSONSchema.Schema, filePath: string, settings?: TsType.TsTypeSettings) { + let path = resolve(filePath) + this.filePath = parse(path) this.declarations = new Map + this.id = schema.id || schema.title || this.filePath.name || 'Interface1' this.settings = Object.assign({}, Compiler.DEFAULT_SETTINGS, settings) - this.declareType(this.toTsType(this.schema), this.id, this.id) + this.declareType(this.toTsType(this.schema, '', true), this.id, this.id) } toString(): string { @@ -36,7 +44,8 @@ class Compiler { private settings: TsType.TsTypeSettings private id: string - private declarations: Map + private declarations: Map + private filePath: ParsedPath private isRequired(propertyName: string, schema: JSONSchema.Schema): boolean { return schema.required ? schema.required.indexOf(propertyName) > -1 : false @@ -50,6 +59,7 @@ class Compiler { if (rule.type === 'array' && rule.items) { return RuleType.TypedArray } + // enum type vs enum constant? if (rule.enum) { return RuleType.Enum } @@ -90,13 +100,45 @@ class Compiler { } // eg. "#/definitions/diskDevice" => ["definitions", "diskDevice"] - private resolveType(path: string): TsType.TsType { - if (path[0] !== '#') - throw new Error('reference must start with #') - if (path === '#' || path === '#/') + // only called in case of a $ref type + private resolveType(refPath: string, propName: string): TsType.TsTypeBase { + if (refPath === '#' || refPath === '#/'){ return TsType.Interface.reference(this.id) - const parts = path.slice(2).split('/') + } + + if (refPath[0] !== '#'){ + let id: string + let fullPath = resolve(join(this.filePath.dir, refPath)) + let file: Buffer + + if (fullPath.startsWith('http')) { + throw new ReferenceError('Remote http references are not yet supported. Could not read ' + fullPath) + } + + try { + file = readFileSync(fullPath) + } catch (err){ + throw new ReferenceError('Unable to find referenced file ' + fullPath) + } + + let targetType = this.toTsType(JSON.parse(file.toString()), propName, false, true) + if (targetType.id){ + id = targetType.toSafeType(this.settings) + } else { + let parsedNewFile = parse(fullPath) + id = parsedNewFile.name + } + + if (this.settings.declareReferenced){ + this.declareType(targetType, id, id) + } + + return new TsType.Literal(id) + }; + + const parts = refPath.slice(2).split('/') let ret = this.settings.declareReferenced ? this.declarations.get(parts.join('/')) : undefined + if (!ret) { let cur: any = this.schema for (let i = 0; cur && i < parts.length; ++i) { @@ -109,13 +151,13 @@ class Compiler { return ret } - private declareType(type: TsType.TsType, path: string, id: string) { + private declareType(type: TsType.TsTypeBase, refPath: string, id: string) { type.id = id - this.declarations.set(path, type) + this.declarations.set(refPath, type) return type } - private toStringLiteral(a: boolean | number | string | Object): TsType.TsType { + private toStringLiteral(a: boolean | number | string | Object): TsType.TsTypeBase { switch (typeof a) { case 'boolean': return new TsType.Boolean // ts doesn't support literal boolean types case 'number': return new TsType.Number // ts doesn't support literal numeric types @@ -135,16 +177,48 @@ class Compiler { } } - private createTsType(rule: JSONSchema.Schema): TsType.TsType { + private createTsType (rule: JSONSchema.Schema, propName?: string, isTop: boolean = false, isReference: boolean = false): TsType.TsTypeBase { switch (this.getRuleType(rule)) { case RuleType.AnonymousSchema: case RuleType.NamedSchema: return this.toTsDeclaration(rule) case RuleType.Enum: - return new TsType.Union(uniqBy( - rule.enum!.map(_ => this.toStringLiteral(_)) - , _ => _.toType(this.settings)) - ) + // we honor the schema's "type" on the enum. if string, generate a union. + // if int, require the tsEnumNames + let enumType = this.validateEnumMembers(rule) + + switch (enumType){ + case EnumType.Integer: + var enumValues = zip(rule.tsEnumNames || [], + // If we try to create a literal from an object, bad stuff can happen... so we have to toString it + rule.enum!.map(_ => new TsType.Literal(_).toType(this.settings).toString())) + .map(_ => new TsType.EnumValue(_)) + + // name our anonymous enum, if it doesn't have an ID, by the property name under + // which it was declared. Failing both of these things, it'll concat together the + // identifiers as EnumOneTwoThree for enum: ["One", "Two", "Three"]. Ugly, but + // practical. + let path = rule.id || propName || ('Enum' + enumValues.map(_ => _.identifier).join('')) + + let enm = new TsType.Enum(enumValues) + let retVal: TsType.TsTypeBase = enm + + // don't add this to the declarations map if this is the top-level type (already declared) + // or if it's a reference and we don't want to declare those. + if ((!isReference || this.settings.declareReferenced)){ + if (!isTop){ + retVal = this.declareType(retVal, path, path) + } else { + retVal.id = path + } + } + + return retVal + case EnumType.String: + return new TsType.Union(uniqBy( + rule.enum!.map(_ => this.toStringLiteral(_)) + , _ => _.toType(this.settings))) + } case RuleType.Any: return new TsType.Any case RuleType.Literal: return new TsType.Literal(rule) case RuleType.TypedArray: return new TsType.Array(this.toTsType(rule.items!)) @@ -159,22 +233,74 @@ class Compiler { case RuleType.AnyOf: return new TsType.Union(rule.anyOf!.map(_ => this.toTsType(_))) case RuleType.Reference: - return this.resolveType(rule.$ref!) + return this.resolveType(rule.$ref!, propName!) } throw new Error('Unknown rule:' + rule.toString()) } - private toTsType(rule: JSONSchema.Schema): TsType.TsType { - let type = this.createTsType(rule) + + private validateEnumMembers(rule: JSONSchema.Schema): EnumType { + if (!rule.type) rule.type = 'string' + + let isDeclaredStringEnum = rule.type === 'string' + let isDeclaredIntegerEnum = rule.type === 'integer' + + if (!isDeclaredStringEnum && !isDeclaredIntegerEnum){ + throw TypeError('Enum type must be string or integer; default is string if undefined') + } + + if (rule.enum!.some(_ => _ instanceof Object)){ + throw TypeError('Enum members must be a list of strings or a list of integers; instead, found an Object') + } + + let isActuallyStringEnum = rule.enum!.every(_ => typeof(_) === 'string') + let isActuallyIntegerEnum = rule.enum!.every(_ => typeof(_) === 'number') + let isIntegerEnumWithValidStringValues = isActuallyIntegerEnum + && rule.tsEnumNames + && rule.tsEnumNames.length === rule.enum!.length + && rule.tsEnumNames!.every(_ => typeof(_) === 'string') + + if (isDeclaredStringEnum && !isActuallyStringEnum){ + throw TypeError('Enum was declared as a string type but found at least one non-string member') + } + + if (isDeclaredIntegerEnum && !isIntegerEnumWithValidStringValues){ + if (!isActuallyIntegerEnum){ + throw TypeError('Enum was declared as an integer type, but found at least one non-integer member') + } + if (!rule.tsEnumNames){ + throw TypeError('Property tsEnumNames is required when enum is declared as an integer type') + } + if (rule.tsEnumNames.length !== rule.enum!.length){ + throw TypeError('Property enum and property tsEnumNames must be the same length') + } + + throw TypeError('Enum was declared as an integer type, but found at least one non-string tsEnumValue') + } + + // I don't think we should ever hit this case. + if (!isActuallyStringEnum && !isIntegerEnumWithValidStringValues){ + throw TypeError('Enum members must be a list of strings or a list of integers (with corresponding tsEnumNames)') + } + + if (isIntegerEnumWithValidStringValues){ + return EnumType.Integer + } else { + return EnumType.String + } + } + + private toTsType (rule: JSONSchema.Schema, propName?: string, isTop: boolean = false, isReference: boolean = false): TsType.TsTypeBase { + let type = this.createTsType(rule, propName, isTop, isReference) if (!type.id) { // the type is not declared, let's check if we should declare it or keep it inline type.id = rule.id || rule.title - if (type.id) + if (type.id && !isReference) this.declareType(type, type.id, type.id) } type.description = type.description || rule.description return type } - private toTsDeclaration(schema: JSONSchema.Schema): TsType.TsType { + private toTsDeclaration(schema: JSONSchema.Schema): TsType.TsTypeBase { let copy = merge({}, Compiler.DEFAULT_SCHEMA, schema) let props = map( copy.properties!, @@ -182,7 +308,7 @@ class Compiler { return { name: k, required: this.isRequired(k, copy), - type: this.toTsType(v) + type: this.toTsType(v, k) } }) if (props.length === 0 && !('additionalProperties' in schema)) { @@ -204,8 +330,8 @@ class Compiler { } } -export function compile(schema: JSONSchema.Schema, settings?: TsType.TsTypeSettings): string { - return new Compiler(schema, settings).toString() +export function compile(schema: JSONSchema.Schema, path: string, settings?: TsType.TsTypeSettings): string { + return new Compiler(schema, path, settings).toString() } export function compileFromFile(inputFilename: string): Promise { @@ -214,7 +340,7 @@ export function compileFromFile(inputFilename: string): Promise if (err) { reject(err) } else { - resolve(compile(JSON.parse(data.toString()))) + resolve(compile(JSON.parse(data.toString()), inputFilename)) } }) ) diff --git a/test/cases/additionalProperties.ts b/test/cases/additionalProperties.ts index 69d921d2..f5c7a51c 100644 --- a/test/cases/additionalProperties.ts +++ b/test/cases/additionalProperties.ts @@ -11,7 +11,7 @@ export var schema = { } } -export var types = `interface AdditionalProperties { +export var types = `export interface AdditionalProperties { foo?: string; [k: string]: number; }` diff --git a/test/cases/allOf.ts b/test/cases/allOf.ts index 0ed725d9..6e8c2ff3 100644 --- a/test/cases/allOf.ts +++ b/test/cases/allOf.ts @@ -31,13 +31,13 @@ export var schema = { "additionalProperties": false } -export var types = `interface Foo { +export var types = `export interface Foo { a: string; b: number; } -interface Bar { +export interface Bar { a: string; } -interface AllOf { +export interface AllOf { foo: Foo & Bar; }` diff --git a/test/cases/anyOf.ts b/test/cases/anyOf.ts index 568b3269..0037817d 100644 --- a/test/cases/anyOf.ts +++ b/test/cases/anyOf.ts @@ -36,18 +36,18 @@ export var schema = { } -export var types = `interface Foo { +export var types = `export interface Foo { a: string; b?: number; } -interface Bar { +export interface Bar { a?: "a" | "b" | "c"; [k: string]: any; } -interface Baz { +export interface Baz { baz?: Bar; [k: string]: any; } -interface AnyOf { +export interface AnyOf { foo: Foo | Bar | Baz; }` diff --git a/test/cases/array-of-type.ts b/test/cases/array-of-type.ts index f19e4499..94ad7d91 100644 --- a/test/cases/array-of-type.ts +++ b/test/cases/array-of-type.ts @@ -11,7 +11,7 @@ export var schema = { } } -export var types = `interface ArrayOfType { +export var types = `export interface ArrayOfType { foo?: string[]; [k: string]: any; }` diff --git a/test/cases/basics.ts b/test/cases/basics.ts index dd186740..cd034312 100644 --- a/test/cases/basics.ts +++ b/test/cases/basics.ts @@ -31,7 +31,7 @@ export var configurations = [ settings: { useInterfaceDeclaration: true, }, - types: `interface ExampleSchema { + types: `export interface ExampleSchema { firstName: string; lastName: string; age?: number; // Age in years @@ -45,7 +45,7 @@ export var configurations = [ settings: { propertyDescription: false }, - types: `interface ExampleSchema { + types: `export interface ExampleSchema { firstName: string; lastName: string; age?: number; diff --git a/test/cases/enum.ts b/test/cases/enum.ts index 66ca8be1..92fd6a65 100644 --- a/test/cases/enum.ts +++ b/test/cases/enum.ts @@ -6,24 +6,42 @@ export var schema = { "enum": ["a", "b", "c"] }, "bar": { - "enum": [1, 2, 3] - }, - "baz": { - "enum": [ - { "a": 1 }, - { "a": 2 }, - { "a": 3 } - ] + "type": "integer", + "enum": [1, 2, 3], + "tsEnumNames": ["One","Two","Three"] } }, - "required": ["foo", "bar", "baz"], + "required": ["foo", "bar"], "additionalProperties": false } -export var types = `interface Enum { +export var configurations = [ + { + settings: { + useConstEnums: false + }, + types: `export enum Bar { + One = 1, + Two = 2, + Three = 3 +} +export interface Enum { + foo: "a" | "b" | "c"; + bar: Bar; +}` + }, + { + settings: { + useConstEnums: true + }, + types: `export const enum Bar { + One = 1, + Two = 2, + Three = 3 +} +export interface Enum { foo: "a" | "b" | "c"; - bar: number; - baz: { - a: number; - }; + bar: Bar; }` + } +] diff --git a/test/cases/enumValidation.1.ts b/test/cases/enumValidation.1.ts new file mode 100644 index 00000000..e7894c97 --- /dev/null +++ b/test/cases/enumValidation.1.ts @@ -0,0 +1,22 @@ +export var schema = { + "title": "Enum", + "type": "object", + "properties": { + "bar": { + "type": "string", + "enum": ["foo", 2, 3], + "tsEnumNames": ["One","Two","Three"] + } + }, + "required": ["bar"], + "additionalProperties": false +} + +export var settings = { + useTypescriptEnums: true +} + +export var error = { + type: TypeError, + message: 'Enum was declared as a string type but found at least one non-string member' +} diff --git a/test/cases/enumValidation.2.ts b/test/cases/enumValidation.2.ts new file mode 100644 index 00000000..22584ab2 --- /dev/null +++ b/test/cases/enumValidation.2.ts @@ -0,0 +1,22 @@ +export var schema = { + "title": "Enum", + "type": "object", + "properties": { + "bar": { + "type": "integer", + "enum": [1, 2, 3], + "tsEnumNames": ["One",2,"Three"] + } + }, + "required": ["bar"], + "additionalProperties": false +} + +export var settings = { + useTypescriptEnums: true +} + +export var error = { + type: TypeError, + message: 'Enum was declared as an integer type, but found at least one non-string tsEnumValue' +} diff --git a/test/cases/enumValidation.3.ts b/test/cases/enumValidation.3.ts new file mode 100644 index 00000000..b49a8855 --- /dev/null +++ b/test/cases/enumValidation.3.ts @@ -0,0 +1,21 @@ +export var schema = { + "title": "Enum", + "type": "object", + "properties": { + "bar": { + "type": "integer", + "enum": [1, 2, 3] + } + }, + "required": ["bar"], + "additionalProperties": false +} + +export var settings = { + useTypescriptEnums: true +} + +export var error = { + type: TypeError, + message: 'Property tsEnumNames is required when enum is declared as an integer type' +} diff --git a/test/cases/enumValidation.4.ts b/test/cases/enumValidation.4.ts new file mode 100644 index 00000000..1023f46f --- /dev/null +++ b/test/cases/enumValidation.4.ts @@ -0,0 +1,22 @@ +export var schema = { + "title": "Enum", + "type": "object", + "properties": { + "bar": { + "type": "integer", + "enum": [1, 2, 3], + "tsEnumNames": ["One","Three"] + } + }, + "required": ["bar"], + "additionalProperties": false +} + +export var settings = { + useTypescriptEnums: true +} + +export var error = { + type: TypeError, + message: 'Property enum and property tsEnumNames must be the same length' +} diff --git a/test/cases/enumValidation.ts b/test/cases/enumValidation.ts new file mode 100644 index 00000000..a3223d00 --- /dev/null +++ b/test/cases/enumValidation.ts @@ -0,0 +1,21 @@ +export var schema = { + "title": "Enum", + "type": "object", + "properties": { + "foo": { + "type": "integer", + "enum": ["a", "b", "c"] + } + }, + "required": ["foo"], + "additionalProperties": false +} + +export var settings = { + useTypescriptEnums: true +} + +export var error = { + type: TypeError, + message: 'Enum was declared as an integer type, but found at least one non-integer member' +} diff --git a/test/cases/json-schema.ts b/test/cases/json-schema.ts index 5307e1d7..e11308c2 100644 --- a/test/cases/json-schema.ts +++ b/test/cases/json-schema.ts @@ -154,13 +154,13 @@ export var configurations = [ settings: { declareSimpleType: true }, - types:`type PositiveInteger = number; -type PositiveIntegerDefault0 = PositiveInteger; -type SchemaArray = HttpJsonSchemaOrgDraft04Schema[]; -type StringArray = string[]; -type SimpleTypes = "array" | "boolean" | "integer" | "null" | "number" | "object" | "string"; + types:`export type PositiveInteger = number; +export type PositiveIntegerDefault0 = PositiveInteger; +export type SchemaArray = HttpJsonSchemaOrgDraft04Schema[]; +export type StringArray = string[]; +export type SimpleTypes = "array" | "boolean" | "integer" | "null" | "number" | "object" | "string"; /** Core schema meta-schema */ -interface HttpJsonSchemaOrgDraft04Schema { +export interface HttpJsonSchemaOrgDraft04Schema { id?: string; $schema?: string; title?: string; @@ -208,9 +208,9 @@ interface HttpJsonSchemaOrgDraft04Schema { settings: { declareSimpleType: false }, - types:`type SimpleTypes = "array" | "boolean" | "integer" | "null" | "number" | "object" | "string"; + types:`export type SimpleTypes = "array" | "boolean" | "integer" | "null" | "number" | "object" | "string"; /** Core schema meta-schema */ -interface HttpJsonSchemaOrgDraft04Schema { +export interface HttpJsonSchemaOrgDraft04Schema { id?: string; $schema?: string; title?: string; diff --git a/test/cases/named-property.ts b/test/cases/named-property.ts index 79d28fcb..cb2ecb5d 100644 --- a/test/cases/named-property.ts +++ b/test/cases/named-property.ts @@ -15,9 +15,9 @@ export var schema = { } export var types = `/** Array of authorized user ids. */ -type UserIdArray = string[]; +export type UserIdArray = string[]; /** My cool schema */ -interface ExampleSchema { +export interface ExampleSchema { users?: UserIdArray; [k: string]: any; }` diff --git a/test/cases/not-extendable.ts b/test/cases/not-extendable.ts index c4d03039..8faa6f8d 100644 --- a/test/cases/not-extendable.ts +++ b/test/cases/not-extendable.ts @@ -18,7 +18,7 @@ export var schema = { "additionalProperties": false } -export var types = `interface ExampleSchema { +export var types = `export interface ExampleSchema { firstName: string; lastName: string; age?: number; // Age in years diff --git a/test/cases/ref.ts b/test/cases/ref.ts new file mode 100644 index 00000000..68d3b0e7 --- /dev/null +++ b/test/cases/ref.ts @@ -0,0 +1,40 @@ +export var schema = +{ + "title": "Referencing", + "type": "object", + "properties": { + "foo": { + "$ref": "test/resources/ReferencedType.json" + } + }, + "required": ["foo"], + "additionalProperties": false +} + +export var configurations = [ + { + settings: { + declareReferenced: true + }, + types: `export interface ExampleSchema { + firstName: string; + lastName: string; + age?: number; // Age in years + height?: number; + favoriteFoods?: any[]; + likesDogs?: boolean; + [k: string]: any; +} +export interface Referencing { + foo: ExampleSchema; +}` + }, + { + settings: { + declareReferenced: false + }, + types: `export interface Referencing { + foo: ExampleSchema; +}` + } +] \ No newline at end of file diff --git a/test/cases/root-anyOf.ts b/test/cases/root-anyOf.ts index 28e0de99..50e91f61 100644 --- a/test/cases/root-anyOf.ts +++ b/test/cases/root-anyOf.ts @@ -27,16 +27,16 @@ export var schema = { } } -export var types = `interface Foo { +export var types = `export interface Foo { a: string; b?: number; } -interface Bar { +export interface Bar { a?: "a" | "b" | "c"; [k: string]: any; } -interface Baz { +export interface Baz { baz?: Bar; [k: string]: any; } -type RootAnyOf = Foo | Bar | Baz;` +export type RootAnyOf = Foo | Bar | Baz;` diff --git a/test/cases/unnamedSchema.ts b/test/cases/unnamedSchema.ts index aae75f02..654b7fb8 100644 --- a/test/cases/unnamedSchema.ts +++ b/test/cases/unnamedSchema.ts @@ -9,6 +9,6 @@ export var schema = { "additionalProperties": false } -export var types = `interface Interface1 { +export var types = `export interface UnnamedSchema { foo: string; }` diff --git a/test/cases/with-description.ts b/test/cases/with-description.ts index 832aa59b..d7afc489 100644 --- a/test/cases/with-description.ts +++ b/test/cases/with-description.ts @@ -19,7 +19,7 @@ export var schema = { } export var types = `/** My cool schema */ -interface ExampleSchema { +export interface ExampleSchema { firstName: string; lastName: string; age?: number; // Age in years diff --git a/test/resources/ReferencedType.json b/test/resources/ReferencedType.json new file mode 100644 index 00000000..8e9e062c --- /dev/null +++ b/test/resources/ReferencedType.json @@ -0,0 +1,27 @@ +{ + "title": "Example Schema", + "type": "object", + "properties": { + "firstName": { + "type": "string" + }, + "lastName": { + "type": "string" + }, + "age": { + "description": "Age in years", + "type": "integer", + "minimum": 0 + }, + "height": { + "type": "number" + }, + "favoriteFoods": { + "type": "array" + }, + "likesDogs": { + "type": "boolean" + } + }, + "required": ["firstName", "lastName"] +} \ No newline at end of file diff --git a/test/test.ts b/test/test.ts index 1f00f01f..49bf82bb 100644 --- a/test/test.ts +++ b/test/test.ts @@ -21,14 +21,22 @@ modules.forEach((exports, name) => { describe(name, function() { exports.configurations.forEach((cfg: any) => { it(JSON.stringify(cfg.settings), () => { - expect(compile(exports.schema, cfg.settings)).to.be.equal(cfg.types) + if (cfg.error){ + expect(() => compile(cfg.schema, name, cfg.settings) ).to.throw(cfg.error.type, cfg.error.message) + } else { + expect(compile(exports.schema, name, cfg.settings)).to.be.equal(cfg.types) + } }) }) }) } else { describe(name, function() { it('default settings', () => { - expect(compile(exports.schema, exports.settings)).to.be.equal(exports.types) + if (exports.error){ + expect(() => compile(exports.schema, name, exports.settings) ).to.throw(exports.error.type, exports.error.message) + } else { + expect(compile(exports.schema, name, exports.settings)).to.be.equal(exports.types) + } }) }) }