Skip to content

Commit

Permalink
feat: Parse schema from body parameters (#23)
Browse files Browse the repository at this point in the history
  • Loading branch information
bennycode authored and ffflorian committed May 27, 2019
1 parent b29a1bd commit ccdd5eb
Show file tree
Hide file tree
Showing 8 changed files with 238 additions and 123 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@
"lint:ts": "tslint --config tslint.json --project tsconfig.json \"**/*.ts?(x)\"",
"prettier": "prettier \"**/*.{json,md}\"",
"release:patch": "git pull && yarn && yarn dist && npm version patch && npm publish && git push --follow-tags",
"start": "yarn clean:temp && ts-node src/cli.ts -i ./samples/wire-sso.json -o ./src/temp",
"start": "yarn clean:temp && ts-node src/cli.ts -i ./src/test/fixtures/wire-sso.json -o ./src/temp",
"test": "yarn lint && yarn test:node",
"test:node": "nyc jasmine --config=jasmine.json"
},
Expand Down
7 changes: 2 additions & 5 deletions samples/wire-ets.json
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,7 @@
"type": "string"
},
"type": {
"format": "string"
"type": "string"
}
},
"required": [
Expand Down Expand Up @@ -1990,10 +1990,7 @@
"200": {
"description": "",
"schema": {
"additionalProperties": {
"$ref": "#/definitions/Instance"
},
"type": "object"
"$ref": "#/definitions/Instance"
}
},
"404": {
Expand Down
112 changes: 112 additions & 0 deletions src/generators/MethodGenerator.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import {Operation} from 'swagger-schema-official';
import {MethodGenerator} from './MethodGenerator';

const WireSSO = require('../test/fixtures/wire-sso.json');

describe('MethodGenerator', () => {
describe('constructor', () => {
it('constructs a RESTful method name', () => {
const url = '/identity-providers';
const method = 'post';
const operation: Operation = {
responses: {
'201': {
description: '',
schema: {
properties: {
extraInfo: {
example: '00000000-0000-0000-0000-000000000000',
format: 'uuid',
type: 'string',
},
id: {
example: '00000000-0000-0000-0000-000000000000',
format: 'uuid',
type: 'string',
},
metadata: {
properties: {
certAuthnResponse: {
items: {
type: 'string',
},
minItems: 1,
type: 'array',
},
issuer: {
type: 'string',
},
requestURI: {
type: 'string',
},
},
required: ['issuer', 'requestURI', 'certAuthnResponse'],
type: 'object',
},
},
required: ['id', 'metadata', 'extraInfo'],
type: 'object',
},
},
'400': {
description: 'Invalid `body`',
},
},
};

const methodDefinition = new MethodGenerator(url, method, operation, WireSSO);

expect(methodDefinition.formattedUrl).toBe("'/identity-providers'");
expect(methodDefinition.method).toBe('post');
expect(methodDefinition.normalizedUrl).toBe('/identity-providers');
expect(methodDefinition.parameterMethod).toBe('postAll');
expect(methodDefinition.parameterName).toBeUndefined();
expect(methodDefinition.returnType).toBe(
'{ extraInfo: string; id: string, metadata: { certAuthnResponse: Array<string>; issuer: string, requestURI: string } }'
);
});

it('recognizes URL variables', () => {
const url = '/identity-providers/{id}';
const method = 'delete';
const operation: Operation = {responses: {'204': {description: ''}, '404': {description: '`id` not found'}}};

const methodDefinition = new MethodGenerator(url, method, operation, WireSSO);

expect(methodDefinition.method).toBe('delete');
expect(methodDefinition.normalizedUrl).toBe('/identity-providers');
expect(methodDefinition.parameterMethod).toBe('deleteById');
expect(methodDefinition.parameterName).toBe('id');
});

it('builds body parameters', () => {
const url = '/identity-providers/{id}';
const method = 'post';
const operation: Operation = {
parameters: [
{
in: 'body',
name: 'body',
required: false,
schema: {
properties: {
user: {
type: 'string',
},
},
},
},
],
responses: {'200': {description: ''}, '404': {description: '`id` not found'}},
};

const methodDefinition = new MethodGenerator(url, method, operation, WireSSO);

expect(methodDefinition.method).toBe('post');
expect(methodDefinition.normalizedUrl).toBe('/identity-providers');
expect(methodDefinition.parameterMethod).toBe('postById');
expect(methodDefinition.parameterName).toBe('id');
expect(methodDefinition.bodyParameters![0]).toEqual({name: 'body', type: '{ user: string }'});
});
});
});
98 changes: 84 additions & 14 deletions src/generators/MethodGenerator.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {Response, Schema, Spec} from 'swagger-schema-official';
import {Operation, Parameter, Reference, Response, Schema, Spec} from 'swagger-schema-official';
import {inspect} from 'util';

import * as StringUtil from '../util/StringUtil';
Expand All @@ -19,24 +19,31 @@ export enum TypeScriptType {
STRING = 'string',
}

interface BodyParameter {
name: string;
type: string;
}

export class MethodGenerator {
private readonly responses: Record<string, Response>;
private readonly responses: Record<string, Response | Reference>;
private readonly spec: Spec;
private readonly url: string;
private readonly operation: Operation;
readonly formattedUrl: string;
readonly method: string;
readonly normalizedUrl: string;
readonly parameterData?: string;
readonly bodyParameters?: BodyParameter[];
readonly parameterMethod: string;
readonly parameterName?: string;
readonly returnType: string;

constructor(url: string, method: string, responses: Record<string, Response>, spec: Spec) {
constructor(url: string, method: string, operation: Operation, spec: Spec) {
this.url = url;
this.operation = operation;
this.normalizedUrl = StringUtil.normalizeUrl(url);
this.formattedUrl = `'${url}'`;
this.spec = spec;
this.responses = responses;
this.responses = operation.responses;

const parameterMatch = url.match(/\{([^}]+)\}/);

Expand All @@ -48,18 +55,79 @@ export class MethodGenerator {
const postFix = parameterMatch ? `By${StringUtil.camelCase(parameterMatch.splice(1), true)}` : 'All';
this.parameterMethod = `${method}${postFix}`;

if (method === 'delete' || method === 'head') {
this.method = method;

if (this.method === 'delete' || this.method === 'head') {
this.returnType = 'void';
} else {
this.returnType = this.buildResponseSchema();
}
this.method = method;

this.bodyParameters = this.buildBodyParameters(this.operation.parameters);
}

private parameterIsReference(parameter: Reference | Parameter): parameter is Reference {
return !!(parameter as Reference).$ref;
}

private buildBodyParameters(parameters?: (Parameter | Reference)[]): BodyParameter[] | undefined {
if (!parameters) {
return undefined;
}

return parameters
.map(parameter => {
if (this.parameterIsReference(parameter)) {
if (!parameter.$ref.startsWith('#/definitions')) {
console.warn(`Invalid reference "${parameter.$ref}".`);
return undefined;
}
if (!this.spec.definitions) {
console.warn(`No reference found for "${parameter.$ref}".`);
return undefined;
}
const definition = parameter.$ref.replace('#/definitions', '');
return this.buildBodyParameters([this.spec.definitions[definition]] as Parameter[]);
}

if (parameter.in === 'path') {
return undefined;
}

if (parameter.in !== 'body') {
console.warn(
`Skipping parameter "${parameter.name}" because it's located in "${
parameter.in
}", which is not supported yet.`
);
return undefined;
}

return {
name: parameter.name,
type: parameter.schema ? this.buildType(parameter.schema, parameter.name) : TypeScriptType.EMPTY_OBJECT,
};
})
.filter(Boolean) as BodyParameter[];
}

private buildType(schema: Schema, schemaName: string): string {
let {required: requiredProperties, properties, type: schemaType} = schema;
const {allOf: multipleSchemas, enum: enumType} = schema;

if (schema.$ref && schema.$ref.startsWith('#/definitions')) {
if (multipleSchemas) {
return multipleSchemas.map(includedSchema => this.buildType(includedSchema, schemaName)).join('|');
}

if (enumType) {
return `"${enumType.join('" | "')}"`;
}

if (schema.$ref) {
if (!schema.$ref.startsWith('#/definitions')) {
console.warn(`Invalid reference "${schema.$ref}".`);
return TypeScriptType.EMPTY_OBJECT;
}
if (!this.spec.definitions) {
console.warn(`No reference found for "${schema.$ref}".`);
return TypeScriptType.EMPTY_OBJECT;
Expand Down Expand Up @@ -90,7 +158,7 @@ export class MethodGenerator {

for (const property of Object.keys(properties)) {
const propertyName = requiredProperties && !requiredProperties.includes(property) ? `${property}?` : property;
schema[propertyName] = this.buildType(properties[property], property);
schema[propertyName] = this.buildType(properties[property], `${schemaName}/${property}`);
}

return inspect(schema, {breakLength: Infinity})
Expand All @@ -109,7 +177,9 @@ export class MethodGenerator {
return `${TypeScriptType.ARRAY}<${itemType}>`;
}

const schemes = schema.items.map(itemSchema => this.buildType(itemSchema, schemaName)).join('|');
const schemes = schema.items
.map((itemSchema, index) => this.buildType(itemSchema, `${schemaName}[${index}]`))
.join('|');
return `${TypeScriptType.ARRAY}<${schemes}>`;
}
default: {
Expand All @@ -119,13 +189,13 @@ export class MethodGenerator {
}

private buildResponseSchema(): string {
const response200 = this.responses['200'];
const response201 = this.responses['201'];
const response200 = this.responses['200'] as Response;
const response201 = this.responses['201'] as Response;

const response200Schema =
response200 && response200.schema ? this.buildType(response200.schema, 'response200') : '';
response200 && response200.schema ? this.buildType(response200.schema, `${this.url}/${this.method}/200`) : '';
const response201Schema =
response201 && response201.schema ? this.buildType(response201.schema, 'response200') : '';
response201 && response201.schema ? this.buildType(response201.schema, `${this.url}/${this.method}/201`) : '';

const responseSchema =
response200Schema && response201Schema
Expand Down
2 changes: 1 addition & 1 deletion src/generators/ResourceGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export class ResourceGenerator extends TemplateGenerator {

Object.entries(resources).forEach(([url, definition]) => {
for (const [method, data] of Object.entries(definition)) {
const methodDefinition = new MethodGenerator(url, method, data.responses, spec);
const methodDefinition = new MethodGenerator(url, method, data, spec);
this.methods.push(methodDefinition);
}
});
Expand Down
12 changes: 10 additions & 2 deletions src/templates/Resource.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,20 @@ export class {{{name}}} {

async {{{this.parameterMethod}}}
(
{{#if this.parameterName}}{{{this.parameterName}}}: string{{/if}}
{{#if this.parameterName}}{{{this.parameterName}}}: string,{{/if}}
{{#each bodyParameters}}
{{{this.name}}}{{#isnt this.required}}?{{/isnt}}: {{{this.type}}},
{{/each}}
): Promise<{{{this.returnType}}}> {
const resource = {{{this.formattedUrl}}};
const response = await this.apiClient.{{{this.method}}}
{{#isnt this.returnType "void"}}<{{{this.returnType}}}>{{/isnt}}
(resource);
(
resource
{{#if bodyParameters}}
, { {{#each bodyParameters}}{{this.name}},{{/each}} }
{{/if}}
);
return response.data;
}
{{/each}}
Expand Down
2 changes: 1 addition & 1 deletion src/templates/index.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@
* It should not be modified by hand.
*/
{{#each exports}}
export * from '{{this}}';
export * from '{{{this}}}';
{{/each}}

0 comments on commit ccdd5eb

Please sign in to comment.