Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/methods #121

Closed
wants to merge 8 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions test/programs/method-extreme/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
interface MyObject {
setTime?<T extends Array<string> & {name: {first: string, last: string}}, T2>(d: {name: string, test: number} | {name: {first: string, last: string}}, someParam?: T2): Array<T>;
}
93 changes: 93 additions & 0 deletions test/programs/method-extreme/schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
{
"type": "object",
"properties": {
"setTime": {
"parameters": [
{
"name": "d",
"type": "union",
"typeArguments": [
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not familiar with this syntax. Can you send me some resources so I can evaluate this?

Copy link
Author

@heikomat heikomat Jun 5, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@domoritz: TypeScript - generics

  • the string in Array<string> is a typeArgument
  • the implements in T implements Array<string> makes Array<string> a constraint (on T)
  • for union-types and intersection-types in method-definitions i used typeArguments to represent the types that are part of that union/intersection.

I unfortunately don't really have sources on how a function should be represented in a JSON-Schema, because there barely are sources. I've read somewhere that JSON-Schema isn't really meant to include functions, so i somewhat came up with my own way to represent them.

If there happens to be standards for this, that i just overlooked, i'm more than happy to follow it

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know Generics in typescript but this syntax in JSON schema strikes me as an odd use case for JSON schema. A schema is usually intended for data structures and not really for functions or method signatures.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, usually you wouldn't use JSON-Schema for that, because a function has functionality. But the use case here is not to verify a whole function (including the implementation), but to verify it's declaration. The goal is to compare "functions" based on their name, parameter, parameter-order, parameter-types, return-type etc.

I think JSON-Schema can be a good tool to verify function-declarations, without comparing the function-implementation.

{
"type": "object",
"properties": {
"name": {
"type": "string"
},
"test": {
"type": "number"
}
}
},
{
"type": "object",
"properties": {
"name": {
"type": "object",
"properties": {
"first": {
"type": "string"
},
"last": {
"type": "string"
}
}
}
}
}
]
},
{
"name": "someParam",
"optional": true,
"type": "T2"
}
],
"typeParameters": [
{
"name": "T",
"constraint": {
"type": "intersection",
"typeArguments": [
{
"type": "Array",
"typeArguments": [
{
"type": "string"
}
]
},
{
"type": "object",
"properties": {
"name": {
"type": "object",
"properties": {
"first": {
"type": "string"
},
"last": {
"type": "string"
}
}
}
}
}
]
}
},
{
"name": "T2"
}
],
"type": "object",
"returnType": "Array",
"returnTypeArguments": [
{
"type": "T"
}
],
"optional": true
}
},
"$schema": "http://json-schema.org/draft-04/schema#"
}
3 changes: 3 additions & 0 deletions test/programs/method-simple/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
interface MyObject {
setTime(d: string): void;
}
18 changes: 18 additions & 0 deletions test/programs/method-simple/schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"type": "object",
"properties": {
"setTime": {
"parameters": [
{
"name": "d",
"type": "string"
}
],
"type": "object"
}
},
"required": [
"setTime"
],
"$schema": "http://json-schema.org/draft-04/schema#"
}
3 changes: 3 additions & 0 deletions test/schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@ describe("schema", () => {
// not supported right now
// assertSchema("module-interface-deep", "main.ts", "Def");

assertSchema("method-extreme", "main.ts", "MyObject");
assertSchema("method-simple", "main.ts", "MyObject");

assertSchema("enums-string", "main.ts", "MyObject");
assertSchema("enums-number", "main.ts", "MyObject");
assertSchema("enums-number-initialized", "main.ts", "Enum");
Expand Down
160 changes: 160 additions & 0 deletions typescript-json-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,18 @@ export type PartialArgs = Partial<Args>;

export type PrimitiveType = number | boolean | string | null;

export type TypeArgument = {
type?: string,
typeArguments?: TypeArgument[],
properties?: {},
constraint?: TypeArgument
};

export type Parameter = TypeArgument & {
name: string,
optional?: boolean
};

export type Definition = {
$ref?: string,
description?: string,
Expand All @@ -59,6 +71,7 @@ export type Definition = {
anyOf?: Definition[],
title?: string,
type?: string | string[],
typeArguments?: TypeArgument[],
definitions?: {[key: string]: any},
format?: string,
items?: Definition,
Expand All @@ -73,6 +86,11 @@ export type Definition = {
propertyOrder?: string[],
properties?: {},
defaultProperties?: string[],
parameters?: Parameter[]
returnType?: string,
returnTypeArguments?: TypeArgument[],
typeParameters?: TypeArgument[],
optional?: boolean,

typeof?: "function"
};
Expand Down Expand Up @@ -502,6 +520,146 @@ export class JsonSchemaGenerator {
return definition;
}

private typeIsTypeReference(type: ts.Node): type is ts.TypeReferenceNode {
return type.kind === ts.SyntaxKind.TypeReference;
}

private typeIsUnionType(type: ts.Node): type is ts.UnionTypeNode {
return type.kind === ts.SyntaxKind.UnionType;
}

private typeIsIntersectionType(type: ts.Node): type is ts.IntersectionTypeNode {
return type.kind === ts.SyntaxKind.IntersectionType;
}

private typeIsTypeLiteral(type: ts.Node): type is ts.TypeLiteralNode {
return type.kind === ts.SyntaxKind.TypeLiteral;
}

private typeElementIsPropertySignature(type: ts.Node): type is ts.PropertySignature {
return type.kind === ts.SyntaxKind.PropertySignature;
}

private declarationIsPrameterDeclaration(declaration: ts.Declaration): declaration is ts.ParameterDeclaration {
return declaration.kind === ts.SyntaxKind.Parameter;
}

private declarationIsTypeParameterDeclaration(declaration: ts.Declaration): declaration is ts.TypeParameterDeclaration {
return declaration.kind === ts.SyntaxKind.TypeParameter;
}

private getTypeDescription(type?: ts.Node): TypeArgument {
const typeObject: TypeArgument = {};

if (!type) {
return typeObject;
}

if (this.typeIsUnionType(type)) {
typeObject.type = "union";
typeObject.typeArguments = type.types.map((subType: ts.TypeNode) => {
return this.getTypeDescription(subType);
});
} else if (this.typeIsIntersectionType(type)) {
typeObject.type = "intersection";
typeObject.typeArguments = type.types.map((subType: ts.TypeNode) => {
return this.getTypeDescription(subType);
});
} else if (this.typeIsTypeReference(type)) {
typeObject.type = type.typeName.getText();
if (type.typeArguments && type.typeArguments.length > 0) {
typeObject.typeArguments = type.typeArguments.map((typeArgument: ts.TypeNode) => {
return this.getTypeDescription(typeArgument);
});
}
} else if (type.kind === ts.SyntaxKind.StringKeyword) {
typeObject.type = "string";
} else if (type.kind === ts.SyntaxKind.NumberKeyword) {
typeObject.type = "number";
} else if (type.kind === ts.SyntaxKind.BooleanKeyword) {
typeObject.type = "boolean";
} else if (this.typeIsTypeLiteral(type)) {
typeObject.type = "object";
typeObject.properties = {};
for (let i = 0; i < type.members.length; i++) {
const typeMember: ts.TypeElement = type.members[i];
if (this.typeElementIsPropertySignature(typeMember)) {
typeObject.properties[typeMember.name.getText()] = this.getTypeDescription(typeMember.type);
}
}
}

return typeObject;
}

private getMethodParameters(parameters: ts.NodeArray<ts.Declaration>): Array<Parameter> {
return parameters.sort((param1, param2) => {
return param1.pos - param2.pos;
})
.map((parameter: ts.Declaration) => {
return this.getMethodParameter(parameter);
});
}

private getMethodParameter(parameter: ts.Declaration) {
let typeObject: TypeArgument = {};
if (this.declarationIsPrameterDeclaration(parameter)) {
typeObject = this.getTypeDescription(parameter.type);
} else if (this.declarationIsTypeParameterDeclaration(parameter)) {
typeObject = this.getTypeDescription(parameter);
} else {
return {name: "__name_not_found__"};
}

const parameterObject: Parameter = {
name: parameter.name.getText(),
};

if (this.declarationIsPrameterDeclaration(parameter) && parameter.questionToken && parameter.questionToken.kind === ts.SyntaxKind.QuestionToken) {
parameterObject.optional = true;
}

if (this.declarationIsTypeParameterDeclaration(parameter) && parameter.constraint) {
parameterObject.constraint = this.getTypeDescription(parameter.constraint);
}

if (typeObject.type) {
parameterObject.type = typeObject.type;
}

if (typeObject.typeArguments) {
parameterObject.typeArguments = typeObject.typeArguments;
}

return parameterObject;
}

private getMethodDefinition(methodType: ts.Type, definition: Definition): Definition {
definition.type = "object";

const declaration: ts.MethodDeclaration = <ts.MethodDeclaration> methodType.getSymbol().getDeclarations()[0];
definition.parameters = this.getMethodParameters(declaration.parameters);
if (declaration.typeParameters) {
definition.typeParameters = this.getMethodParameters(declaration.typeParameters);
}

const returnType: TypeArgument = this.getTypeDescription(declaration.type);
if (returnType.type) {
definition.returnType = returnType.type;
}

if (returnType.typeArguments) {
definition.returnTypeArguments = returnType.typeArguments;
}

if (declaration.questionToken && declaration.questionToken.kind === ts.SyntaxKind.QuestionToken) {
definition.optional = true;
}
// The description describes the return-type, which is misleading, because one would expect it to describe the method itsef
delete definition.description;
return definition;
}

private getClassDefinition(clazzType: ts.Type, tc: ts.TypeChecker, definition: Definition): Definition {
const node = clazzType.getSymbol().getDeclarations()[0];
if (this.args.useTypeOfKeyword && node.kind === ts.SyntaxKind.FunctionType) {
Expand Down Expand Up @@ -766,6 +924,8 @@ export class JsonSchemaGenerator {
// {} is TypeLiteral with no members. Need special case because it doesn't have declarations.
definition.type = "object";
definition.properties = {};
} else if (symbol && symbol.getDeclarations()[0].kind === ts.SyntaxKind.MethodSignature) {
this.getMethodDefinition(typ, definition);
} else {
this.getClassDefinition(typ, tc, definition);
}
Expand Down