From e4404dfe5be413f967ca8c933672f4c151c65c79 Mon Sep 17 00:00:00 2001 From: p-mcgowan Date: Fri, 13 Jun 2025 07:53:11 +0200 Subject: [PATCH 1/2] docs: improve wording, add multiple template invocations --- readme.md | 46 +++++++++++++++++++++++++++------------------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/readme.md b/readme.md index 06d2a1f..43a9c99 100644 --- a/readme.md +++ b/readme.md @@ -76,23 +76,30 @@ then you can just run `npx bc model test --dry-run`. ## Custom Generator Overrides -You can override any of the generators by adding any of these files to a templates folder, and passing `--templates ` to the cli: -- boats-rc.js -- component-index.js -- create.js -- delete.js -- index.js -- list.js -- model.js -- models.js -- pagination-model.js -- param.js -- path-index.js -- replace.js -- show.js -- update.js - -Or, alternatively, exporting any of the following methods from a module (local file or node module): +You can override any of the generators by adding any files or exports to the templates option (`--templates `). + +Boats cli will try to import from the folling: + +| file | export | +|---------------------|--------------------| +| boats-rc.js | getBoatsRc | +| component-index.js | getComponentIndex | +| create.js | getCreate | +| delete.js | getDelete | +| index.js | getIndex | +| list.js | getList | +| model.js | getModel | +| models.js | getModels | +| pagination-model.js | getPaginationModel | +| param.js | getParam | +| path-index.js | getPathIndex | +| replace.js | getReplace | +| show.js | getShow | +| update.js | getUpdate | + +Multiple invocations of `-T, --templates` will merge / override the results of the templates, in the order supplied. + +A module export might look like: ```js exports.getBoatsRc = (opts, file) => { /* ... */ }; exports.getIndex = (opts, file) => { /* ... */ }; @@ -110,7 +117,7 @@ exports.getUpdate = (opts, file) => { /* ... */ }; exports.getReplace = (opts, file) => { /* ... */ }; ``` -for example, `templates/index.js` or `exports.getList`: +or overriding `path --list`, a file `templates/list.js` or module with `exports.getList`: ```js // @ts-check const { toYaml } = require('@acrontum/boats-cli/dist/src/lib'); @@ -134,9 +141,10 @@ module.exports = (_globalOptions, file, pluralName, schemaRef, parameters) => { }); }; +// or exports.getList = (_globalOptions, file, pluralName, schemaRef, parameters) => { ... } ```` -or disabling the default generator and instead creating 2 different files for models `templates/model.yml` or `exports.getModel`: +or disabling the default generator and instead creating 2 different files for models (`templates/model.yml` or `exports.getModel`): ```js // @ts-check const { toYaml } = require('@acrontum/boats-cli/dist/src/lib'); From d783e01d9442584d6e721ef899c8f3a07248157b Mon Sep 17 00:00:00 2001 From: p-mcgowan Date: Fri, 13 Jun 2025 07:53:22 +0200 Subject: [PATCH 2/2] enh: templates can be invoked multiple times Added support for extending multiple template overrides. Each call overwrites the previous, if both are defined. --- src/cli.ts | 15 +- test/custom-models.spec.ts | 50 ++- .../fixtures/overrides/single-export/model.js | 29 ++ .../overrides/single-export/models.js | 18 + test/fixtures/spec/custom-multi.json | 335 ++++++++++++++++++ 5 files changed, 410 insertions(+), 37 deletions(-) create mode 100644 test/fixtures/overrides/single-export/model.js create mode 100644 test/fixtures/overrides/single-export/models.js create mode 100644 test/fixtures/spec/custom-multi.json diff --git a/src/cli.ts b/src/cli.ts index 48374ec..aba27e5 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -93,7 +93,7 @@ export const cliArguments: Record = { type: 'string', short: 'T', [argname]: 'TEMPLATES', - [description]: 'Folder or module containing template overrides', + [description]: 'Folder or module containing template overrides (can be invoked multiple times)', }, force: { type: 'boolean', short: 'f', [description]: 'Overwrite existing files' }, 'no-index': { type: 'boolean', short: 'I', [description]: 'Skip auto-creating index files, only models' }, @@ -338,7 +338,13 @@ export const cli = async (args: string[]): Promise getIndex(globalOptions, 'src/index.yml'), filename: 'src/index.yml' }, diff --git a/test/custom-models.spec.ts b/test/custom-models.spec.ts index a0d4d74..d77d5c5 100644 --- a/test/custom-models.spec.ts +++ b/test/custom-models.spec.ts @@ -29,7 +29,7 @@ describe('custom-models.spec.ts', async () => { `), ); - const indexFile = await boats('test/output/custom/src/index.yml', 'test/output/custom/api.json'); + const indexFile = await boats('test/output/custom/src/index.yml', 'test/output/custom/build/api.json'); assert.strictEqual(indexFile !== '', true, 'boats failed'); assert.strictEqual(await getFile(indexFile), await getFile('test/fixtures/spec/custom.json'), 'spec mismatch'); @@ -96,38 +96,30 @@ describe('custom-models.spec.ts', async () => { --quiet --output test/output/custom -T test/fixtures/overrides/module.js + --templates test/fixtures/overrides + --templates test/fixtures/overrides/single-export/ `), ); - const files = await getAllFiles('test/output/custom/'); - assert.deepStrictEqual(files, [ - 'test/output/custom/.boatsrc', - 'test/output/custom/src/components/parameters/index.yml', - 'test/output/custom/src/components/parameters/pathUserId.yml', - 'test/output/custom/src/components/parameters/queryLimit.yml', - 'test/output/custom/src/components/parameters/queryOffset.yml', - 'test/output/custom/src/components/parameters/queryUserId.yml', - 'test/output/custom/src/components/schemas/index.yml', - 'test/output/custom/src/components/schemas/jwt/model.yml', - 'test/output/custom/src/components/schemas/pagination/model.yml', - 'test/output/custom/src/components/schemas/user/model.yml', - 'test/output/custom/src/components/schemas/user/models.yml', - 'test/output/custom/src/components/schemas/user/patch.yml', - 'test/output/custom/src/components/schemas/user/post.yml', - 'test/output/custom/src/components/schemas/user/put.yml', - 'test/output/custom/src/index.yml', - 'test/output/custom/src/paths/index.yml', - 'test/output/custom/src/paths/users/get.yml', - 'test/output/custom/src/paths/users/post.yml', - 'test/output/custom/src/paths/users/{userId}/delete.yml', - 'test/output/custom/src/paths/users/{userId}/get.yml', - 'test/output/custom/src/paths/users/{userId}/patch.yml', - 'test/output/custom/src/paths/users/{userId}/put.yml', - ]); + const indexFile = await boats('test/output/custom/src/index.yml', 'test/output/custom/build/api.json'); + + assert.strictEqual(indexFile !== '', true, 'boats failed'); + assert.strictEqual(await getFile(indexFile), await getFile('test/fixtures/spec/custom-multi.json'), 'spec mismatch'); + }); + + await it('adds and overwrites templates when invoked multiple times', async () => { + assert.deepStrictEqual(await getAllFiles('test/output/custom/').catch(() => []), []); - for (const file of await getAllFiles('test/output/custom/')) { - assert.deepStrictEqual(await getFile(file), file.replace('test/output/custom/', '')); - } + await cli( + toArgv(` + path users/:userId -crudl --put + model jwt + model userId --type query + --quiet + --output test/output/custom + -T test/fixtures/overrides/module.js + `), + ); }); await it('outputs a meaningful error message', async () => { diff --git a/test/fixtures/overrides/single-export/model.js b/test/fixtures/overrides/single-export/model.js new file mode 100644 index 0000000..403db01 --- /dev/null +++ b/test/fixtures/overrides/single-export/model.js @@ -0,0 +1,29 @@ +// @ts-check + +/** @type{import('../../../../').CustomTemplates['getModel']} */ +module.exports = (_globalOptions, _file) => { + return `\ +type: "object" +required: + - "id" + - "createdAt" + - "updatedAt" +properties: + id: + type: "string" + format: "uuid" + createdAt: + type: "string" + format: "date-time" + updatedAt: + type: "string" + format: "date-time" + name: + type: "string" + description: "Name of the thing, separated by dashes (-)" + example: "this-is-an-example" + minLength: 1 + pattern: "\\\\S" + nullable: true +`; +}; diff --git a/test/fixtures/overrides/single-export/models.js b/test/fixtures/overrides/single-export/models.js new file mode 100644 index 0000000..169f049 --- /dev/null +++ b/test/fixtures/overrides/single-export/models.js @@ -0,0 +1,18 @@ +// @ts-check + +const { toYaml } = require('../../../../dist/src/lib'); + +/** @type{import('../../../../').CustomTemplates['getModels']} */ +module.exports = (_globalOptions, _file) => { + return toYaml({ + type: 'object', + required: ['meta', 'data'], + properties: { + meta: { $ref: '#/components/schemas/Pagination' }, + data: { + type: 'array', + items: { $ref: './model.yml' }, + }, + }, + }); +}; diff --git a/test/fixtures/spec/custom-multi.json b/test/fixtures/spec/custom-multi.json new file mode 100644 index 0000000..0a13775 --- /dev/null +++ b/test/fixtures/spec/custom-multi.json @@ -0,0 +1,335 @@ +{ + "openapi": "3.1.0", + "paths": { + "/users": { + "get": { + "summary": "from src/paths/users/get.yml", + "description": "pluralName users", + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Users" + } + } + } + } + } + }, + "post": { + "summary": "create", + "description": "from src/paths/users/post.yml" + } + }, + "/users/{userId}": { + "delete": { + "summary": "delete", + "description": "from src/paths/users/{userId}/delete.yml" + }, + "get": { + "summary": "Show something, from src/paths/users/{userId}/get.yml", + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "type": "string" + } + } + } + }, + "404": { + "description": "not found" + } + } + }, + "patch": { + "summary": "Update parts of a user", + "description": "from src/paths/users/{userId}/patch.yml", + "parameters": [ + { + "$ref": "#/components/parameters/PathUserId" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserPatch" + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + } + }, + "404": { + "description": "User not found" + }, + "422": { + "description": "Invalid user supplied" + } + } + }, + "put": { + "summary": "upsert user", + "description": "from src/paths/users/{userId}/put.yml" + } + } + }, + "info": { + "title": "from src/index.yml", + "version": "1.0" + }, + "components": { + "parameters": { + "PathUserId": { + "in": "path", + "name": "userId", + "required": true, + "schema": { + "type": "string" + }, + "description": "path param that does some stuff", + "example": "dont pass in a file, like this: src/components/parameters/pathUserId.yml" + }, + "QueryLimit": { + "in": "query", + "name": "limit", + "required": false, + "schema": { + "type": "integer" + }, + "description": "query param that does some stuff", + "example": "dont pass in a file, like this: src/components/parameters/queryLimit.yml" + }, + "QueryOffset": { + "in": "query", + "name": "offset", + "required": false, + "schema": { + "type": "integer" + }, + "description": "query param that does some stuff", + "example": "dont pass in a file, like this: src/components/parameters/queryOffset.yml" + }, + "QueryUserId": { + "in": "query", + "name": "userId", + "required": false, + "schema": { + "type": "string" + }, + "description": "query param that does some stuff", + "example": "dont pass in a file, like this: src/components/parameters/queryUserId.yml" + } + }, + "schemas": { + "Jwt": { + "type": "object", + "required": [ + "id", + "createdAt", + "updatedAt" + ], + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + }, + "name": { + "type": "string", + "description": "Name of the thing, separated by dashes (-)", + "example": "this-is-an-example", + "minLength": 1, + "pattern": "\\S", + "nullable": true + } + } + }, + "Pagination": { + "type": "object", + "required": [ + "offset", + "limit", + "total" + ], + "properties": { + "offset": { + "type": "integer", + "minimum": 0, + "description": "Starting index" + }, + "limit": { + "type": "integer", + "minimum": 0, + "description": "Max items returned" + }, + "total": { + "type": "integer", + "minimum": 0, + "description": "Total items available" + } + } + }, + "User": { + "type": "object", + "required": [ + "id", + "createdAt", + "updatedAt" + ], + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + }, + "name": { + "type": "string", + "description": "Name of the thing, separated by dashes (-)", + "example": "this-is-an-example", + "minLength": 1, + "pattern": "\\S", + "nullable": true + } + } + }, + "Users": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/Pagination" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/User" + } + } + } + }, + "UserPatch": { + "type": "object", + "required": [ + "id", + "createdAt", + "updatedAt" + ], + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + }, + "name": { + "type": "string", + "description": "Name of the thing, separated by dashes (-)", + "example": "this-is-an-example", + "minLength": 1, + "pattern": "\\S", + "nullable": true + } + } + }, + "UserPost": { + "type": "object", + "required": [ + "id", + "createdAt", + "updatedAt" + ], + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + }, + "name": { + "type": "string", + "description": "Name of the thing, separated by dashes (-)", + "example": "this-is-an-example", + "minLength": 1, + "pattern": "\\S", + "nullable": true + } + } + }, + "UserPut": { + "type": "object", + "required": [ + "id", + "createdAt", + "updatedAt" + ], + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + }, + "name": { + "type": "string", + "description": "Name of the thing, separated by dashes (-)", + "example": "this-is-an-example", + "minLength": 1, + "pattern": "\\S", + "nullable": true + } + } + } + } + } +} \ No newline at end of file