Skip to content

Commit

Permalink
Merge pull request #111 from alesandroLang/extends
Browse files Browse the repository at this point in the history
added support for the "extends" keyword
  • Loading branch information
bcherny committed Aug 7, 2017
2 parents 87f548e + 930727f commit ec62911
Show file tree
Hide file tree
Showing 8 changed files with 190 additions and 20 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ json2ts -i foo.json -o foo.d.ts
- [x] `anyOf` ("union")
- [x] `oneOf` (treated like `anyOf`)
- [x] `additionalProperties` of type
- [ ] [`extends`](https://github.com/json-schema/json-schema/wiki/Extends)
- [x] [`extends`](https://github.com/json-schema/json-schema/wiki/Extends)
- [x] `required` properties on objects ([eg](https://github.com/tdegrunt/jsonschema/blob/67c0e27ce9542efde0bf43dc1b2a95dd87df43c3/examples/all.js#L130))
- [ ] `validateRequired` ([eg](https://github.com/tdegrunt/jsonschema/blob/67c0e27ce9542efde0bf43dc1b2a95dd87df43c3/examples/all.js#L124))
- [x] literal objects in enum ([eg](https://github.com/tdegrunt/jsonschema/blob/67c0e27ce9542efde0bf43dc1b2a95dd87df43c3/examples/all.js#L236))
Expand Down
13 changes: 10 additions & 3 deletions src/generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ function declareEnums(
type = generateStandaloneEnum(ast, options) + '\n'
break
case 'INTERFACE':
type = ast.params.reduce((prev, { ast }) =>
type = getSuperTypesAndParams(ast).reduce((prev, ast) =>
prev + declareEnums(ast, options, processed),
'')
break
Expand Down Expand Up @@ -69,7 +69,7 @@ function declareNamedInterfaces(
case 'INTERFACE':
type = [
hasStandaloneName(ast) && (ast.standaloneName === rootASTName || options.declareExternallyReferenced) && generateStandaloneInterface(ast, options),
ast.params.map(({ ast }) =>
getSuperTypesAndParams(ast).map(ast =>
declareNamedInterfaces(ast, options, rootASTName, processed)
).filter(Boolean).join('\n')
].filter(Boolean).join('\n')
Expand Down Expand Up @@ -109,7 +109,7 @@ function declareNamedTypes(
type = ''
break
case 'INTERFACE':
type = ast.params.map(({ ast }) => declareNamedTypes(ast, options, processed)).filter(Boolean).join('\n')
type = getSuperTypesAndParams(ast).map(ast => declareNamedTypes(ast, options, processed)).filter(Boolean).join('\n')
break
case 'INTERSECTION':
case 'UNION':
Expand Down Expand Up @@ -216,6 +216,7 @@ function generateStandaloneEnum(ast: TEnum, options: Options): string {
function generateStandaloneInterface(ast: TNamedInterface, options: Options): string {
return (hasComment(ast) ? generateComment(ast.comment, options, 0) + '\n' : '')
+ `export interface ${toSafeString(ast.standaloneName)} `
+ (ast.superTypes.length > 0 ? `extends ${ast.superTypes.map(superType => toSafeString(superType.standaloneName)).join(', ')} ` : '')
+ generateInterface(ast, options, 0)
+ (options.enableTrailingSemicolonForInterfaces ? ';' : '')
}
Expand All @@ -239,3 +240,9 @@ function escapeKeyName(keyName: string): string {
}
return JSON.stringify(keyName)
}

function getSuperTypesAndParams(ast: TInterface): AST[] {
return ast.params
.map(param => param.ast)
.concat(ast.superTypes)
}
52 changes: 36 additions & 16 deletions src/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { whiteBright } from 'cli-color'
import { JSONSchema4Type, JSONSchema4TypeName } from 'json-schema'
import { findKey, includes, isPlainObject, map } from 'lodash'
import { typeOfSchema } from './typeOfSchema'
import { AST, T_ANY, T_ANY_ADDITIONAL_PROPERTIES, TInterfaceParam } from './types/AST'
import { AST, hasName, T_ANY, T_ANY_ADDITIONAL_PROPERTIES, TInterface, TInterfaceParam, TNamedInterface } from './types/AST'
import { JSONSchema, JSONSchemaWithDefinitions, SchemaSchema } from './types/JSONSchema'
import { error, log } from './utils'

Expand Down Expand Up @@ -102,13 +102,7 @@ function parseNonLiteral(
type: 'ENUM'
})
case 'NAMED_SCHEMA':
return set({
comment: schema.description,
keyName,
params: parseSchema(schema as SchemaSchema, rootSchema, processed),
standaloneName: computeSchemaName(schema as SchemaSchema),
type: 'INTERFACE'
})
return set(newInterface(schema, rootSchema, processed, keyName))
case 'NULL':
return set({
comment: schema.description,
Expand Down Expand Up @@ -182,14 +176,7 @@ function parseNonLiteral(
type: 'UNION'
})
case 'UNNAMED_SCHEMA':
return set({
comment: schema.description,
keyName,
params: parseSchema(schema as SchemaSchema, rootSchema, processed),
standaloneName: computeSchemaName(schema as SchemaSchema)
|| keyNameFromDefinition,
type: 'INTERFACE'
})
return set(newInterface(schema, rootSchema, processed, keyName, keyNameFromDefinition))
case 'UNTYPED_ARRAY':
return set({
comment: schema.description,
Expand All @@ -201,6 +188,39 @@ function parseNonLiteral(
}
}

function newInterface(schema: JSONSchema, rootSchema: JSONSchema, processed: Map<JSONSchema | JSONSchema4Type, AST>, keyName?: string,
keyNameFromDefinition?: string): TInterface {
return {
comment: schema.description,
keyName,
params: parseSchema(schema as SchemaSchema, rootSchema, processed),
standaloneName: computeSchemaName(schema as SchemaSchema) || keyNameFromDefinition,
superTypes: parseSuperTypes(schema as SchemaSchema, processed),
type: 'INTERFACE'
}
}

function parseSuperTypes(schema: SchemaSchema, processed: Map<JSONSchema | JSONSchema4Type, AST>): TNamedInterface[] {
// type assertion needed because of dereferencing step
const superTypes = schema.extends as SchemaSchema | SchemaSchema[] | undefined
if (typeof superTypes === 'undefined') {
return []
} else if (superTypes instanceof Array) {
return superTypes.map(superType => newNamedInterface(superType, superType, processed))
} else {
return [newNamedInterface(superTypes, superTypes, processed)]
}
}

function newNamedInterface(schema: JSONSchema, rootSchema: JSONSchema, processed: Map<JSONSchema | JSONSchema4Type, AST>): TNamedInterface {
const namedInterface = newInterface(schema, rootSchema, processed)
if (hasName(namedInterface)) {
return namedInterface
} else {
throw error('Supertype must have standalone name!', namedInterface)
}
}

/**
* Compute a schema name using a series of fallbacks
*/
Expand Down
6 changes: 6 additions & 0 deletions src/types/AST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ export function hasStandaloneName(ast: AST): ast is ASTWithStandaloneName {
return 'standaloneName' in ast && ast.standaloneName != null && ast.standaloneName !== ''
}

export function hasName(ast: TInterface): ast is TNamedInterface {
return hasStandaloneName(ast)
}

//////////////////////////////////////////// types

export interface TAny extends AbstractAST {
Expand Down Expand Up @@ -56,12 +60,14 @@ export interface TEnumParam {
export interface TInterface extends AbstractAST {
type: 'INTERFACE'
params: TInterfaceParam[]
superTypes: TNamedInterface[]
}

export interface TNamedInterface extends AbstractAST {
standaloneName: string
type: 'INTERFACE'
params: TInterfaceParam[]
superTypes: TNamedInterface[]
}

export interface TInterfaceParam {
Expand Down
51 changes: 51 additions & 0 deletions test/e2e/extends.1.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
export const input = {
"title": "Extends",
'type': "object",
"extends": {
"$ref": "test/resources/BaseType.1.json"
},
properties: {
"foo": {
"type": "string"
}
},
required: ["foo"],
additionalProperties: false
}

export const outputs = [
{
options: {
declareExternallyReferenced: true
},
output: `/**
* This file was automatically generated by json-schema-to-typescript.
* DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file,
* and run json-schema-to-typescript to regenerate this file.
*/
export interface Extends extends Base1 {
foo: string;
}
export interface Base1 {
firstName: string;
lastName: string;
}
`
},
{
options: {
declareExternallyReferenced: false
},
output: `/**
* This file was automatically generated by json-schema-to-typescript.
* DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file,
* and run json-schema-to-typescript to regenerate this file.
*/
export interface Extends extends Base1 {
foo: string;
}
`
}
]
59 changes: 59 additions & 0 deletions test/e2e/extends.2.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
export const input = {
"title": "Extends",
'type': "object",
"extends": [
{
"$ref": "test/resources/BaseType.1.json"
},
{
"$ref": "test/resources/BaseType.2.json"
}
],
properties: {
"foo": {
"type": "string"
}
},
required: ["foo"],
additionalProperties: false
}

export const outputs = [
{
options: {
declareExternallyReferenced: true
},
output: `/**
* This file was automatically generated by json-schema-to-typescript.
* DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file,
* and run json-schema-to-typescript to regenerate this file.
*/
export interface Extends extends Base1, Base2 {
foo: string;
}
export interface Base1 {
firstName: string;
lastName: string;
}
export interface Base2 {
age: number;
}
`
},
{
options: {
declareExternallyReferenced: false
},
output: `/**
* This file was automatically generated by json-schema-to-typescript.
* DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file,
* and run json-schema-to-typescript to regenerate this file.
*/
export interface Extends extends Base1, Base2 {
foo: string;
}
`
}
]
15 changes: 15 additions & 0 deletions test/resources/BaseType.1.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"id": "http://dummy.com/api/base1",
"title": "Base1",
"type": "object",
"properties": {
"firstName": {
"type": "string"
},
"lastName": {
"type": "string"
}
},
"required": ["firstName", "lastName"],
"additionalProperties": false
}
12 changes: 12 additions & 0 deletions test/resources/BaseType.2.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"id": "http://dummy.com/api/base2",
"title": "Base2",
"type": "object",
"properties": {
"age": {
"type": "integer"
}
},
"required": ["age"],
"additionalProperties": false
}

0 comments on commit ec62911

Please sign in to comment.