Skip to content

Commit

Permalink
Hashmap support
Browse files Browse the repository at this point in the history
  • Loading branch information
alexey-pelykh committed Mar 10, 2017
1 parent d950784 commit 92e7083
Show file tree
Hide file tree
Showing 16 changed files with 362 additions and 130 deletions.
1 change: 1 addition & 0 deletions src/metadataGeneration/metadataGenerator.ts
Expand Up @@ -106,6 +106,7 @@ export interface ReferenceType {
description: string;
name: string;
properties: Property[];
additionalProperties?: Property[];
enum?: string[];
}

Expand Down
37 changes: 32 additions & 5 deletions src/metadataGeneration/resolveType.ts
Expand Up @@ -58,12 +58,16 @@ function generateReferenceType(typeName: string): ReferenceType {

const modelTypeDeclaration = getModelTypeDeclaration(typeName);
const properties = getModelTypeProperties(modelTypeDeclaration);
const additionalProperties = getModelTypeAdditionalProperties(modelTypeDeclaration);

const referenceType: ReferenceType = {
description: getModelDescription(modelTypeDeclaration),
name: typeName,
properties: properties
};
if (additionalProperties && additionalProperties.length) {
referenceType.additionalProperties = additionalProperties;
}
if (modelTypeDeclaration.kind === ts.SyntaxKind.TypeAliasDeclaration) {
const innerType = modelTypeDeclaration.type;
if (innerType.kind === ts.SyntaxKind.UnionType && (innerType as any).types) {
Expand Down Expand Up @@ -132,26 +136,26 @@ function getModelTypeProperties(node: UsableDeclaration) {
const interfaceDeclaration = node as ts.InterfaceDeclaration;
return interfaceDeclaration.members
.filter(member => member.kind === ts.SyntaxKind.PropertySignature)
.map((property: any) => {
const propertyDeclaration = property as ts.PropertyDeclaration;
.map((member: any) => {
const propertyDeclaration = member as ts.PropertyDeclaration;
const identifier = propertyDeclaration.name as ts.Identifier;

if (!propertyDeclaration.type) { throw new Error('No valid type found for property declaration.'); }

return {
description: getNodeDescription(propertyDeclaration),
name: identifier.text,
required: !property.questionToken,
required: !propertyDeclaration.questionToken,
type: ResolveType(propertyDeclaration.type)
};
});
}

if (node.kind === ts.SyntaxKind.TypeAliasDeclaration) {
/**
* TOOD
* TODO
*
* Flesh this out so that we can properly support Type Alii instead of just assuming
* Flesh this out so that we can properly support Type Alias instead of just assuming
* string literal enums
*/
return [];
Expand Down Expand Up @@ -186,6 +190,29 @@ function getModelTypeProperties(node: UsableDeclaration) {
});
}

function getModelTypeAdditionalProperties(node: UsableDeclaration) {
if (node.kind === ts.SyntaxKind.InterfaceDeclaration) {
const interfaceDeclaration = node as ts.InterfaceDeclaration;
return interfaceDeclaration.members
.filter(member => member.kind === ts.SyntaxKind.IndexSignature)
.map((member: any) => {
const indexSignatureDeclaration = member as ts.IndexSignatureDeclaration;

const indexType = ResolveType(<ts.TypeNode>indexSignatureDeclaration.parameters[0].type);
if (indexType !== 'string') { throw new Error('Only string indexers are supported'); }

return {
description: '',
name: '',
required: true,
type: ResolveType(<ts.TypeNode>indexSignatureDeclaration.type)
};
});
}

return undefined;
}

function hasPublicModifier(node: ts.Node) {
return !node.modifiers || node.modifiers.every(modifier => {
return modifier.kind !== ts.SyntaxKind.ProtectedKeyword && modifier.kind !== ts.SyntaxKind.PrivateKeyword;
Expand Down
36 changes: 32 additions & 4 deletions src/routeGeneration/routeGenerator.ts
Expand Up @@ -72,9 +72,20 @@ export class RouteGenerator {
const models: any = {
{{#each models}}
'{{name}}': {
{{#each properties}}
'{{name}}': { typeName: '{{typeName}}', required: {{required}} {{#if arrayType}}, arrayType: '{{arrayType}}' {{/if}} },
{{/each}}
{{#if properties}}
properties: {
{{#each properties}}
'{{name}}': { typeName: '{{typeName}}', required: {{required}} {{#if arrayType}}, arrayType: '{{arrayType}}' {{/if}} },
{{/each}}
},
{{/if}}
{{#if additionalProperties}}
additionalProperties: [
{{#each additionalProperties}}
{typeName: ''},
{{/each}}
],
{{/if}}
},
{{/each}}
};
Expand Down Expand Up @@ -114,10 +125,14 @@ export class RouteGenerator {
return Object.keys(this.metadata.ReferenceTypes).map(key => {
const referenceType = this.metadata.ReferenceTypes[key];

return {
let templateModel: TemplateModel = {
name: key,
properties: referenceType.properties.map(property => this.getTemplateProperty(property))
};
if (referenceType.additionalProperties && referenceType.additionalProperties.length) {
templateModel.additionalProperties = referenceType.additionalProperties.map(property => this.getTemplateAdditionalProperty(property));
}
return templateModel;
});
}

Expand Down Expand Up @@ -154,6 +169,14 @@ export class RouteGenerator {
return templateProperty;
}

private getTemplateAdditionalProperty(source: Property): TemplateAdditionalProperty {
const templateAdditionalProperty: TemplateAdditionalProperty = {
typeName: this.getStringRepresentationOfType(source.type)
};

return templateAdditionalProperty;
}

private getTemplateParameter(parameter: Parameter): TemplateParameter {
const templateParameter: TemplateParameter = {
argumentName: parameter.argumentName,
Expand All @@ -175,6 +198,7 @@ export class RouteGenerator {
interface TemplateModel {
name: string;
properties: TemplateProperty[];
additionalProperties?: TemplateAdditionalProperty[];
}

interface TemplateProperty {
Expand All @@ -185,6 +209,10 @@ interface TemplateProperty {
request?: boolean;
}

interface TemplateAdditionalProperty {
typeName: string;
}

interface TemplateParameter {
name: String;
argumentName: string;
Expand Down
28 changes: 24 additions & 4 deletions src/routeGeneration/templateHelpers.ts
Expand Up @@ -69,10 +69,30 @@ function validateModel(modelValue: any, typeName: string): any {
const modelDefinition = models[typeName];

if (modelDefinition) {
Object.keys(modelDefinition).forEach((key: string) => {
const property = modelDefinition[key];
modelValue[key] = ValidateParam(property, modelValue[key], models, key);
});
if (modelDefinition.properties) {
Object.keys(modelDefinition.properties).forEach((key: string) => {
const property = modelDefinition.properties[key];
modelValue[key] = ValidateParam(property, modelValue[key], models, key);
});
}
if (modelDefinition.additionalProperties) {
Object.keys(modelValue).forEach((key: string) => {
let validatedValue;
for (const additionalProperty of modelDefinition.additionalProperties) {
try {
validatedValue = ValidateParam(additionalProperty, modelValue[key], models, key);
break;
} catch (err) {
continue;
}
}
if (validatedValue) {
modelValue[key] = validatedValue;
} else {
throw new Error(`No matching model found in additionalProperties`);
}
});
}
}

return modelValue;
Expand Down
16 changes: 16 additions & 0 deletions src/swagger/specGenerator.ts
Expand Up @@ -65,6 +65,9 @@ export class SpecGenerator {
required: referenceType.properties.filter(p => p.required).map(p => p.name),
type: 'object'
};
if (referenceType.additionalProperties) {
definitions[referenceType.name].additionalProperties = this.buildAdditionalProperties(referenceType.additionalProperties);
}
if (referenceType.enum) {
definitions[referenceType.name].type = 'string';
delete definitions[referenceType.name].properties;
Expand Down Expand Up @@ -182,6 +185,19 @@ export class SpecGenerator {
return swaggerProperties;
}

private buildAdditionalProperties(properties: Property[]) {
let swaggerAdditionalProperties: { [ref: string]: string } = {};

properties.forEach(property => {
const swaggerType = this.getSwaggerType(property.type);
if (swaggerType.$ref) {
swaggerAdditionalProperties['$ref'] = swaggerType.$ref;
}
});

return swaggerAdditionalProperties;
}

private buildOperation(controllerName: string, method: Method) {
const responses: any = {};

Expand Down
2 changes: 1 addition & 1 deletion src/swagger/swagger.ts
Expand Up @@ -133,7 +133,7 @@ export namespace Swagger {
export interface Schema extends BaseSchema {
$ref?: string;
allOf?: [Schema];
additionalProperties?: boolean;
additionalProperties?: boolean | { [ref: string]: string };
properties?: { [propertyName: string]: Schema };
discriminator?: string;
readOnly?: boolean;
Expand Down
90 changes: 59 additions & 31 deletions tests/fixtures/express/routes.ts
Expand Up @@ -12,51 +12,79 @@ import { SecurityTestController } from './../controllers/securityController';

const models: any = {
'TestModel': {
'numberValue': { typeName: 'number', required: true },
'numberArray': { typeName: 'array', required: true, arrayType: 'number' },
'stringValue': { typeName: 'string', required: true },
'stringArray': { typeName: 'array', required: true, arrayType: 'string' },
'boolValue': { typeName: 'boolean', required: true },
'boolArray': { typeName: 'array', required: true, arrayType: 'boolean' },
'modelValue': { typeName: 'TestSubModel', required: true },
'modelsArray': { typeName: 'array', required: true, arrayType: 'TestSubModel' },
'strLiteralVal': { typeName: 'StrLiteral', required: true },
'strLiteralArr': { typeName: 'array', required: true, arrayType: 'StrLiteral' },
'dateValue': { typeName: 'datetime', required: false },
'optionalString': { typeName: 'string', required: false },
'id': { typeName: 'number', required: true },
properties: {
'numberValue': { typeName: 'number', required: true },
'numberArray': { typeName: 'array', required: true, arrayType: 'number' },
'stringValue': { typeName: 'string', required: true },
'stringArray': { typeName: 'array', required: true, arrayType: 'string' },
'boolValue': { typeName: 'boolean', required: true },
'boolArray': { typeName: 'array', required: true, arrayType: 'boolean' },
'modelValue': { typeName: 'TestSubModel', required: true },
'modelsArray': { typeName: 'array', required: true, arrayType: 'TestSubModel' },
'strLiteralVal': { typeName: 'StrLiteral', required: true },
'strLiteralArr': { typeName: 'array', required: true, arrayType: 'StrLiteral' },
'dateValue': { typeName: 'datetime', required: false },
'optionalString': { typeName: 'string', required: false },
'modelsObjectIndirect': { typeName: 'TestSubModelContainer', required: false },
'id': { typeName: 'number', required: true },
},
},
'TestSubModel': {
'email': { typeName: 'string', required: true },
'circular': { typeName: 'TestModel', required: false },
'id': { typeName: 'number', required: true },
properties: {
'email': { typeName: 'string', required: true },
'circular': { typeName: 'TestModel', required: false },
'id': { typeName: 'number', required: true },
},
},
'StrLiteral': {
},
'TestSubModel2': {
properties: {
'testSubModel2': { typeName: 'boolean', required: true },
'email': { typeName: 'string', required: true },
'circular': { typeName: 'TestModel', required: false },
'id': { typeName: 'number', required: true },
},
},
'TestSubModelContainer': {
additionalProperties: [
{ typeName: '' },
],
},
'TestClassModel': {
'publicStringProperty': { typeName: 'string', required: true },
'optionalPublicStringProperty': { typeName: 'string', required: false },
'stringProperty': { typeName: 'string', required: true },
'publicConstructorVar': { typeName: 'string', required: true },
'optionalPublicConstructorVar': { typeName: 'string', required: false },
'id': { typeName: 'number', required: true },
properties: {
'publicStringProperty': { typeName: 'string', required: true },
'optionalPublicStringProperty': { typeName: 'string', required: false },
'stringProperty': { typeName: 'string', required: true },
'publicConstructorVar': { typeName: 'string', required: true },
'optionalPublicConstructorVar': { typeName: 'string', required: false },
'id': { typeName: 'number', required: true },
},
},
'Result': {
'value': { typeName: 'object', required: true },
properties: {
'value': { typeName: 'object', required: true },
},
},
'ErrorResponseModel': {
'status': { typeName: 'number', required: true },
'message': { typeName: 'string', required: true },
properties: {
'status': { typeName: 'number', required: true },
'message': { typeName: 'string', required: true },
},
},
'ParameterTestModel': {
'firstname': { typeName: 'string', required: true },
'lastname': { typeName: 'string', required: true },
'age': { typeName: 'number', required: true },
'human': { typeName: 'boolean', required: true },
properties: {
'firstname': { typeName: 'string', required: true },
'lastname': { typeName: 'string', required: true },
'age': { typeName: 'number', required: true },
'human': { typeName: 'boolean', required: true },
},
},
'UserResponseModel': {
'id': { typeName: 'number', required: true },
'name': { typeName: 'string', required: true },
properties: {
'id': { typeName: 'number', required: true },
'name': { typeName: 'string', required: true },
},
},
};

Expand Down

0 comments on commit 92e7083

Please sign in to comment.