diff --git a/src/client/interfaces/Enum.d.ts b/src/client/interfaces/Enum.d.ts index be38d2d1..18475c3f 100644 --- a/src/client/interfaces/Enum.d.ts +++ b/src/client/interfaces/Enum.d.ts @@ -1,7 +1,8 @@ -import { EnumValue } from './EnumValue'; +import { EnumSymbol } from './EnumSymbol'; export interface Enum { name: string; type: string; - values: EnumValue[]; + symbols: EnumSymbol[]; + validation: string | null; } diff --git a/src/client/interfaces/EnumValue.d.ts b/src/client/interfaces/EnumSymbol.ts similarity index 56% rename from src/client/interfaces/EnumValue.d.ts rename to src/client/interfaces/EnumSymbol.ts index aca3a4cf..921833e1 100644 --- a/src/client/interfaces/EnumValue.d.ts +++ b/src/client/interfaces/EnumSymbol.ts @@ -1,4 +1,4 @@ -export interface EnumValue { +export interface EnumSymbol { name: string; value: string; } diff --git a/src/client/interfaces/Model.d.ts b/src/client/interfaces/Model.d.ts index 53972d86..13ceb79b 100644 --- a/src/client/interfaces/Model.d.ts +++ b/src/client/interfaces/Model.d.ts @@ -1,6 +1,6 @@ import { ModelProperty } from './ModelProperty'; import { Enum } from './Enum'; -import { EnumValue } from './EnumValue'; +import { EnumSymbol } from './EnumSymbol'; export interface Model { isInterface: boolean; @@ -12,9 +12,9 @@ export interface Model { template: string | null; validation: string | null; description: string | null; - extends: string | null; + extends: string[]; imports: string[]; enums: Enum[]; - values: EnumValue[]; + symbols: EnumSymbol[]; properties: ModelProperty[]; } diff --git a/src/client/interfaces/ModelProperty.d.ts b/src/client/interfaces/ModelProperty.d.ts index 86440c6f..e883d9a9 100644 --- a/src/client/interfaces/ModelProperty.d.ts +++ b/src/client/interfaces/ModelProperty.d.ts @@ -4,6 +4,6 @@ export interface ModelProperty { required: boolean; nullable: boolean; readOnly: boolean; - validation: string | null; description: string | null; + validation: string | null; } diff --git a/src/client/interfaces/Operation.d.ts b/src/client/interfaces/Operation.d.ts index b543b503..59c91f10 100644 --- a/src/client/interfaces/Operation.d.ts +++ b/src/client/interfaces/Operation.d.ts @@ -4,9 +4,9 @@ import { OperationParameters } from './OperationParameters'; export interface Operation extends OperationParameters { service: string; name: string; - summary?: string; - description?: string; - deprecated?: boolean; + summary: string | null; + description: string | null; + deprecated: boolean; method: string; path: string; errors: OperationError[]; diff --git a/src/index.ts b/src/index.ts index c550fd7d..1ebbbe3d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -29,8 +29,8 @@ export enum HttpClient { * @param httpClient: The selected httpClient (fetch or XHR) */ export function generate(input: string, output: string, language: Language = Language.TYPESCRIPT, httpClient: HttpClient = HttpClient.FETCH): void { - const inputPath: string = path.resolve(process.cwd(), input); - const outputPath: string = path.resolve(process.cwd(), output); + const inputPath = path.resolve(process.cwd(), input); + const outputPath = path.resolve(process.cwd(), output); console.log(chalk.bold.green('Generate:')); console.log(chalk.grey(' Input:'), input); @@ -42,22 +42,22 @@ export function generate(input: string, output: string, language: Language = Lan try { // Load the specification, read the OpenAPI version and load the // handlebar templates for the given language - const openApi: any = getOpenApiSpec(inputPath); - const openApiVersion: OpenApiVersion = getOpenApiVersion(openApi); - const templates: Templates = readHandlebarsTemplates(language); + const openApi = getOpenApiSpec(inputPath); + const openApiVersion = getOpenApiVersion(openApi); + const templates = readHandlebarsTemplates(language); switch (language) { case Language.JAVASCRIPT: case Language.TYPESCRIPT: // Generate and write version 2 client if (openApiVersion === OpenApiVersion.V2) { - const clientV2: Client = parseV2(openApi); + const clientV2 = parseV2(openApi); writeClient(clientV2, language, templates, outputPath); } // Generate and write version 3 client if (openApiVersion === OpenApiVersion.V3) { - const clientV3: Client = parseV3(openApi); + const clientV3 = parseV3(openApi); writeClient(clientV3, language, templates, outputPath); } } diff --git a/src/openApi/v2/interfaces/OpenApiSchema.d.ts b/src/openApi/v2/interfaces/OpenApiSchema.d.ts index 57bfb7f4..1ced5937 100644 --- a/src/openApi/v2/interfaces/OpenApiSchema.d.ts +++ b/src/openApi/v2/interfaces/OpenApiSchema.d.ts @@ -7,6 +7,7 @@ import { OpenApiXml } from './OpenApiXml'; * https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#schemaObject */ export interface OpenApiSchema { + $ref?: string; format?: 'int32' | 'int64' | 'float' | 'double' | 'string' | 'boolean' | 'byte' | 'binary' | 'date' | 'date-time' | 'password'; title?: string; description?: string; diff --git a/src/openApi/v2/parser/getArrayType.ts b/src/openApi/v2/parser/getArrayType.ts index 2ef0b6a5..c7d2b813 100644 --- a/src/openApi/v2/parser/getArrayType.ts +++ b/src/openApi/v2/parser/getArrayType.ts @@ -1,6 +1,5 @@ import { getType } from './getType'; import { PrimaryType } from './constants'; -import { Type } from '../../../client/interfaces/Type'; import { OpenApiItems } from '../interfaces/OpenApiItems'; export interface ArrayType { @@ -22,7 +21,7 @@ export function getArrayType(items: OpenApiItems): ArrayType { // If the parameter has a type than it can be a basic or generic type. if (items.type) { - const itemsType: Type = getType(items.type); + const itemsType = getType(items.type); result.type = itemsType.type; result.base = itemsType.base; result.template = itemsType.template; @@ -31,7 +30,7 @@ export function getArrayType(items: OpenApiItems): ArrayType { // If the parameter is an Array type, we check for the child type, // so we can create a typed array, otherwise this will be a "any[]". if (items.type === 'array' && items.items) { - const arrayType: ArrayType = getArrayType(items.items); + const arrayType = getArrayType(items.items); result.type = `${arrayType.type}[]`; result.base = arrayType.base; result.template = arrayType.template; diff --git a/src/openApi/v2/parser/getEnum.ts b/src/openApi/v2/parser/getEnum.ts index 60a9ff89..7f0bda98 100644 --- a/src/openApi/v2/parser/getEnum.ts +++ b/src/openApi/v2/parser/getEnum.ts @@ -1,25 +1,12 @@ -import { ModelEnumValue } from '../../../client/interfaces/ModelEnumValue'; +import { Enum } from '../../../client/interfaces/Enum'; -export function getModelEnum(values?: (string | number)[]): ModelEnumValue[] { - if (Array.isArray(values)) { - return values - .filter((value: string | number, index: number, arr: (string | number)[]) => { - return arr.indexOf(value) === index; - }) - .map( - (value: string | number): ModelEnumValue => { - if (typeof value === 'number') { - return { - name: `NUM_${value}`, - value: String(value), - }; - } - return { - name: value.replace(/([a-z])([A-Z]+)/g, '$1_$2').toUpperCase(), - value: `'${value}'`, - }; - } - ); - } - return []; +export function getModelEnum(): Enum { + const prop: Enum = { + name: '', + type: '', + values: [], + validation: null, + }; + + return prop; } diff --git a/src/openApi/v2/parser/getEnumSymbols.ts b/src/openApi/v2/parser/getEnumSymbols.ts new file mode 100644 index 00000000..866ba84b --- /dev/null +++ b/src/openApi/v2/parser/getEnumSymbols.ts @@ -0,0 +1,23 @@ +import { EnumSymbol } from '../../../client/interfaces/EnumSymbol'; + +export function getEnumSymbols(values?: (string | number)[]): EnumSymbol[] { + if (Array.isArray(values)) { + return values + .filter((value, index, arr) => { + return arr.indexOf(value) === index; + }) + .map(value => { + if (typeof value === 'number') { + return { + name: `NUM_${value}`, + value: String(value), + }; + } + return { + name: value.replace(/([a-z])([A-Z]+)/g, '$1_$2').toUpperCase(), + value: `'${value}'`, + }; + }); + } + return []; +} diff --git a/src/openApi/v2/parser/getEnumFromDescription.ts b/src/openApi/v2/parser/getEnumSymbolsFromDescription.ts similarity index 52% rename from src/openApi/v2/parser/getEnumFromDescription.ts rename to src/openApi/v2/parser/getEnumSymbolsFromDescription.ts index f789404b..cc3e2b2a 100644 --- a/src/openApi/v2/parser/getEnumFromDescription.ts +++ b/src/openApi/v2/parser/getEnumSymbolsFromDescription.ts @@ -1,16 +1,16 @@ -import { ModelEnumValue } from '../../../client/interfaces/ModelEnumValue'; +import { EnumSymbol } from '../../../client/interfaces/EnumSymbol'; -export function getModelEnumFromDescription(description: string): ModelEnumValue[] { +export function getEnumSymbolsFromDescription(description: string): EnumSymbol[] { // Check if we can find this special format string: // None=0,Something=1,AnotherThing=2 if (/^(\w+=[0-9]+,?)+$/g.test(description)) { - const matches: RegExpMatchArray | null = description.match(/(\w+=[0-9]+,?)/g); + const matches = description.match(/(\w+=[0-9]+,?)/g); if (matches) { // Grab the values from the description - const symbols: ModelEnumValue[] = []; - matches.forEach((match: string): void => { - const name: string = match.split('=')[0]; - const value: number = parseInt(match.split('=')[1].replace(/[^0-9]/g, '')); + const symbols: EnumSymbol[] = []; + matches.forEach(match => { + const name = match.split('=')[0]; + const value = parseInt(match.split('=')[1].replace(/[^0-9]/g, '')); if (name && Number.isInteger(value)) { symbols.push({ name: name.replace(/([a-z])([A-Z]+)/g, '$1_$2').toUpperCase(), @@ -20,7 +20,7 @@ export function getModelEnumFromDescription(description: string): ModelEnumValue }); // Filter out any duplicate names - return symbols.filter((symbol: ModelEnumValue, index: number, arr: ModelEnumValue[]): boolean => { + return symbols.filter((symbol, index, arr) => { return arr.map(item => item.name).indexOf(symbol.name) === index; }); } diff --git a/src/openApi/v2/parser/getEnumType.ts b/src/openApi/v2/parser/getEnumType.ts index bbe8b462..2c7c5d90 100644 --- a/src/openApi/v2/parser/getEnumType.ts +++ b/src/openApi/v2/parser/getEnumType.ts @@ -1,15 +1,18 @@ -export function getEnumType(symbols: ModelSymbol[], addParentheses = false): string { +import { EnumSymbol } from '../../../client/interfaces/EnumSymbol'; +import { getEnumValues } from './getEnumValues'; + +export function getEnumType(symbols: EnumSymbol[], addParentheses: boolean = false): string { // Fetch values from the symbols, just to be sure we filter out // any double values and finally we sort them to make them easier // to read when we use them in our generated code. - const entries: string[] = getEnumValues(symbols); + const values = getEnumValues(symbols); // Add grouping parentheses if needed. This can be handy if enum values // are used in Arrays, so that you will get the following definition: // const myArray: ('EnumValue1' | 'EnumValue2' | 'EnumValue3')[]; - if (entries.length > 1 && addParentheses) { - return `(${entries.join(' | ')})`; + if (values.length > 1 && addParentheses) { + return `(${values.join(' | ')})`; } - return entries.join(' | '); + return values.join(' | '); } diff --git a/src/openApi/v2/parser/getEnumValues.ts b/src/openApi/v2/parser/getEnumValues.ts index da4aad13..6be78e01 100644 --- a/src/openApi/v2/parser/getEnumValues.ts +++ b/src/openApi/v2/parser/getEnumValues.ts @@ -1,11 +1,13 @@ -export function getEnumValues(symbols: ModelSymbol[]): string[] { +import { EnumSymbol } from '../../../client/interfaces/EnumSymbol'; + +export function getEnumValues(symbols: EnumSymbol[]): string[] { // Fetch values from the symbols, just to be sure we filter out // any double values and finally we sort them to make them easier // to read when we use them in our generated code. return symbols .map(symbol => symbol.value) - .filter((value: string, index: number, arr: string[]): boolean => { - return arr.indexOf(value) === index; + .filter((symbol, index, arr) => { + return arr.indexOf(symbol) === index; }) .sort(); } diff --git a/src/openApi/v2/parser/getMappedType.ts b/src/openApi/v2/parser/getMappedType.ts index 67bb6d92..ca11fa7e 100644 --- a/src/openApi/v2/parser/getMappedType.ts +++ b/src/openApi/v2/parser/getMappedType.ts @@ -4,7 +4,7 @@ import { PrimaryType, TYPE_MAPPINGS } from './constants'; * Get mapped type for given type to any basic Typescript/Javascript type. */ export function getMappedType(type: string): PrimaryType | string { - const mapped: string | undefined = TYPE_MAPPINGS.get(type.toLowerCase()); + const mapped = TYPE_MAPPINGS.get(type.toLowerCase()); if (mapped) { return mapped; } diff --git a/src/openApi/v2/parser/getModel.ts b/src/openApi/v2/parser/getModel.ts index caf7981c..1c53f0c2 100644 --- a/src/openApi/v2/parser/getModel.ts +++ b/src/openApi/v2/parser/getModel.ts @@ -2,158 +2,204 @@ import { OpenApi } from '../interfaces/OpenApi'; import { OpenApiSchema } from '../interfaces/OpenApiSchema'; import { getComment } from './getComment'; import { getType } from './getType'; -import { Type } from '../../../client/interfaces/Type'; -import { getEnumType } from './getEnumType'; -import { PrimaryType } from './constants'; -import { OpenApiReference } from '../interfaces/OpenApiReference'; -import { getRef } from './getRef'; -import { getEnumValues } from './getEnumValues'; import { Model } from '../../../client/interfaces/Model'; +import { getValidationForRef } from './getValidationForRef'; +import { getValidationForType } from './getValidationForType'; +import { getValidationForArrayRef } from './getValidationForArrayRef'; +import { getModelType } from './getModelType'; +import { getModelValidation } from './getModelValidation'; +import { getValidation } from './getValidation'; +import { PrimaryType } from './constants'; +import { getEnumType } from './getEnumType'; +import { getEnumSymbols } from './getEnumSymbols'; +import { getEnumValues } from './getEnumValues'; +import { getEnumSymbolsFromDescription } from './getEnumSymbolsFromDescription'; -export function getModel(openApi: OpenApi, schema: OpenApiSchema, name: string): Model { +export function getModel(openApi: OpenApi, definition: OpenApiSchema, definitionName: string = 'unknown'): Model { const result: Model = { - name, isInterface: false, isType: false, isEnum: false, + name: definitionName, type: 'any', base: 'any', template: null, validation: null, - description: getComment(schema.description), - extends: null, + description: getComment(definition.description), + extends: [], imports: [], enums: [], + symbols: [], properties: [], }; + if (definition.$ref) { + const definitionRef = getType(definition.$ref); + result.isType = true; + result.type = definitionRef.type; + result.base = definitionRef.base; + result.template = definitionRef.template; + result.validation = getValidationForRef(definitionRef); + result.imports.push(...definitionRef.imports); + return result; + } + // If the param is a enum then return the values as an inline type. - if (schema.enum) { - const enumSymbols: ModelSymbol[] = getEnumSymbols(schema.enum); + if (definition.enum) { + const enumSymbols = getEnumSymbols(definition.enum); if (enumSymbols.length) { result.isEnum = true; result.symbols = enumSymbols; result.type = getEnumType(enumSymbols); result.base = PrimaryType.STRING; - result.validation = `yup.mixed<${name}>().oneOf([${getEnumValues(enumSymbols).join(', ')}])`; + result.validation = `yup.mixed<${definitionName}>().oneOf([${getEnumValues(enumSymbols).join(', ')}])`; return result; } + return result; } // If the param is a enum then return the values as an inline type. - if (schema.type === 'int' && schema.description) { - const enumSymbols: ModelSymbol[] = getEnumSymbolsFromDescription(schema.description); + if (definition.type === 'int' && definition.description) { + const enumSymbols = getEnumSymbolsFromDescription(definition.description); if (enumSymbols.length) { result.isEnum = true; result.symbols = enumSymbols; result.type = getEnumType(enumSymbols); result.base = PrimaryType.NUMBER; - result.validation = `yup.mixed<${name}>().oneOf([${getEnumValues(enumSymbols).join(', ')}])`; + result.validation = `yup.mixed<${definitionName}>().oneOf([${getEnumValues(enumSymbols).join(', ')}])`; return result; } + return result; } // If the schema is an Array type, we check for the child type, // so we can create a typed array, otherwise this will be a "any[]". - if (schema.type === 'array' && schema.items) { - if (schema.items.$ref) { - const arrayType: Type = getType(schema.items.$ref); - result.imports.push(...arrayType.imports); + if (definition.type === 'array' && definition.items) { + if (definition.items.$ref) { + const arrayItemsRef = getType(definition.items.$ref); + result.imports.push(...arrayItemsRef.imports); result.isType = true; - result.type = `${arrayType.type}[]`; - result.base = arrayType.base; - result.template = arrayType.template; - result.validation = `yup.array<${result.name}>().of(${result.base}.schema)`; // TODO: Simple strings! - result.imports.push(...arrayType.imports); + result.type = `${arrayItemsRef.type}[]`; + result.base = arrayItemsRef.base; + result.template = arrayItemsRef.template; + result.validation = getValidationForArrayRef(arrayItemsRef); + result.imports.push(...arrayItemsRef.imports); } else { - const array: Schema = getSchema(openApi, schema.items, 'unkown'); - const arrayType: string = getSchemaType(array); + const arrayItemsModel = getModel(openApi, definition.items); result.isType = true; - result.type = `${arrayType}[]`; - result.base = arrayType; - result.template = null; - result.validation = `yup.array<${result.name}>().of(${result.base}.schema)`; // TODO: Simple strings! - result.imports.push(...array.imports); + result.type = `${arrayItemsModel.type}[]`; + result.base = arrayItemsModel.base; + result.template = arrayItemsModel.template; + // result.validation = getValidationForArray(array.validation || 'any'); + result.imports.push(...arrayItemsModel.imports); } return result; } - /* // Check if this model extends other models - if (schema.allOf) { - schema.allOf.forEach((parent: OpenApiSchema & OpenApiReference): void => { - const parentSchema: SchemaReference = getSchemaReference(openApi, parent); - result.extends.push(parentSchema.type); - result.imports.push(parentSchema.base); - - // Merge properties of other models - if (parent.properties) { - for (const propertyName in schema.properties) { - if (schema.properties.hasOwnProperty(propertyName)) { - const propertyRef: OpenApiSchema & OpenApiReference = schema.properties[propertyName]; - const propertyRequired: boolean = (schema.required && schema.required.includes(propertyName)) || false; - const property: SchemaProperty = getSchemaProperty(openApi, propertyRef, propertyName, propertyRequired); - result.imports.push(...property.imports); - result.properties.set(propertyName, property); + if (definition.allOf) { + definition.allOf.forEach(parent => { + if (parent.$ref) { + const parentRef = getType(parent.$ref); + result.extends.push(parentRef.type); + result.imports.push(parentRef.base); + } + if (parent.type === 'object' && parent.properties) { + for (const propertyName in parent.properties) { + if (parent.properties.hasOwnProperty(propertyName)) { + const property = parent.properties[propertyName]; + const propertyRequired = !!(parent.required && parent.required.includes(propertyName)); + const propertyReadOnly = !!property.readOnly; + if (property.$ref) { + const propertyRef = getType(property.$ref); + result.imports.push(...propertyRef.imports); + result.properties.push({ + name: propertyName, + type: propertyRef.type, + required: propertyRequired, + nullable: false, + readOnly: propertyReadOnly, + description: property.description || null, + validation: getValidationForRef(propertyRef, propertyRequired), + }); + } else { + const propertyModel = getModel(openApi, property); + result.imports.push(...propertyModel.imports); + result.properties.push({ + name: propertyName, + type: propertyModel.type, + required: propertyRequired, + nullable: false, + readOnly: propertyReadOnly, + description: property.description || null, + validation: propertyModel.validation ? getValidation(propertyModel.validation, propertyRequired) : null, + }); + } } } } }); - } - */ - if (schema.type === 'object' && schema.properties) { + // Validation needs to also check extended schema! + // Check ModelThatExtends.ts result.isInterface = true; - result.type = 'interface'; - result.base = 'interface'; + result.type = getModelType(result.properties); + result.validation = getModelValidation(definitionName, result.properties); + result.base = PrimaryType.OBJECT; result.template = null; + } - for (const propertyName in schema.properties) { - if (schema.properties.hasOwnProperty(propertyName)) { - const propertyRequired: boolean = (schema.required && schema.required.includes(propertyName)) || false; - const propertyRef: OpenApiSchema & OpenApiReference = schema.properties[propertyName]; - - if (propertyRef.$ref) { - const propertyType: Type = getType(propertyRef.$ref); - result.imports.push(...propertyType.imports); + if (definition.type === 'object' && definition.properties) { + for (const propertyName in definition.properties) { + if (definition.properties.hasOwnProperty(propertyName)) { + const property = definition.properties[propertyName]; + const propertyRequired = !!(definition.required && definition.required.includes(propertyName)); + const propertyReadOnly = !!property.readOnly; + if (property.$ref) { + const propertyRef = getType(property.$ref); + result.imports.push(...propertyRef.imports); result.properties.push({ name: propertyName, - type: propertyType.type, + type: propertyRef.type, required: propertyRequired, nullable: false, - readOnly: false, + readOnly: propertyReadOnly, + description: property.description || null, + validation: getValidationForRef(propertyRef, propertyRequired), }); } else { - const property: OpenApiSchema = getRef(openApi, propertyRef); - const propertySchema: Schema = getSchema(openApi, property, propertyName); - const propertyType: string = getSchemaType(propertySchema); - result.imports.push(...propertySchema.imports); + const propertyModel = getModel(openApi, property); + result.imports.push(...propertyModel.imports); result.properties.push({ name: propertyName, - type: propertyType, + type: propertyModel.type, required: propertyRequired, nullable: false, - readOnly: property.readOnly || false, - description: property.description, + readOnly: propertyReadOnly, + description: property.description || null, + validation: propertyModel.validation ? getValidation(propertyModel.validation, propertyRequired) : null, }); - - // TODO: This also needs a validation logic, maybe we can store that - // per schema and have them 'concatenate' on demand?? } } } + result.isInterface = true; + result.type = getModelType(result.properties); + result.validation = getModelValidation(definitionName, result.properties); + result.base = PrimaryType.OBJECT; + result.template = null; return result; } // If the schema has a type than it can be a basic or generic type. - if (schema.type) { - const schemaType: Type = getType(schema.type); + if (definition.type !== 'object' && definition.type) { + const definitionType = getType(definition.type); result.isType = true; - result.type = schemaType.type; - result.base = schemaType.base; - result.template = schemaType.template; - result.imports.push(...schemaType.imports); + result.type = definitionType.type; + result.base = definitionType.base; + result.template = definitionType.template; + result.validation = getValidationForType(definitionType); + result.imports.push(...definitionType.imports); return result; } diff --git a/src/openApi/v2/parser/getModelTemplate.spec.ts b/src/openApi/v2/parser/getModelTemplate.spec.ts index 4f7cd572..67e46f54 100644 --- a/src/openApi/v2/parser/getModelTemplate.spec.ts +++ b/src/openApi/v2/parser/getModelTemplate.spec.ts @@ -2,7 +2,7 @@ import { getModelTemplate } from './getModelTemplate'; describe('getModelTemplate', () => { it('should return generic for template type', () => { - const template: string = getModelTemplate({ + const template = getModelTemplate({ type: 'Link', base: 'Link', template: 'Model', @@ -12,7 +12,7 @@ describe('getModelTemplate', () => { }); it('should return empty for primary type', () => { - const template: string = getModelTemplate({ + const template = getModelTemplate({ type: 'string', base: 'string', template: null, diff --git a/src/openApi/v2/parser/getModelType.ts b/src/openApi/v2/parser/getModelType.ts index 575e42b9..7e7f3de6 100644 --- a/src/openApi/v2/parser/getModelType.ts +++ b/src/openApi/v2/parser/getModelType.ts @@ -1,17 +1,12 @@ -import { Model } from '../../../client/interfaces/Model'; +import { EOL } from 'os'; +import { ModelProperty } from '../../../client/interfaces/ModelProperty'; -// string -// array[test] -// array[{ -// foo: string -// bar: string -// }] -export function getModelType(model: Model): string { - // if (schema.properties) { - // return schema.type - // } - if (model.type) { - return model.type; - } - return 'any'; +export function getModelType(properties: ModelProperty[]): string { + return [ + `{`, + ...properties.map(property => { + return ` ${property.readOnly ? 'readonly ' : ''}${property.name}${property.required ? '' : '?'}: ${property.type},`; + }), + `}`, + ].join(EOL); } diff --git a/src/openApi/v2/parser/getModelValidation.ts b/src/openApi/v2/parser/getModelValidation.ts new file mode 100644 index 00000000..1d652c4f --- /dev/null +++ b/src/openApi/v2/parser/getModelValidation.ts @@ -0,0 +1,12 @@ +import { EOL } from 'os'; +import { ModelProperty } from '../../../client/interfaces/ModelProperty'; + +export function getModelValidation(name: string, properties: ModelProperty[]): string { + return [ + `yup.object().shape({`, + ...properties.map(property => { + return ` ${property.name}: ${property.validation},`; + }), + `}).noUnknown()`, + ].join(EOL); +} diff --git a/src/openApi/v2/parser/getModels.ts b/src/openApi/v2/parser/getModels.ts index 56f5faf6..7d24d914 100644 --- a/src/openApi/v2/parser/getModels.ts +++ b/src/openApi/v2/parser/getModels.ts @@ -1,14 +1,13 @@ import { Model } from '../../../client/interfaces/Model'; import { OpenApi } from '../interfaces/OpenApi'; -import { OpenApiSchema } from '../interfaces/OpenApiSchema'; import { getModel } from './getModel'; export function getModels(openApi: OpenApi): Model[] { const models: Model[] = []; for (const definitionName in openApi.definitions) { if (openApi.definitions.hasOwnProperty(definitionName)) { - const definition: OpenApiSchema = openApi.definitions[definitionName]; - const definitionModel: Model = getModel(openApi, definition, definitionName); + const definition = openApi.definitions[definitionName]; + const definitionModel = getModel(openApi, definition, definitionName); models.push(definitionModel); } } diff --git a/src/openApi/v2/parser/getOperation.ts b/src/openApi/v2/parser/getOperation.ts index a0a498c7..ec41b6d7 100644 --- a/src/openApi/v2/parser/getOperation.ts +++ b/src/openApi/v2/parser/getOperation.ts @@ -9,13 +9,14 @@ import { getComment } from './getComment'; import { getOperationResponses } from './getOperationResponses'; import { getOperationResponse } from './getOperationResponse'; import { getOperationErrors } from './getOperationErrors'; +import { Operation } from '../../../client/interfaces/Operation'; export function getOperation(openApi: OpenApi, url: string, method: string, op: OpenApiOperation): Operation { - const serviceName: string = (op.tags && op.tags[0]) || 'Service'; - const serviceClassName: string = getServiceClassName(serviceName); - const operationNameFallback: string = `${method}${serviceClassName}`; - const operationName: string = getOperationName(op.operationId || operationNameFallback); - const operationPath: string = getOperationPath(url); + const serviceName = (op.tags && op.tags[0]) || 'Service'; + const serviceClassName = getServiceClassName(serviceName); + const operationNameFallback = `${method}${serviceClassName}`; + const operationName = getOperationName(op.operationId || operationNameFallback); + const operationPath = getOperationPath(url); // Create a new operation object for this method. const result: Operation = { @@ -23,7 +24,7 @@ export function getOperation(openApi: OpenApi, url: string, method: string, op: name: operationName, summary: getComment(op.summary), description: getComment(op.description), - deprecated: op.deprecated, + deprecated: op.deprecated || false, method: method, path: operationPath, parameters: [], @@ -39,7 +40,7 @@ export function getOperation(openApi: OpenApi, url: string, method: string, op: // Parse the operation parameters (path, query, body, etc). if (op.parameters) { - const parameters: OperationParameters = getOperationParameters(openApi, op.parameters); + const parameters = getOperationParameters(openApi, op.parameters); result.imports.push(...parameters.imports); result.parameters.push(...parameters.parameters); result.parametersPath.push(...parameters.parametersPath); @@ -51,9 +52,9 @@ export function getOperation(openApi: OpenApi, url: string, method: string, op: // Parse the operation responses. if (op.responses) { - const responses: OperationResponse[] = getOperationResponses(openApi, op.responses); - const response: OperationResponse = getOperationResponse(responses); - const errors: OperationError[] = getOperationErrors(responses); + const responses = getOperationResponses(openApi, op.responses); + const response = getOperationResponse(responses); + const errors = getOperationErrors(responses); result.imports.push(...response.imports); result.errors = errors; result.result = response.type; diff --git a/src/openApi/v2/parser/getOperationErrors.ts b/src/openApi/v2/parser/getOperationErrors.ts index 9029d310..b4152696 100644 --- a/src/openApi/v2/parser/getOperationErrors.ts +++ b/src/openApi/v2/parser/getOperationErrors.ts @@ -1,12 +1,13 @@ +import { OperationResponse } from '../../../client/interfaces/OperationResponse'; +import { OperationError } from '../../../client/interfaces/OperationError'; + export function getOperationErrors(responses: OperationResponse[]): OperationError[] { return responses - .filter((response: OperationResponse): boolean => { + .filter(response => { return response.code >= 300 && response.text !== undefined && response.text !== ''; }) - .map( - (response: OperationResponse): OperationError => ({ - code: response.code, - text: response.text, - }) - ); + .map(response => ({ + code: response.code, + text: response.text, + })); } diff --git a/src/openApi/v2/parser/getOperationName.ts b/src/openApi/v2/parser/getOperationName.ts index ed46b315..83a9abcb 100644 --- a/src/openApi/v2/parser/getOperationName.ts +++ b/src/openApi/v2/parser/getOperationName.ts @@ -6,6 +6,6 @@ import camelCase from 'camelcase'; * the most popular Javascript and Typescript writing style. */ export function getOperationName(value: string): string { - const clean: string = value.replace(/[^\w\s\-]+/g, '_').trim(); + const clean = value.replace(/[^\w\s\-]+/g, '_').trim(); return camelCase(clean); } diff --git a/src/openApi/v2/parser/getOperationParameter.ts b/src/openApi/v2/parser/getOperationParameter.ts index 644a7294..c8b6d14e 100644 --- a/src/openApi/v2/parser/getOperationParameter.ts +++ b/src/openApi/v2/parser/getOperationParameter.ts @@ -1,15 +1,15 @@ import { OpenApiParameter } from '../interfaces/OpenApiParameter'; import { getType } from './getType'; -import { Type } from '../../../client/interfaces/Type'; import { OpenApi } from '../interfaces/OpenApi'; -import { getParameterName } from './getParameterName'; import { getComment } from './getComment'; +import { getOperationParameterName } from './getOperationParameterName'; +import { OperationParameter } from '../../../client/interfaces/OperationParameter'; -export function getParameter(openApi: OpenApi, parameter: OpenApiParameter): Parameter { - const result: Parameter = { +export function getOperationParameter(openApi: OpenApi, parameter: OpenApiParameter): OperationParameter { + const result: OperationParameter = { in: parameter.in, prop: parameter.name, - name: getParameterName(parameter.name), + name: getOperationParameterName(parameter.name), type: 'any', base: 'any', template: null, @@ -22,7 +22,7 @@ export function getParameter(openApi: OpenApi, parameter: OpenApiParameter): Par // If the parameter has a type than it can be a basic or generic type. if (parameter.type) { - const parameterType: Type = getType(parameter.type); + const parameterType = getType(parameter.type); result.type = parameterType.type; result.base = parameterType.base; result.template = parameterType.template; diff --git a/src/openApi/v2/parser/getOperationParameterName.ts b/src/openApi/v2/parser/getOperationParameterName.ts index b911c644..54d80d9f 100644 --- a/src/openApi/v2/parser/getOperationParameterName.ts +++ b/src/openApi/v2/parser/getOperationParameterName.ts @@ -4,7 +4,7 @@ import camelCase from 'camelcase'; * Replaces any invalid characters from a parameter name. * For example: 'filter.someProperty' becomes 'filterSomeProperty'. */ -export function getParameterName(value: string): string { - const clean: string = value.replace(/[^\w\s\-]+/g, '_').trim(); +export function getOperationParameterName(value: string): string { + const clean = value.replace(/[^\w\s\-]+/g, '_').trim(); return camelCase(clean); } diff --git a/src/openApi/v2/parser/getOperationParameters.ts b/src/openApi/v2/parser/getOperationParameters.ts index 8d4eee74..f9a322ab 100644 --- a/src/openApi/v2/parser/getOperationParameters.ts +++ b/src/openApi/v2/parser/getOperationParameters.ts @@ -1,10 +1,12 @@ import { OpenApiParameter } from '../interfaces/OpenApiParameter'; import { OpenApiReference } from '../interfaces/OpenApiReference'; -import { getParameter } from './getParameter'; import { OpenApi } from '../interfaces/OpenApi'; import { getRef } from './getRef'; +import { OperationParameters } from '../../../client/interfaces/OperationParameters'; +import { OperationParameter } from '../../../client/interfaces/OperationParameter'; +import { getOperationParameter } from './getOperationParameter'; -function sortByRequired(a: Parameter, b: Parameter): number { +function sortByRequired(a: OperationParameter, b: OperationParameter): number { return a.required && !b.required ? -1 : !a.required && b.required ? 1 : 0; } @@ -21,8 +23,8 @@ export function getOperationParameters(openApi: OpenApi, parameters: (OpenApiPar // Iterate over the parameters parameters.forEach(parameter => { - const paramRef: OpenApiParameter = getRef(openApi, parameter); - const param: Parameter = getParameter(openApi, paramRef); + const paramRef = getRef(openApi, parameter); + const param = getOperationParameter(openApi, paramRef); // We ignore the "api-version" param, since we do not want to add this // as the first / default parameter for each of the service calls. diff --git a/src/openApi/v2/parser/getOperationResponse.ts b/src/openApi/v2/parser/getOperationResponse.ts index 8c1133d2..f06122e4 100644 --- a/src/openApi/v2/parser/getOperationResponse.ts +++ b/src/openApi/v2/parser/getOperationResponse.ts @@ -1,4 +1,5 @@ import { PrimaryType } from './constants'; +import { OperationResponse } from '../../../client/interfaces/OperationResponse'; export function getOperationResponse(responses: OperationResponse[]): OperationResponse { const response: OperationResponse = { @@ -11,7 +12,7 @@ export function getOperationResponse(responses: OperationResponse[]): OperationR }; // Fetch the first valid (2XX range) response code and return that type. - const result: OperationResponse | undefined = responses.find(response => response.code && response.code >= 200 && response.code < 300); + const result = responses.find(response => response.code && response.code >= 200 && response.code < 300); if (result) { response.code = result.code; response.text = result.text; diff --git a/src/openApi/v2/parser/getOperationResponseCode.ts b/src/openApi/v2/parser/getOperationResponseCode.ts index 649229a8..080cdc02 100644 --- a/src/openApi/v2/parser/getOperationResponseCode.ts +++ b/src/openApi/v2/parser/getOperationResponseCode.ts @@ -6,7 +6,7 @@ export function getOperationResponseCode(value: string | 'default'): number | nu // Check if we can parse the code and return of successful. if (/[0-9]+/g.test(value)) { - const code: number = parseInt(value); + const code = parseInt(value); if (Number.isInteger(code)) { return code; } diff --git a/src/openApi/v2/parser/getOperationResponses.ts b/src/openApi/v2/parser/getOperationResponses.ts index 8613f018..4e2b3919 100644 --- a/src/openApi/v2/parser/getOperationResponses.ts +++ b/src/openApi/v2/parser/getOperationResponses.ts @@ -1,9 +1,9 @@ import { OpenApiResponses } from '../interfaces/OpenApiResponses'; import { getOperationResponseCode } from './getOperationResponseCode'; import { OpenApiResponse } from '../interfaces/OpenApiResponse'; -import { OpenApiReference } from '../interfaces/OpenApiReference'; import { getRef } from './getRef'; import { OpenApi } from '../interfaces/OpenApi'; +import { OperationResponse } from '../../../client/interfaces/OperationResponse'; export function getOperationResponses(openApi: OpenApi, responses: OpenApiResponses): OperationResponse[] { const results: OperationResponse[] = []; @@ -12,9 +12,9 @@ export function getOperationResponses(openApi: OpenApi, responses: OpenApiRespon // status code and response message (if any). for (const code in responses) { if (responses.hasOwnProperty(code)) { - const responseOrReference: OpenApiResponse & OpenApiReference = responses[code]; - const response: OpenApiResponse = getRef(openApi, responseOrReference); - const responseCode: number | null = getOperationResponseCode(code); + const responseOrReference = responses[code]; + const response = getRef(openApi, responseOrReference); + const responseCode = getOperationResponseCode(code); // If there is a response code then we check what data we get back, // if there is no typed data, we just return so the user is still diff --git a/src/openApi/v2/parser/getRef.ts b/src/openApi/v2/parser/getRef.ts index fa624691..303382ba 100644 --- a/src/openApi/v2/parser/getRef.ts +++ b/src/openApi/v2/parser/getRef.ts @@ -5,7 +5,7 @@ export function getRef(openApi: OpenApi, item: T & OpenApiReference): T { if (item.$ref) { // Fetch the paths to the definitions, this converts: // "#/definitions/Form" to ["definitions", "Form"] - const paths: string[] = item.$ref + const paths = item.$ref .replace(/^#/g, '') .split('/') .filter(item => item); diff --git a/src/openApi/v2/parser/getServer.ts b/src/openApi/v2/parser/getServer.ts index ab229940..6037e0c5 100644 --- a/src/openApi/v2/parser/getServer.ts +++ b/src/openApi/v2/parser/getServer.ts @@ -5,8 +5,8 @@ import { OpenApi } from '../interfaces/OpenApi'; * @param openApi */ export function getServer(openApi: OpenApi): string { - const scheme: string = (openApi.schemes && openApi.schemes[0]) || 'http'; - const host: string | undefined = openApi.host; - const basePath: string = openApi.basePath || ''; + const scheme = (openApi.schemes && openApi.schemes[0]) || 'http'; + const host = openApi.host; + const basePath = openApi.basePath || ''; return host ? `${scheme}://${host}${basePath}` : basePath; } diff --git a/src/openApi/v2/parser/getServiceClassName.ts b/src/openApi/v2/parser/getServiceClassName.ts index 57dcae30..94131a3d 100644 --- a/src/openApi/v2/parser/getServiceClassName.ts +++ b/src/openApi/v2/parser/getServiceClassName.ts @@ -5,8 +5,8 @@ import camelCase from 'camelcase'; * the input string to PascalCase and appends the "Service" prefix if needed. */ export function getServiceClassName(value: string): string { - const clean: string = value.replace(/[^\w\s\-]+/g, '_').trim(); - const name: string = camelCase(clean, { pascalCase: true }); + const clean = value.replace(/[^\w\s\-]+/g, '_').trim(); + const name = camelCase(clean, { pascalCase: true }); if (name && !name.endsWith('Service')) { return `${name}Service`; } diff --git a/src/openApi/v2/parser/getServiceVersion.ts b/src/openApi/v2/parser/getServiceVersion.ts index 34777afc..9986f1af 100644 --- a/src/openApi/v2/parser/getServiceVersion.ts +++ b/src/openApi/v2/parser/getServiceVersion.ts @@ -3,6 +3,6 @@ * This basically removes any "v" prefix from the version string. * @param version */ -export function getServiceVersion(version: string = '1.0'): string { +export function getServiceVersion(version = '1.0'): string { return version.replace(/^v/gi, ''); } diff --git a/src/openApi/v2/parser/getServices.ts b/src/openApi/v2/parser/getServices.ts index bc676b68..30cb3e53 100644 --- a/src/openApi/v2/parser/getServices.ts +++ b/src/openApi/v2/parser/getServices.ts @@ -1,16 +1,15 @@ import { Service } from '../../../client/interfaces/Service'; import { OpenApi } from '../interfaces/OpenApi'; -import { OpenApiPath } from '../interfaces/OpenApiPath'; import { Method } from './constants'; /** * Get the OpenAPI services */ export function getServices(openApi: OpenApi): Service[] { - const services: Map = new Map(); + const services = new Map(); for (const url in openApi.paths) { if (openApi.paths.hasOwnProperty(url)) { - const path: OpenApiPath = openApi.paths[url]; + const path = openApi.paths[url]; for (const method in path) { if (path.hasOwnProperty(method)) { switch (method) { diff --git a/src/openApi/v2/parser/getType.ts b/src/openApi/v2/parser/getType.ts index c1c39175..fd77a13b 100644 --- a/src/openApi/v2/parser/getType.ts +++ b/src/openApi/v2/parser/getType.ts @@ -17,15 +17,15 @@ export function getType(value: string | undefined, template: string | null = nul }; // Remove definitions prefix and cleanup string. - const valueClean: string = stripNamespace(value || ''); + const valueClean = stripNamespace(value || ''); // Check of we have an Array type or generic type, for instance: "Link[Model]". if (/\[.*\]$/g.test(valueClean)) { - const matches: RegExpMatchArray | null = valueClean.match(/(.*?)\[(.*)\]$/); + const matches = valueClean.match(/(.*?)\[(.*)\]$/); if (matches && matches.length) { // Both of the types can be complex types so parse each of them. - const match1: Type = getType(matches[1]); - const match2: Type = getType(matches[2]); + const match1 = getType(matches[1]); + const match2 = getType(matches[2]); // If the first match is a generic array then construct a correct array type, // for example the type "Array[Model]" becomes "Model[]". @@ -50,7 +50,7 @@ export function getType(value: string | undefined, template: string | null = nul result.imports.push(...match2.imports); } } else if (hasMappedType(valueClean)) { - const mapped: string = getMappedType(valueClean); + const mapped = getMappedType(valueClean); result.type = mapped; result.base = mapped; } else if (valueClean) { diff --git a/src/openApi/v2/parser/getValidation.ts b/src/openApi/v2/parser/getValidation.ts new file mode 100644 index 00000000..d2c10bbf --- /dev/null +++ b/src/openApi/v2/parser/getValidation.ts @@ -0,0 +1,11 @@ +export function getValidation(validation: string, required: boolean = false, nullable: boolean = false): string { + if (required) { + validation = `${validation}.required()`; + } + + if (nullable) { + validation = `${validation}.nullable()`; + } + + return validation; +} diff --git a/src/openApi/v2/parser/getValidationForArrayRef.ts b/src/openApi/v2/parser/getValidationForArrayRef.ts new file mode 100644 index 00000000..16f1bc57 --- /dev/null +++ b/src/openApi/v2/parser/getValidationForArrayRef.ts @@ -0,0 +1,15 @@ +import { Type } from '../../../client/interfaces/Type'; + +export function getValidationForArrayRef(ref: Type, required: boolean = false, nullable: boolean = false): string { + let validation = `yup.array<${ref.type}>().of(${ref.base}.schema)`; + + if (required) { + validation = `${validation}.required()`; + } + + if (nullable) { + validation = `${validation}.nullable()`; + } + + return validation; +} diff --git a/src/openApi/v2/parser/getValidationForArrayType.ts b/src/openApi/v2/parser/getValidationForArrayType.ts new file mode 100644 index 00000000..98b6b99d --- /dev/null +++ b/src/openApi/v2/parser/getValidationForArrayType.ts @@ -0,0 +1,28 @@ +import { PrimaryType } from './constants'; +import { Type } from '../../../client/interfaces/Type'; + +export function getValidationForArrayType(type: Type, required: boolean = false, nullable: boolean = false): string { + let validation = `yup.array().of(yup.mixed())`; + + switch (type.type) { + case PrimaryType.BOOLEAN: + validation = `yup.array().of(yup.boolean())`; + break; + case PrimaryType.NUMBER: + validation = `yup.array().of(yup.number())`; + break; + case PrimaryType.STRING: + validation = `yup.array().of(yup.string())`; + break; + } + + if (required) { + validation = `${validation}.required()`; + } + + if (nullable) { + validation = `${validation}.nullable()`; + } + + return validation; +} diff --git a/src/openApi/v2/parser/getValidationForRef.ts b/src/openApi/v2/parser/getValidationForRef.ts new file mode 100644 index 00000000..d09700c5 --- /dev/null +++ b/src/openApi/v2/parser/getValidationForRef.ts @@ -0,0 +1,15 @@ +import { Type } from '../../../client/interfaces/Type'; + +export function getValidationForRef(ref: Type, required = false, nullable = false): string { + let validation = `${ref.base}.schema`; + + if (required) { + validation = `${validation}.required()`; + } + + if (nullable) { + validation = `${validation}.nullable()`; + } + + return validation; +} diff --git a/src/openApi/v2/parser/getValidationForType.ts b/src/openApi/v2/parser/getValidationForType.ts new file mode 100644 index 00000000..00a6ed09 --- /dev/null +++ b/src/openApi/v2/parser/getValidationForType.ts @@ -0,0 +1,28 @@ +import { PrimaryType } from './constants'; +import { Type } from '../../../client/interfaces/Type'; + +export function getValidationForType(type: Type, required: boolean = false, nullable: boolean = false): string { + let validation = `yup.mixed<${type.type}>()`; + + switch (type.type) { + case PrimaryType.BOOLEAN: + validation = `yup.boolean()`; + break; + case PrimaryType.NUMBER: + validation = `yup.number()`; + break; + case PrimaryType.STRING: + validation = `yup.string()`; + break; + } + + if (required) { + validation = `${validation}.required()`; + } + + if (nullable) { + validation = `${validation}.nullable()`; + } + + return validation; +} diff --git a/src/openApi/v2/parser/isPrimaryType.spec.ts b/src/openApi/v2/parser/isPrimaryType.spec.ts index ed3e1774..90edc5bf 100644 --- a/src/openApi/v2/parser/isPrimaryType.spec.ts +++ b/src/openApi/v2/parser/isPrimaryType.spec.ts @@ -6,6 +6,7 @@ describe('isPrimaryType', () => { expect(isPrimaryType('boolean')).toBeTruthy(); expect(isPrimaryType('string')).toBeTruthy(); expect(isPrimaryType('any')).toBeTruthy(); + expect(isPrimaryType('any[]')).toBeTruthy(); expect(isPrimaryType('void')).toBeTruthy(); expect(isPrimaryType('null')).toBeTruthy(); expect(isPrimaryType('Array')).toBeFalsy(); diff --git a/src/openApi/v2/parser/isPrimaryType.ts b/src/openApi/v2/parser/isPrimaryType.ts index dfb9e778..6bba755c 100644 --- a/src/openApi/v2/parser/isPrimaryType.ts +++ b/src/openApi/v2/parser/isPrimaryType.ts @@ -7,6 +7,7 @@ import { PrimaryType } from './constants'; export function isPrimaryType(type: string): type is PrimaryType { switch (type.toLowerCase()) { case PrimaryType.FILE: + case PrimaryType.ARRAY: case PrimaryType.OBJECT: case PrimaryType.BOOLEAN: case PrimaryType.NUMBER: diff --git a/src/openApi/v3/parser/getServer.ts b/src/openApi/v3/parser/getServer.ts index 8e09f536..aa0180b6 100644 --- a/src/openApi/v3/parser/getServer.ts +++ b/src/openApi/v3/parser/getServer.ts @@ -1,12 +1,9 @@ import { OpenApi } from '../interfaces/OpenApi'; -import { OpenApiServer } from '../interfaces/OpenApiServer'; -import { Dictionary } from '../../../utils/types'; -import { OpenApiServerVariable } from '../interfaces/OpenApiServerVariable'; export function getServer(openApi: OpenApi): string { - const server: OpenApiServer | undefined = openApi.servers && openApi.servers[0]; - const variables: Dictionary = (server && server.variables) || {}; - let url: string = (server && server.url) || ''; + const server = openApi.servers && openApi.servers[0]; + const variables = (server && server.variables) || {}; + let url = (server && server.url) || ''; for (const variable in variables) { if (variables.hasOwnProperty(variable)) { url = url.replace(`{${variable}}`, variables[variable].default); diff --git a/src/templates/typescript/core/getFormData.ts b/src/templates/typescript/core/getFormData.ts index df4fffd0..ceaecb57 100644 --- a/src/templates/typescript/core/getFormData.ts +++ b/src/templates/typescript/core/getFormData.ts @@ -9,7 +9,7 @@ * @param params Key value based object. */ export function getFormData(params: { [key: string]: any }): FormData { - const formData: FormData = new FormData(); + const formData = new FormData(); for (const key in params) { if (typeof params[key] !== 'undefined') { const value: any = params[key]; diff --git a/src/templates/typescript/core/request.ts b/src/templates/typescript/core/request.ts index 83fc5101..84ce4eb8 100644 --- a/src/templates/typescript/core/request.ts +++ b/src/templates/typescript/core/request.ts @@ -17,10 +17,10 @@ import { Result } from './Result'; */ export async function request(options: Readonly): Promise> { // Create the request URL - let url: string = `${OpenAPI.BASE}${options.path}`; + let url = `${OpenAPI.BASE}${options.path}`; // Create request headers - const headers: Headers = new Headers({ + const headers = new Headers({ ...options.headers, Accept: 'application/json', }); diff --git a/src/templates/typescript/core/requestUsingFetch.ts b/src/templates/typescript/core/requestUsingFetch.ts index 4ec4050b..ef3a1c5a 100644 --- a/src/templates/typescript/core/requestUsingFetch.ts +++ b/src/templates/typescript/core/requestUsingFetch.ts @@ -14,7 +14,7 @@ import { Result } from './Result'; */ export async function requestUsingFetch(url: string, request: Readonly): Promise> { // Fetch response using fetch API. - const response: Response = await fetch(url, request); + const response = await fetch(url, request); // Create result object. const result: Result = { @@ -28,7 +28,7 @@ export async function requestUsingFetch(url: string, request: Readonly< // Try to parse the content for any response status code. // We check the "Content-Type" header to see if we need to parse the // content as json or as plain text. - const contentType: string | null = response.headers.get('Content-Type'); + const contentType = response.headers.get('Content-Type'); if (contentType) { switch (contentType.toLowerCase()) { case 'application/json': diff --git a/src/templates/typescript/core/requestUsingXHR.ts b/src/templates/typescript/core/requestUsingXHR.ts index a3774493..c7d74681 100644 --- a/src/templates/typescript/core/requestUsingXHR.ts +++ b/src/templates/typescript/core/requestUsingXHR.ts @@ -16,14 +16,14 @@ import { isSuccess } from './isSuccess'; */ export async function requestUsingXHR(url: string, request: Readonly): Promise> { return new Promise(resolve => { - const xhr: XMLHttpRequest = new XMLHttpRequest(); + const xhr = new XMLHttpRequest(); // Open the request, remember to do this before adding any headers, // because the request needs to be initialized! xhr.open(request.method!, url, true); // Add the headers (required when dealing with JSON) - const headers: Headers = request.headers as Headers; + const headers = request.headers as Headers; headers.forEach((value: string, key: string): void => { xhr.setRequestHeader(key, value); }); @@ -43,7 +43,7 @@ export async function requestUsingXHR(url: string, request: Readonly { @@ -83,6 +84,7 @@ export enum {{{name}}} { {{#if validation}} export namespace {{{name}}} { + export const schema = {{{validation}}}; export function validate(value: any): Promise<{{{name}}}> { diff --git a/src/templates/typescript/service.hbs b/src/templates/typescript/service.hbs index cafd7338..adcc567e 100644 --- a/src/templates/typescript/service.hbs +++ b/src/templates/typescript/service.hbs @@ -44,7 +44,7 @@ export class {{{name}}} { {{/each}} {{/if}} - const result: Result = await request({ + const result = await request({ method: '{{{method}}}', path: `{{{path}}}`,{{#if parametersHeader}} headers: { diff --git a/src/utils/cleanupModels.ts b/src/utils/cleanupModels.ts index 8289a788..f1c39d0a 100644 --- a/src/utils/cleanupModels.ts +++ b/src/utils/cleanupModels.ts @@ -2,8 +2,19 @@ import { getSortedImports } from './getSortedImports'; import { Model } from '../client/interfaces/Model'; export function cleanupModels(models: Model[]): Model[] { - models.forEach((models: Model): void => { - models.imports = getSortedImports(models.imports); + models.forEach(model => { + model.imports = getSortedImports(model.imports).filter(name => { + return model.name !== name; + }); + model.properties = model.properties + .filter((property, index, arr) => { + return arr.findIndex(item => item.name === property.name) === index; + }) + .sort((a, b) => { + const nameA = a.name.toLowerCase(); + const nameB = b.name.toLowerCase(); + return nameA.localeCompare(nameB); + }); }); return models; } diff --git a/src/utils/cleanupServices.ts b/src/utils/cleanupServices.ts index 5cc5ddd1..e54a5aad 100644 --- a/src/utils/cleanupServices.ts +++ b/src/utils/cleanupServices.ts @@ -1,28 +1,27 @@ import { Service } from '../client/interfaces/Service'; import { getSortedImports } from './getSortedImports'; -import { Operation } from '../client/interfaces/Operation'; export function cleanupServices(services: Service[]): Service[] { - services.forEach((service: Service): void => { - const names: Map = new Map(); + services.forEach(service => { + const names = new Map(); - service.imports = getSortedImports(service.imports); + service.imports = getSortedImports(service.imports).filter(name => { + return service.name !== name; + }); service.operations = service.operations - .map( - (operation: Operation): Operation => { - const name: string = operation.name; - const index: number = names.get(name) || 0; - if (index > 0) { - operation.name = `${name}${index}`; - } - names.set(name, index + 1); - return operation; + .map(operation => { + const name = operation.name; + const index = names.get(name) || 0; + if (index > 0) { + operation.name = `${name}${index}`; } - ) - .sort((a: Operation, b: Operation): number => { - const nameA: string = a.name.toLowerCase(); - const nameB: string = b.name.toLowerCase(); + names.set(name, index + 1); + return operation; + }) + .sort((a, b) => { + const nameA = a.name.toLowerCase(); + const nameB = b.name.toLowerCase(); return nameA.localeCompare(nameB); }); }); diff --git a/src/utils/getOpenApiSpec.ts b/src/utils/getOpenApiSpec.ts index c06cfccb..709de0f9 100644 --- a/src/utils/getOpenApiSpec.ts +++ b/src/utils/getOpenApiSpec.ts @@ -24,8 +24,8 @@ function read(filePath: string): string { * @param filePath */ export function getOpenApiSpec(filePath: string): any { - const content: string = read(filePath); - const extname: string = path.extname(filePath).toLowerCase(); + const content = read(filePath); + const extname = path.extname(filePath).toLowerCase(); switch (extname) { case '.yml': case '.yaml': diff --git a/src/utils/getOpenApiVersion.ts b/src/utils/getOpenApiVersion.ts index 7cde3691..b9ff66b5 100644 --- a/src/utils/getOpenApiVersion.ts +++ b/src/utils/getOpenApiVersion.ts @@ -12,8 +12,8 @@ export enum OpenApiVersion { export function getOpenApiVersion(openApi: any): OpenApiVersion { const info: any = openApi.swagger || openApi.openapi; if (info && typeof info === 'string') { - const c: string = info.charAt(0); - const v: number = Number.parseInt(c); + const c = info.charAt(0); + const v = Number.parseInt(c); if (v === OpenApiVersion.V2 || v === OpenApiVersion.V3) { return v as OpenApiVersion; } diff --git a/src/utils/getSortedImports.ts b/src/utils/getSortedImports.ts index b1236f4a..78d462dd 100644 --- a/src/utils/getSortedImports.ts +++ b/src/utils/getSortedImports.ts @@ -6,12 +6,12 @@ export function getSortedImports(imports: string[]): string[] { return imports .filter(name => name) .filter(name => name.trim()) - .filter((name: string, index: number, arr: string[]) => { + .filter((name, index, arr) => { return arr.indexOf(name) === index; }) .sort((a, b) => { - const nameA: string = a.toLowerCase(); - const nameB: string = b.toLowerCase(); + const nameA = a.toLowerCase(); + const nameB = b.toLowerCase(); return nameA.localeCompare(nameB, 'en'); }); } diff --git a/src/utils/getSortedModels.spec.ts b/src/utils/getSortedModels.spec.ts index 2527a6c9..63f594fb 100644 --- a/src/utils/getSortedModels.spec.ts +++ b/src/utils/getSortedModels.spec.ts @@ -14,9 +14,9 @@ describe('getSortedModels', () => { template: null, validation: null, description: null, - extends: null, + extends: [], imports: [], - values: [], + symbols: [], properties: [], enums: [], }, @@ -30,9 +30,9 @@ describe('getSortedModels', () => { template: null, validation: null, description: null, - extends: null, + extends: [], imports: [], - values: [], + symbols: [], properties: [], enums: [], }, @@ -46,9 +46,9 @@ describe('getSortedModels', () => { template: null, validation: null, description: null, - extends: null, + extends: [], imports: [], - values: [], + symbols: [], properties: [], enums: [], }, diff --git a/src/utils/getSortedModels.ts b/src/utils/getSortedModels.ts index 281e4104..3fc44c1f 100644 --- a/src/utils/getSortedModels.ts +++ b/src/utils/getSortedModels.ts @@ -2,8 +2,8 @@ import { Model } from '../client/interfaces/Model'; export function getSortedModels(models: Model[]): Model[] { return models.sort((a, b) => { - const nameA: string = a.name.toLowerCase(); - const nameB: string = b.name.toLowerCase(); + const nameA = a.name.toLowerCase(); + const nameB = b.name.toLowerCase(); return nameA.localeCompare(nameB, 'en'); }); } diff --git a/src/utils/getSortedServices.ts b/src/utils/getSortedServices.ts index 82e86cfb..49fc6cb4 100644 --- a/src/utils/getSortedServices.ts +++ b/src/utils/getSortedServices.ts @@ -2,8 +2,8 @@ import { Service } from '../client/interfaces/Service'; export function getSortedServices(services: Service[]): Service[] { return services.sort((a, b) => { - const nameA: string = a.name.toLowerCase(); - const nameB: string = b.name.toLowerCase(); + const nameA = a.name.toLowerCase(); + const nameB = b.name.toLowerCase(); return nameA.localeCompare(nameB, 'en'); }); } diff --git a/src/utils/readHandlebarsTemplate.ts b/src/utils/readHandlebarsTemplate.ts index 3e8dd6a0..44df7f4d 100644 --- a/src/utils/readHandlebarsTemplate.ts +++ b/src/utils/readHandlebarsTemplate.ts @@ -7,9 +7,11 @@ import * as handlebars from 'handlebars'; */ export function readHandlebarsTemplate(filePath: string): handlebars.TemplateDelegate { if (fs.existsSync(filePath)) { - const template: string = fs.readFileSync(filePath, 'utf8').toString(); + const template = fs.readFileSync(filePath, 'utf8').toString(); try { - return handlebars.compile(template); + return handlebars.compile(template, { + strict: true, + }); } catch (e) { throw new Error(`Could not compile Handlebar template: "${filePath}"`); } diff --git a/src/utils/readHandlebarsTemplates.ts b/src/utils/readHandlebarsTemplates.ts index 445c9b85..4b0a5569 100644 --- a/src/utils/readHandlebarsTemplates.ts +++ b/src/utils/readHandlebarsTemplates.ts @@ -15,9 +15,9 @@ export interface Templates { * @param language The language we need to generate (Typescript or Javascript). */ export function readHandlebarsTemplates(language: Language): Templates { - const pathTemplateIndex: string = path.resolve(__dirname, `../../src/templates/${language}/index.hbs`); - const pathTemplateModel: string = path.resolve(__dirname, `../../src/templates/${language}/model.hbs`); - const pathTemplateService: string = path.resolve(__dirname, `../../src/templates/${language}/service.hbs`); + const pathTemplateIndex = path.resolve(__dirname, `../../src/templates/${language}/index.hbs`); + const pathTemplateModel = path.resolve(__dirname, `../../src/templates/${language}/model.hbs`); + const pathTemplateService = path.resolve(__dirname, `../../src/templates/${language}/service.hbs`); try { return { diff --git a/src/utils/writeClient.ts b/src/utils/writeClient.ts index a532497c..11313f9b 100644 --- a/src/utils/writeClient.ts +++ b/src/utils/writeClient.ts @@ -23,9 +23,9 @@ import { cleanupModels } from './cleanupModels'; * @param outputPath */ export function writeClient(client: Client, language: Language, templates: Templates, outputPath: string): void { - const outputPathCore: string = path.resolve(outputPath, 'core'); - const outputPathModels: string = path.resolve(outputPath, 'models'); - const outputPathServices: string = path.resolve(outputPath, 'services'); + const outputPathCore = path.resolve(outputPath, 'core'); + const outputPathModels = path.resolve(outputPath, 'models'); + const outputPathServices = path.resolve(outputPath, 'services'); // Clean output directory try { @@ -45,9 +45,9 @@ export function writeClient(client: Client, language: Language, templates: Templ } // Copy all core files - const coreFiles: string = path.resolve(__dirname, `../../src/templates/${language}/core/`); - const coreFilesExt: string = getFileName('*', language); - const coreFilesList: string[] = glob.sync(coreFilesExt, { cwd: coreFiles }); + const coreFiles = path.resolve(__dirname, `../../src/templates/${language}/core/`); + const coreFilesExt = getFileName('*', language); + const coreFilesList = glob.sync(coreFilesExt, { cwd: coreFiles }); coreFilesList.forEach(file => fs.copyFileSync( path.resolve(coreFiles, file), // From input path @@ -57,7 +57,6 @@ export function writeClient(client: Client, language: Language, templates: Templ // Write the client files try { - // TODO: Cleanup models writeClientIndex(client, language, templates.index, outputPath); writeClientModels(getSortedModels(cleanupModels(client.models)), language, templates.model, outputPathModels); writeClientServices(getSortedServices(cleanupServices(client.services)), language, templates.service, outputPathServices); diff --git a/src/utils/writeClientIndex.ts b/src/utils/writeClientIndex.ts index 486a2782..fbb7a6a7 100644 --- a/src/utils/writeClientIndex.ts +++ b/src/utils/writeClientIndex.ts @@ -17,7 +17,7 @@ import { getFileName } from './getFileName'; * @param outputPath: */ export function writeClientIndex(client: Client, language: Language, template: handlebars.TemplateDelegate, outputPath: string): void { - const fileName: string = getFileName('index', language); + const fileName = getFileName('index', language); try { fs.writeFileSync( path.resolve(outputPath, fileName), diff --git a/src/utils/writeClientModels.spec.ts b/src/utils/writeClientModels.spec.ts index cd58fbd8..c8e76fb5 100644 --- a/src/utils/writeClientModels.spec.ts +++ b/src/utils/writeClientModels.spec.ts @@ -20,9 +20,9 @@ describe('writeClientModels', () => { template: null, validation: null, description: null, - extends: null, + extends: [], imports: [], - values: [], + symbols: [], properties: [], enums: [], }, diff --git a/src/utils/writeClientModels.ts b/src/utils/writeClientModels.ts index 3fe7cbea..e0d9b5c0 100644 --- a/src/utils/writeClientModels.ts +++ b/src/utils/writeClientModels.ts @@ -14,7 +14,7 @@ import { getFileName } from './getFileName'; */ export function writeClientModels(models: Model[], language: Language, template: handlebars.TemplateDelegate, outputPath: string): void { models.forEach(model => { - const fileName: string = getFileName(model.name, language); + const fileName = getFileName(model.name, language); try { fs.writeFileSync(path.resolve(outputPath, fileName), template(model)); } catch (e) { diff --git a/src/utils/writeClientServices.ts b/src/utils/writeClientServices.ts index 980ad12b..8b366f24 100644 --- a/src/utils/writeClientServices.ts +++ b/src/utils/writeClientServices.ts @@ -14,7 +14,7 @@ import { getFileName } from './getFileName'; */ export function writeClientServices(services: Service[], language: Language, template: handlebars.TemplateDelegate, outputPath: string): void { services.forEach(service => { - const fileName: string = getFileName(service.name, language); + const fileName = getFileName(service.name, language); try { fs.writeFileSync(path.resolve(outputPath, fileName), template(service)); } catch (e) { diff --git a/test/mock/v2/spec.json b/test/mock/v2/spec.json index 7af3f6ba..728f31a0 100644 --- a/test/mock/v2/spec.json +++ b/test/mock/v2/spec.json @@ -24,6 +24,10 @@ "description": "This is a simple string", "type": "string" }, + "SimpleFile": { + "description": "This is a simple file", + "type": "File" + }, "SimpleEnumWithStrings": { "description": "This is a simple enum with strings", "enum": [ @@ -76,6 +80,7 @@ "description": "This is a simple array with a properties", "type": "array", "items": { + "type": "object", "properties": { "foo": { "type": "string" @@ -86,6 +91,10 @@ } } }, + "SimpleReference": { + "description": "This is a simple model reference", + "$ref": "#/definitions/ModelWithString" + }, "ModelWithInteger": { "description": "This is a model with one number property", "type": "object", @@ -107,8 +116,8 @@ } }, "ModelWithString": { - "type": "object", "description": "This is a model with one string property", + "type": "object", "properties": { "prop": { "description": "This is a simple string property", @@ -117,8 +126,8 @@ } }, "ModelWithEnum": { - "type": "object", "description": "This is a model with one enum", + "type": "object", "properties": { "prop": { "description": "This is a simple enum with strings", @@ -131,8 +140,8 @@ } }, "ModelWithEnumFromDescription": { - "type": "object", "description": "This is a model with one enum", + "type": "object", "properties": { "prop": { "type": "integer", @@ -141,8 +150,8 @@ } }, "ModelWithReference": { - "type": "object", "description": "This is a model with one property containing a reference", + "type": "object", "properties": { "prop": { "$ref": "#/definitions/ModelWithString" @@ -150,8 +159,8 @@ } }, "ModelWithCircularReference": { - "type": "object", "description": "This is a model with one property containing a circular reference", + "type": "object", "properties": { "prop": { "$ref": "#/definitions/ModelWithCircularReference" @@ -159,8 +168,8 @@ } }, "ModelWithNestedProperties": { - "type": "object", "description": "This is a model with one nested property", + "type": "object", "properties": { "first": { "type": "object", @@ -176,6 +185,74 @@ } } } + }, + "ModelWithDuplicateProperties": { + "description": "This is a model with duplicated properties", + "type": "object", + "properties": { + "prop": { + "$ref": "#/definitions/ModelWithString" + }, + "prop": { + "$ref": "#/definitions/ModelWithString" + }, + "prop": { + "$ref": "#/definitions/ModelWithString" + } + } + }, + "ModelWithDuplicateImports": { + "description": "This is a model with duplicated imports", + "type": "object", + "properties": { + "propA": { + "$ref": "#/definitions/ModelWithString" + }, + "propB": { + "$ref": "#/definitions/ModelWithString" + }, + "propC": { + "$ref": "#/definitions/ModelWithString" + } + } + }, + "ModelThatExtends": { + "type": "object", + "allOf": [ + { + "$ref": "#/definitions/ModelWithString" + }, + { + "type": "object", + "properties": { + "propExtendsA": { + "type": "string" + }, + "propExtendsB": { + "$ref": "#/definitions/ModelWithString" + } + } + } + ] + }, + "ModelThatExtendsExtends": { + "type": "object", + "allOf": [ + { + "$ref": "#/definitions/ModelThatExtends" + }, + { + "type": "object", + "properties": { + "propExtendsC": { + "type": "string" + }, + "propExtendsD": { + "$ref": "#/definitions/ModelWithString" + } + } + } + ] } } }