diff --git a/compiler/src/model/metamodel.ts b/compiler/src/model/metamodel.ts index 26f47a9895..9e1a870f89 100644 --- a/compiler/src/model/metamodel.ts +++ b/compiler/src/model/metamodel.ts @@ -469,6 +469,8 @@ export class Endpoint { index?: string[] cluster?: string[] } + + codegenExclude?: boolean } export class UrlTemplate { diff --git a/compiler/src/model/utils.ts b/compiler/src/model/utils.ts index 6e8ff5a809..96cdc1d170 100644 --- a/compiler/src/model/utils.ts +++ b/compiler/src/model/utils.ts @@ -634,7 +634,7 @@ export function hoistRequestAnnotations ( request: model.Request, jsDocs: JSDoc[], mappings: Record, response: model.TypeName | null ): void { const knownRequestAnnotations = [ - 'rest_spec_name', 'behavior', 'class_serializer', 'index_privileges', 'cluster_privileges', 'doc_id', 'availability', 'doc_tag', 'ext_doc_id' + 'rest_spec_name', 'behavior', 'class_serializer', 'index_privileges', 'cluster_privileges', 'doc_id', 'availability', 'doc_tag', 'ext_doc_id', 'codegen_exclude' ] // in most of the cases the jsDocs comes in a single block, // but it can happen that the user defines multiple single line jsDoc. @@ -720,6 +720,9 @@ export function hoistRequestAnnotations ( } else if (tag === 'doc_tag') { assert(jsDocs, value.trim() !== '', `Request ${request.name.name}'s @doc_tag cannot be empty`) endpoint.docTag = value.trim() + } else if (tag === 'codegen_exclude') { + // Mark this endpoint to be excluded from client code generation + endpoint.codegenExclude = true } else { assert(jsDocs, false, `Unhandled tag: '${tag}' with value: '${value}' on request ${request.name.name}`) } diff --git a/output/schema/schema.json b/output/schema/schema.json index 22c3e4fd66..a809034813 100644 --- a/output/schema/schema.json +++ b/output/schema/schema.json @@ -14,6 +14,7 @@ "visibility": "private" } }, + "codegenExclude": true, "description": "This API is a diagnostics API and the output should not be relied upon for building applications.", "docUrl": null, "name": "_internal.delete_desired_balance", @@ -45,6 +46,7 @@ "visibility": "private" } }, + "codegenExclude": true, "description": "Designed for indirect use by ECE/ESS and ECK, direct use is not supported.", "docUrl": null, "name": "_internal.delete_desired_nodes", @@ -76,6 +78,7 @@ "visibility": "private" } }, + "codegenExclude": true, "description": "This API is a diagnostics API and the output should not be relied upon for building applications.", "docUrl": null, "name": "_internal.get_desired_balance", @@ -107,6 +110,7 @@ "visibility": "private" } }, + "codegenExclude": true, "description": "Gets the latest desired nodes.", "docUrl": null, "name": "_internal.get_desired_nodes", @@ -138,6 +142,7 @@ "visibility": "private" } }, + "codegenExclude": true, "description": "Prevalidates node removal from the cluster.", "docUrl": null, "name": "_internal.prevalidate_node_removal", @@ -169,6 +174,7 @@ "visibility": "private" } }, + "codegenExclude": true, "description": "Designed for indirect use by ECE/ESS and ECK, direct use is not supported.", "docUrl": null, "name": "_internal.update_desired_nodes", @@ -50912,7 +50918,7 @@ } } ], - "specLocation": "_internal/delete_desired_balance/InternalDeleteDesiredBalanceRequest.ts#L23-L43" + "specLocation": "_internal/delete_desired_balance/InternalDeleteDesiredBalanceRequest.ts#L23-L44" }, { "kind": "response", @@ -50973,7 +50979,7 @@ } } ], - "specLocation": "_internal/delete_desired_nodes/InternalDeleteDesiredNodesRequest.ts#L23-L48" + "specLocation": "_internal/delete_desired_nodes/InternalDeleteDesiredNodesRequest.ts#L23-L49" }, { "kind": "response", @@ -51021,7 +51027,7 @@ } } ], - "specLocation": "_internal/get_desired_balance/InternalGetDesiredBalanceRequest.ts#L23-L43" + "specLocation": "_internal/get_desired_balance/InternalGetDesiredBalanceRequest.ts#L23-L44" }, { "kind": "response", @@ -51073,7 +51079,7 @@ } } ], - "specLocation": "_internal/get_desired_nodes/InternalGetDesiredNodesRequest.ts#L23-L43" + "specLocation": "_internal/get_desired_nodes/InternalGetDesiredNodesRequest.ts#L23-L44" }, { "kind": "response", @@ -51186,7 +51192,7 @@ } } ], - "specLocation": "_internal/prevalidate_node_removal/InternalPrevalidateNodeRemovalRequest.ts#L23-L63" + "specLocation": "_internal/prevalidate_node_removal/InternalPrevalidateNodeRemovalRequest.ts#L23-L64" }, { "kind": "response", @@ -51292,7 +51298,7 @@ } } ], - "specLocation": "_internal/update_desired_nodes/InternalUpdateDesiredNodesRequest.ts#L25-L66" + "specLocation": "_internal/update_desired_nodes/InternalUpdateDesiredNodesRequest.ts#L25-L67" }, { "kind": "response", diff --git a/specification/_internal/delete_desired_balance/InternalDeleteDesiredBalanceRequest.ts b/specification/_internal/delete_desired_balance/InternalDeleteDesiredBalanceRequest.ts index 5ae988ac69..817b659bbd 100644 --- a/specification/_internal/delete_desired_balance/InternalDeleteDesiredBalanceRequest.ts +++ b/specification/_internal/delete_desired_balance/InternalDeleteDesiredBalanceRequest.ts @@ -25,6 +25,7 @@ import { Duration } from '@_types/Time' * * @rest_spec_name _internal.delete_desired_balance * @availability stack stability=experimental visibility=private + * @codegen_exclude */ export interface Request extends RequestBase { urls: [ diff --git a/specification/_internal/delete_desired_nodes/InternalDeleteDesiredNodesRequest.ts b/specification/_internal/delete_desired_nodes/InternalDeleteDesiredNodesRequest.ts index 046c42fbcd..64d7f6c32c 100644 --- a/specification/_internal/delete_desired_nodes/InternalDeleteDesiredNodesRequest.ts +++ b/specification/_internal/delete_desired_nodes/InternalDeleteDesiredNodesRequest.ts @@ -25,6 +25,7 @@ import { Duration } from '@_types/Time' * * @rest_spec_name _internal.delete_desired_nodes * @availability stack stability=experimental visibility=private + * @codegen_exclude */ export interface Request extends RequestBase { urls: [ diff --git a/specification/_internal/get_desired_balance/InternalGetDesiredBalanceRequest.ts b/specification/_internal/get_desired_balance/InternalGetDesiredBalanceRequest.ts index 5b1ace6e51..dec68526f1 100644 --- a/specification/_internal/get_desired_balance/InternalGetDesiredBalanceRequest.ts +++ b/specification/_internal/get_desired_balance/InternalGetDesiredBalanceRequest.ts @@ -25,6 +25,7 @@ import { Duration } from '@_types/Time' * * @rest_spec_name _internal.get_desired_balance * @availability stack stability=experimental visibility=private + * @codegen_exclude */ export interface Request extends RequestBase { urls: [ diff --git a/specification/_internal/get_desired_nodes/InternalGetDesiredNodesRequest.ts b/specification/_internal/get_desired_nodes/InternalGetDesiredNodesRequest.ts index fca80bae87..e1ddb36ced 100644 --- a/specification/_internal/get_desired_nodes/InternalGetDesiredNodesRequest.ts +++ b/specification/_internal/get_desired_nodes/InternalGetDesiredNodesRequest.ts @@ -25,6 +25,7 @@ import { Duration } from '@_types/Time' * * @rest_spec_name _internal.get_desired_nodes * @availability stack stability=experimental visibility=private + * @codegen_exclude */ export interface Request extends RequestBase { urls: [ diff --git a/specification/_internal/prevalidate_node_removal/InternalPrevalidateNodeRemovalRequest.ts b/specification/_internal/prevalidate_node_removal/InternalPrevalidateNodeRemovalRequest.ts index 51d8f05a8d..04b3827ecb 100644 --- a/specification/_internal/prevalidate_node_removal/InternalPrevalidateNodeRemovalRequest.ts +++ b/specification/_internal/prevalidate_node_removal/InternalPrevalidateNodeRemovalRequest.ts @@ -25,6 +25,7 @@ import { Duration } from '@_types/Time' * * @rest_spec_name _internal.prevalidate_node_removal * @availability stack stability=experimental visibility=private + * @codegen_exclude */ export interface Request extends RequestBase { urls: [ diff --git a/specification/_internal/update_desired_nodes/InternalUpdateDesiredNodesRequest.ts b/specification/_internal/update_desired_nodes/InternalUpdateDesiredNodesRequest.ts index 4d4229b8cf..a8aeed6b1d 100644 --- a/specification/_internal/update_desired_nodes/InternalUpdateDesiredNodesRequest.ts +++ b/specification/_internal/update_desired_nodes/InternalUpdateDesiredNodesRequest.ts @@ -27,6 +27,7 @@ import { UserDefinedValue } from '@spec_utils/UserDefinedValue' * * @rest_spec_name _internal.update_desired_nodes * @availability stack stability=experimental visibility=private + * @codegen_exclude */ export interface Request extends RequestBase { urls: [ diff --git a/specification/eslint.config.js b/specification/eslint.config.js index 3579aa3315..8c53878c57 100644 --- a/specification/eslint.config.js +++ b/specification/eslint.config.js @@ -38,6 +38,7 @@ export default defineConfig([ 'es-spec-validator/no-native-types': 'error', 'es-spec-validator/invalid-node-types': 'error', 'es-spec-validator/no-generic-number': 'error', + 'es-spec-validator/codegen-exclude-on-request-only': 'error', 'es-spec-validator/request-must-have-urls': 'error', 'es-spec-validator/no-variants-on-responses': 'error', 'es-spec-validator/no-inline-unions': 'error', @@ -144,6 +145,7 @@ export default defineConfig([ 'behavior_meta', 'class_serializer', 'cluster_privileges', + 'codegen_exclude', 'codegen_name', 'codegen_names', 'doc_id', diff --git a/typescript-generator/src/metamodel.ts b/typescript-generator/src/metamodel.ts index 26f47a9895..9e1a870f89 100644 --- a/typescript-generator/src/metamodel.ts +++ b/typescript-generator/src/metamodel.ts @@ -469,6 +469,8 @@ export class Endpoint { index?: string[] cluster?: string[] } + + codegenExclude?: boolean } export class UrlTemplate { diff --git a/validator/README.md b/validator/README.md index cd7bed5213..daddc399f3 100644 --- a/validator/README.md +++ b/validator/README.md @@ -5,20 +5,21 @@ It is configured [in the specification directory](../specification/eslint.config ## Rules -| Name | Description | -|---------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `single-key-dictionary-key-is-string` | `SingleKeyDictionary` keys must be strings. | -| `dictionary-key-is-string` | `Dictionary` keys must be strings. | -| `no-native-types` | TypeScript native utility types (`Record`, `Partial`, etc.) and collection types (`Map`, `Set`, etc.) are not allowed. Use spec-defined aliases like `Dictionary` instead. | -| `invalid-node-types` | The spec uses a subset of TypeScript, so some types, clauses and expressions are not allowed. | -| `no-generic-number` | Generic `number` type is not allowed outside of `_types/Numeric.ts`. Use concrete numeric types like `integer`, `long`, `float`, `double`, etc. | -| `request-must-have-urls` | All Request interfaces extending `RequestBase` must have a `urls` property defining their endpoint paths and HTTP methods. | -| `no-variants-on-responses` | `@variants` is only supported on Interface types, not on Request or Response classes. Use value_body pattern with `@codegen_name` instead. | -| `no-inline-unions` | Inline union types (e.g., `field: A \| B`) are not allowed in properties/fields. Define a named type alias instead to improve code generation for statically-typed languages. | -| `prefer-tagged-variants` | Union of class types should use tagged variants (`@variants internal` or `@variants container`) instead of inline unions for better deserialization support in statically-typed languages. | -| `no-duplicate-type-names` | All types must be unique across class and enum definitions. | +| Name | Description | +|---------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `single-key-dictionary-key-is-string` | `SingleKeyDictionary` keys must be strings. | +| `dictionary-key-is-string` | `Dictionary` keys must be strings. | +| `no-native-types` | TypeScript native utility types (`Record`, `Partial`, etc.) and collection types (`Map`, `Set`, etc.) are not allowed. Use spec-defined aliases like `Dictionary` instead. | +| `invalid-node-types` | The spec uses a subset of TypeScript, so some types, clauses and expressions are not allowed. | +| `no-generic-number` | Generic `number` type is not allowed outside of `_types/Numeric.ts`. Use concrete numeric types like `integer`, `long`, `float`, `double`, etc. | +| `request-must-have-urls` | All Request interfaces extending `RequestBase` must have a `urls` property defining their endpoint paths and HTTP methods. | +| `no-variants-on-responses` | `@variants` is only supported on Interface types, not on Request or Response classes. Use value_body pattern with `@codegen_name` instead. Includes additional checks on variant tag use. | +| `no-inline-unions` | Inline union types (e.g., `field: A \| B`) are not allowed in properties/fields. Define a named type alias instead to improve code generation for statically-typed languages. | +| `prefer-tagged-variants` | Union of class types should use tagged variants (`@variants internal` or `@variants container`) instead of inline unions for better deserialization support in statically-typed languages. | +| `no-duplicate-type-names` | All types must be unique across class and enum definitions. | +| `no-all-string-literal-unions | Unions consisting entirely of string literals (e.g., `"green" \| "yellow" \| "red"`) are not allowed, use enums instead. | | `jsdoc-endpoint-check` | Validates JSDoc on endpoints in the specification. Ensuring consistent formatting. Some errors can be fixed with `--fix`. | -| `no-all-string-literal-unions | Unions consisting entirely of string literals (e.g., `"green" \| "yellow" \| "red"`) are not allowed, use enums instead. | | +| `codegen-exclude-on-request-only` | Ensures `@codegen_exclude` is only used on request definitions located in namespaced `specification/` files (i.e. files. | ## Usage diff --git a/validator/eslint-plugin-es-spec.js b/validator/eslint-plugin-es-spec.js index b89f617f80..75e2717c6e 100644 --- a/validator/eslint-plugin-es-spec.js +++ b/validator/eslint-plugin-es-spec.js @@ -28,6 +28,7 @@ import preferTaggedVariants from './rules/prefer-tagged-variants.js' import noDuplicateTypeNames from './rules/no-duplicate-type-names.js' import noAllStringLiteralUnions from './rules/no-all-string-literal-unions.js' import jsdocEndpointCheck from './rules/jsdoc-endpoint-check.js' +import codegenExcludeOnRequestOnly from './rules/codegen-exclude-on-request-only.js' export default { rules: { @@ -40,8 +41,9 @@ export default { 'no-variants-on-responses': noVariantsOnResponses, 'no-inline-unions': noInlineUnions, 'prefer-tagged-variants': preferTaggedVariants, - 'no-all-string-literal-unions': noAllStringLiteralUnions, 'no-duplicate-type-names': noDuplicateTypeNames, - 'jsdoc-endpoint-check': jsdocEndpointCheck + 'no-all-string-literal-unions': noAllStringLiteralUnions, + 'jsdoc-endpoint-check': jsdocEndpointCheck, + 'codegen-exclude-on-request-only': codegenExcludeOnRequestOnly } } diff --git a/validator/rules/codegen-exclude-on-request-only.js b/validator/rules/codegen-exclude-on-request-only.js new file mode 100644 index 0000000000..7d92cfd719 --- /dev/null +++ b/validator/rules/codegen-exclude-on-request-only.js @@ -0,0 +1,58 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ESLintUtils } from '@typescript-eslint/utils' + +export const codegenExcludeOnRequestOnly = ESLintUtils.RuleCreator.withoutDocs({ + name: 'codegen-exclude-on-request-only', + meta: { + type: 'problem', + docs: { + description: 'Ensures @codegen_exclude is only used on Request.ts files in specification/', + recommended: 'error' + }, + messages: { + invalidUsage: '@codegen_exclude may only appear on Request definitions in specification/* (files named *Request.ts).' + }, + schema: [] + }, + defaultOptions: [], + create (context) { + const sourceCode = context.getSourceCode() + + function findCodegenExcludeComments () { + return sourceCode.getAllComments().filter(c => c.type === 'Block' && c.value.startsWith('*') && /@codegen_exclude\b/.test(c.value)) + } + + return { + Program (node) { + const filename = context.getFilename() + const allowed = /[\\/]specification[\\/][^\\/]+[\\/].*Request\.ts$/.test(filename) + const offending = findCodegenExcludeComments() + for (const c of offending) { + if (!allowed) { + context.report({ loc: c.loc, messageId: 'invalidUsage' }) + } + } + } + } + } +}) + +export default codegenExcludeOnRequestOnly diff --git a/validator/test/codegen-exclude-on-request-only.test.js b/validator/test/codegen-exclude-on-request-only.test.js new file mode 100644 index 0000000000..98c35b0d76 --- /dev/null +++ b/validator/test/codegen-exclude-on-request-only.test.js @@ -0,0 +1,85 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { RuleTester } from '@typescript-eslint/rule-tester' +import rule from '../rules/codegen-exclude-on-request-only.js' + +const ruleTester = new RuleTester({ + languageOptions: { + parserOptions: { + projectService: { + allowDefaultProject: ['*.ts*'], + defaultProject: 'tsconfig.json' + }, + tsconfigRootDir: new URL('../../specification/', import.meta.url).pathname + } + } +}) + +ruleTester.run('codegen-exclude-on-request-only', rule, { + valid: [ + { + name: 'namespaced Request file with @codegen_exclude', + filename: new URL('../../specification/ilm/put_lifecycle/PutLifecycleRequest.ts', import.meta.url).pathname, + code: ` + /** + * Some endpoint + * @rest_spec_name ilm.put_lifecycle + * @codegen_exclude + */ + export interface Request {} + ` + }, + { + name: 'request without tag in any file', + filename: new URL('../../specification/ilm/put_lifecycle/PutLifecycleRequest.ts', import.meta.url).pathname, + code: ` + export interface Request {} + ` + } + ], + invalid: [ + { + name: 'non-namespaced file under specification with @codegen_exclude', + filename: new URL('../../specification/SomeFile.ts', import.meta.url).pathname, + code: ` + /** + * @codegen_exclude + */ + export interface Something {} + `, + errors: [ + { messageId: 'invalidUsage' } + ] + }, + { + name: 'namespaced Response file with @codegen_exclude', + filename: new URL('../../specification/ilm/put_lifecycle/PutLifecycleResponse.ts', import.meta.url).pathname, + code: ` + /** + * @codegen_exclude + */ + export interface Response {} + `, + errors: [ + { messageId: 'invalidUsage' } + ] + } + ] +})