diff --git a/bin/index.js b/bin/index.js index 32f2fecbc..70bb392a4 100755 --- a/bin/index.js +++ b/bin/index.js @@ -16,6 +16,7 @@ const params = program .option('--name ', 'Custom client class name') .option('--useOptions', 'Use options instead of arguments') .option('--useUnionTypes', 'Use union types instead of enums') + .option('--useTuples ', 'Whether to convert constant size arrays to tuples.', false) .option('--exportCore ', 'Write core files to disk', true) .option('--exportServices ', 'Write services to disk', true) .option('--exportModels ', 'Write models to disk', true) @@ -37,6 +38,7 @@ if (OpenAPI) { clientName: params.name, useOptions: params.useOptions, useUnionTypes: params.useUnionTypes, + useTuples: JSON.parse(params.useTuples) === true, exportCore: JSON.parse(params.exportCore) === true, exportServices: JSON.parse(params.exportServices) === true, exportModels: JSON.parse(params.exportModels) === true, diff --git a/rollup.config.mjs b/rollup.config.mjs index df7373395..ddc68043b 100644 --- a/rollup.config.mjs +++ b/rollup.config.mjs @@ -39,6 +39,7 @@ const handlebarsPlugin = () => ({ escapeComment: true, escapeDescription: true, camelCase: true, + repeatTimes: true, }, }); return `export default ${templateSpec};`; diff --git a/src/client/interfaces/Model.d.ts b/src/client/interfaces/Model.d.ts index 5f0318942..090d5e41d 100644 --- a/src/client/interfaces/Model.d.ts +++ b/src/client/interfaces/Model.d.ts @@ -15,4 +15,5 @@ export interface Model extends Schema { enum: Enum[]; enums: Model[]; properties: Model[]; + isConstantSize?: boolean; } diff --git a/src/index.ts b/src/index.ts index e63919085..ceca5bc60 100644 --- a/src/index.ts +++ b/src/index.ts @@ -19,6 +19,7 @@ export type Options = { clientName?: string; useOptions?: boolean; useUnionTypes?: boolean; + useTuples?: boolean; exportCore?: boolean; exportServices?: boolean; exportModels?: boolean; @@ -57,6 +58,7 @@ export const generate = async ({ clientName, useOptions = false, useUnionTypes = false, + useTuples = false, exportCore = true, exportServices = true, exportModels = true, @@ -73,6 +75,7 @@ export const generate = async ({ httpClient, useUnionTypes, useOptions, + useTuples, }); switch (openApiVersion) { @@ -87,6 +90,7 @@ export const generate = async ({ httpClient, useOptions, useUnionTypes, + useTuples, exportCore, exportServices, exportModels, @@ -111,6 +115,7 @@ export const generate = async ({ httpClient, useOptions, useUnionTypes, + useTuples, exportCore, exportServices, exportModels, diff --git a/src/openApi/v3/parser/getModel.ts b/src/openApi/v3/parser/getModel.ts index 9e9c60a98..164f5a83e 100644 --- a/src/openApi/v3/parser/getModel.ts +++ b/src/openApi/v3/parser/getModel.ts @@ -73,26 +73,28 @@ export const getModel = ( } if (definition.type === 'array' && definition.items) { + model.export = 'array'; + model.isConstantSize = Boolean( + definition.minItems && definition.maxItems && definition.minItems === definition.maxItems + ); + if (definition.items.$ref) { const arrayItems = getType(definition.items.$ref); - model.export = 'array'; model.type = arrayItems.type; model.base = arrayItems.base; model.template = arrayItems.template; model.imports.push(...arrayItems.imports); model.default = getModelDefault(definition, model); - return model; } else { const arrayItems = getModel(openApi, definition.items); - model.export = 'array'; model.type = arrayItems.type; model.base = arrayItems.base; model.template = arrayItems.template; model.link = arrayItems; model.imports.push(...arrayItems.imports); model.default = getModelDefault(definition, model); - return model; } + return model; } if ( diff --git a/src/openApi/v3/parser/getModelProperties.ts b/src/openApi/v3/parser/getModelProperties.ts index 6e25ca833..9a1ed64af 100644 --- a/src/openApi/v3/parser/getModelProperties.ts +++ b/src/openApi/v3/parser/getModelProperties.ts @@ -98,6 +98,7 @@ export const getModelProperties = ( enum: model.enum, enums: model.enums, properties: model.properties, + isConstantSize: model.isConstantSize, ...propertyValues, }); } diff --git a/src/templates/partials/type.hbs b/src/templates/partials/type.hbs index b6b4834bd..bd2995158 100644 --- a/src/templates/partials/type.hbs +++ b/src/templates/partials/type.hbs @@ -5,7 +5,17 @@ {{else equals export 'enum'}} {{>typeEnum}} {{else equals export 'array'}} + +{{~#if @root.useTuples~}} +{{~#if isConstantSize ~}} +{{>typeTuple}} +{{~else~}} {{>typeArray}} +{{~/if~}} +{{~else~}} +{{>typeArray}} +{{~/if~}} + {{else equals export 'dictionary'}} {{>typeDictionary}} {{else equals export 'one-of'}} diff --git a/src/templates/partials/typeTuple.hbs b/src/templates/partials/typeTuple.hbs new file mode 100644 index 000000000..d501808fd --- /dev/null +++ b/src/templates/partials/typeTuple.hbs @@ -0,0 +1,21 @@ +{{~#if link~}} + +[ +{{~#repeatTimes minItems~}} +{{>type link}} +{{#unless isLast}}, {{/unless}} +{{~/repeatTimes~}} +] +{{>isNullable}} + +{{~else~}} + +[ +{{~#repeatTimes minItems~}} +{{>base}} +{{#unless isLast}}, {{/unless}} +{{~/repeatTimes~}} +] +{{>isNullable}} + +{{~/if~}} \ No newline at end of file diff --git a/src/utils/registerHandlebarHelpers.spec.ts b/src/utils/registerHandlebarHelpers.spec.ts index f8347abdb..4d4552904 100644 --- a/src/utils/registerHandlebarHelpers.spec.ts +++ b/src/utils/registerHandlebarHelpers.spec.ts @@ -9,6 +9,7 @@ describe('registerHandlebarHelpers', () => { httpClient: HttpClient.FETCH, useOptions: false, useUnionTypes: false, + useTuples: false, }); const helpers = Object.keys(Handlebars.helpers); expect(helpers).toContain('ifdef'); diff --git a/src/utils/registerHandlebarHelpers.ts b/src/utils/registerHandlebarHelpers.ts index 88f47c19b..dcc0cf966 100644 --- a/src/utils/registerHandlebarHelpers.ts +++ b/src/utils/registerHandlebarHelpers.ts @@ -11,6 +11,7 @@ export const registerHandlebarHelpers = (root: { httpClient: HttpClient; useOptions: boolean; useUnionTypes: boolean; + useTuples: boolean; }): void => { Handlebars.registerHelper('ifdef', function (this: any, ...args): string { const options = args.pop(); @@ -104,4 +105,16 @@ export const registerHandlebarHelpers = (root: { Handlebars.registerHelper('camelCase', function (value: string): string { return camelCase(value); }); + + Handlebars.registerHelper('repeatTimes', function (n, block) { + let accum = ''; + for (let i = 0; i < n; ++i) { + const t = Handlebars.createFrame(this); + if (i == n - 1) { + t.isLast = true; + } + accum += block.fn(t); + } + return accum; + }); }; diff --git a/src/utils/registerHandlebarTemplates.spec.ts b/src/utils/registerHandlebarTemplates.spec.ts index 5e1302384..f5559ff6f 100644 --- a/src/utils/registerHandlebarTemplates.spec.ts +++ b/src/utils/registerHandlebarTemplates.spec.ts @@ -7,6 +7,7 @@ describe('registerHandlebarTemplates', () => { httpClient: HttpClient.FETCH, useOptions: false, useUnionTypes: false, + useTuples: false, }); expect(templates.index).toBeDefined(); expect(templates.exports.model).toBeDefined(); diff --git a/src/utils/registerHandlebarTemplates.ts b/src/utils/registerHandlebarTemplates.ts index bf77cbdc1..59a9f7930 100644 --- a/src/utils/registerHandlebarTemplates.ts +++ b/src/utils/registerHandlebarTemplates.ts @@ -82,6 +82,7 @@ import partialTypeGeneric from '../templates/partials/typeGeneric.hbs'; import partialTypeInterface from '../templates/partials/typeInterface.hbs'; import partialTypeIntersection from '../templates/partials/typeIntersection.hbs'; import partialTypeReference from '../templates/partials/typeReference.hbs'; +import partialTypeTuple from '../templates/partials/typeTuple.hbs'; import partialTypeUnion from '../templates/partials/typeUnion.hbs'; import { registerHandlebarHelpers } from './registerHandlebarHelpers'; @@ -113,6 +114,7 @@ export const registerHandlebarTemplates = (root: { httpClient: HttpClient; useOptions: boolean; useUnionTypes: boolean; + useTuples: boolean; }): Templates => { registerHandlebarHelpers(root); @@ -162,6 +164,7 @@ export const registerHandlebarTemplates = (root: { Handlebars.registerPartial('typeGeneric', Handlebars.template(partialTypeGeneric)); Handlebars.registerPartial('typeInterface', Handlebars.template(partialTypeInterface)); Handlebars.registerPartial('typeReference', Handlebars.template(partialTypeReference)); + Handlebars.registerPartial('typeTuple', Handlebars.template(partialTypeTuple)); Handlebars.registerPartial('typeUnion', Handlebars.template(partialTypeUnion)); Handlebars.registerPartial('typeIntersection', Handlebars.template(partialTypeIntersection)); Handlebars.registerPartial('base', Handlebars.template(partialBase)); diff --git a/src/utils/writeClient.spec.ts b/src/utils/writeClient.spec.ts index 3c06a95a5..2bfc3e284 100644 --- a/src/utils/writeClient.spec.ts +++ b/src/utils/writeClient.spec.ts @@ -43,6 +43,7 @@ describe('writeClient', () => { HttpClient.FETCH, false, false, + false, true, true, true, diff --git a/src/utils/writeClient.ts b/src/utils/writeClient.ts index cea2f3d88..615e69e14 100644 --- a/src/utils/writeClient.ts +++ b/src/utils/writeClient.ts @@ -40,6 +40,7 @@ export const writeClient = async ( httpClient: HttpClient, useOptions: boolean, useUnionTypes: boolean, + useTuples: boolean, exportCore: boolean, exportServices: boolean, exportModels: boolean, @@ -91,7 +92,15 @@ export const writeClient = async ( if (exportModels) { await rmdir(outputPathModels); await mkdir(outputPathModels); - await writeClientModels(client.models, templates, outputPathModels, httpClient, useUnionTypes, indent); + await writeClientModels( + client.models, + templates, + outputPathModels, + httpClient, + useUnionTypes, + useTuples, + indent + ); } if (isDefined(clientName)) { @@ -106,6 +115,7 @@ export const writeClient = async ( templates, outputPath, useUnionTypes, + useTuples, exportCore, exportServices, exportModels, diff --git a/src/utils/writeClientIndex.spec.ts b/src/utils/writeClientIndex.spec.ts index a7d5b610a..05f1957cf 100644 --- a/src/utils/writeClientIndex.spec.ts +++ b/src/utils/writeClientIndex.spec.ts @@ -36,7 +36,7 @@ describe('writeClientIndex', () => { }, }; - await writeClientIndex(client, templates, '/', true, true, true, true, true, 'Service', ''); + await writeClientIndex(client, templates, '/', true, true, true, true, true, true, 'Service', ''); expect(writeFile).toBeCalledWith(resolve('/', '/index.ts'), 'index'); }); diff --git a/src/utils/writeClientIndex.ts b/src/utils/writeClientIndex.ts index 5044294d5..0bab6f018 100644 --- a/src/utils/writeClientIndex.ts +++ b/src/utils/writeClientIndex.ts @@ -28,6 +28,7 @@ export const writeClientIndex = async ( templates: Templates, outputPath: string, useUnionTypes: boolean, + useTuples: boolean, exportCore: boolean, exportServices: boolean, exportModels: boolean, @@ -42,6 +43,7 @@ export const writeClientIndex = async ( exportModels, exportSchemas, useUnionTypes, + useTuples, postfixServices, postfixModels, clientName, diff --git a/src/utils/writeClientModels.spec.ts b/src/utils/writeClientModels.spec.ts index ee0f2b4f6..61707dac9 100644 --- a/src/utils/writeClientModels.spec.ts +++ b/src/utils/writeClientModels.spec.ts @@ -52,7 +52,7 @@ describe('writeClientModels', () => { }, }; - await writeClientModels(models, templates, '/', HttpClient.FETCH, false, Indent.SPACE_4); + await writeClientModels(models, templates, '/', HttpClient.FETCH, false, false, Indent.SPACE_4); expect(writeFile).toBeCalledWith(resolve('/', '/User.ts'), `model${EOL}`); }); diff --git a/src/utils/writeClientModels.ts b/src/utils/writeClientModels.ts index 997569b9f..03249a378 100644 --- a/src/utils/writeClientModels.ts +++ b/src/utils/writeClientModels.ts @@ -23,6 +23,7 @@ export const writeClientModels = async ( outputPath: string, httpClient: HttpClient, useUnionTypes: boolean, + useTuples: boolean, indent: Indent ): Promise => { for (const model of models) { @@ -31,6 +32,7 @@ export const writeClientModels = async ( ...model, httpClient, useUnionTypes, + useTuples, }); await writeFile(file, i(f(templateResult), indent)); } diff --git a/src/utils/writeClientSchemas.ts b/src/utils/writeClientSchemas.ts index e1c885f64..f5bea4f3e 100644 --- a/src/utils/writeClientSchemas.ts +++ b/src/utils/writeClientSchemas.ts @@ -31,6 +31,7 @@ export const writeClientSchemas = async ( ...model, httpClient, useUnionTypes, + useTuples: true, }); await writeFile(file, i(f(templateResult), indent)); }