From 8c848bc140060d10bd01de054b269f02ec3ab094 Mon Sep 17 00:00:00 2001 From: margaretjgu Date: Thu, 6 Nov 2025 11:06:30 -0500 Subject: [PATCH 1/4] requests to have urls --- specification/eslint.config.js | 3 +- validator/README.md | 1 + validator/eslint-plugin-es-spec.js | 26 +-- validator/rules/request-must-have-urls.js | 73 ++++++++ validator/test/request-must-have-urls.test.js | 164 ++++++++++++++++++ 5 files changed, 254 insertions(+), 13 deletions(-) create mode 100644 validator/rules/request-must-have-urls.js create mode 100644 validator/test/request-must-have-urls.test.js diff --git a/specification/eslint.config.js b/specification/eslint.config.js index ad85b7f178..1791b0d344 100644 --- a/specification/eslint.config.js +++ b/specification/eslint.config.js @@ -35,6 +35,7 @@ export default defineConfig({ 'es-spec-validator/dictionary-key-is-string': 'error', 'es-spec-validator/no-native-types': 'error', 'es-spec-validator/invalid-node-types': 'error', - 'es-spec-validator/no-generic-number': 'error' + 'es-spec-validator/no-generic-number': 'error', + 'es-spec-validator/request-must-have-urls': 'error' } }) diff --git a/validator/README.md b/validator/README.md index bfc886feb1..8e48e886fd 100644 --- a/validator/README.md +++ b/validator/README.md @@ -12,6 +12,7 @@ It is configured [in the specification directory](../specification/eslint.config | `no-native-types` | `Typescript native types not allowed, use aliases. | | `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. | ## Usage diff --git a/validator/eslint-plugin-es-spec.js b/validator/eslint-plugin-es-spec.js index a823ed46ee..d01be4ed79 100644 --- a/validator/eslint-plugin-es-spec.js +++ b/validator/eslint-plugin-es-spec.js @@ -16,18 +16,20 @@ * specific language governing permissions and limitations * under the License. */ -import singleKeyDict from './rules/single-key-dictionary-key-is-string.js' -import dict from './rules/dictionary-key-is-string.js' -import noNativeTypes from './rules/no-native-types.js' -import invalidNodeTypes from './rules/invalid-node-types.js' -import noGenericNumber from './rules/no-generic-number.js' +import singleKeyDict from "./rules/single-key-dictionary-key-is-string.js"; +import dict from "./rules/dictionary-key-is-string.js"; +import noNativeTypes from "./rules/no-native-types.js"; +import invalidNodeTypes from "./rules/invalid-node-types.js"; +import noGenericNumber from "./rules/no-generic-number.js"; +import requestMustHaveUrls from "./rules/request-must-have-urls.js"; export default { rules: { - 'single-key-dictionary-key-is-string': singleKeyDict, - 'dictionary-key-is-string': dict, - 'no-native-types': noNativeTypes, - 'invalid-node-types': invalidNodeTypes, - 'no-generic-number': noGenericNumber, - } -} + "single-key-dictionary-key-is-string": singleKeyDict, + "dictionary-key-is-string": dict, + "no-native-types": noNativeTypes, + "invalid-node-types": invalidNodeTypes, + "no-generic-number": noGenericNumber, + "request-must-have-urls": requestMustHaveUrls, + }, +}; diff --git a/validator/rules/request-must-have-urls.js b/validator/rules/request-must-have-urls.js new file mode 100644 index 0000000000..c244f563df --- /dev/null +++ b/validator/rules/request-must-have-urls.js @@ -0,0 +1,73 @@ +/* + * 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"; + +const createRule = ESLintUtils.RuleCreator( + (name) => `https://example.com/rule/${name}`, +); + +export default createRule({ + name: "request-must-have-urls", + create(context) { + return { + TSInterfaceDeclaration(node) { + const extendsRequestBase = node.extends?.some( + (heritage) => + heritage.expression.type === "Identifier" && + heritage.expression.name === "RequestBase", + ); + + const isNamedRequest = node.id.name === "Request"; + + if (!extendsRequestBase && !isNamedRequest) { + return; + } + + const hasUrls = node.body.body.some( + (member) => + member.type === "TSPropertySignature" && + member.key.type === "Identifier" && + member.key.name === "urls", + ); + + if (!hasUrls) { + context.report({ + node, + messageId: "missingUrls", + data: { + interfaceName: node.id.name, + }, + }); + } + }, + }; + }, + meta: { + docs: { + description: + "All Request interfaces must have a urls property defining their endpoints", + }, + messages: { + missingUrls: + 'Request interface "{{interfaceName}}" must have a urls property with endpoint paths and methods', + }, + type: "problem", + }, + defaultOptions: [], +}); diff --git a/validator/test/request-must-have-urls.test.js b/validator/test/request-must-have-urls.test.js new file mode 100644 index 0000000000..4474939e7b --- /dev/null +++ b/validator/test/request-must-have-urls.test.js @@ -0,0 +1,164 @@ +/* + * 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/request-must-have-urls.js"; + +const ruleTester = new RuleTester({ + languageOptions: { + parserOptions: { + projectService: { + allowDefaultProject: ["*.ts*"], + defaultProject: "tsconfig.json", + }, + tsconfigRootDir: new URL("../../specification/", import.meta.url) + .pathname, + }, + }, +}); + +ruleTester.run("request-must-have-urls", rule, { + valid: [ + { + name: "Request with urls property", + code: ` + interface RequestBase {} + export interface Request extends RequestBase { + urls: [ + { + path: '/_search' + methods: ['GET', 'POST'] + } + ] + path_parts: {} + query_parameters: {} + body: {} + } + `, + }, + { + name: "Request with multiple URL patterns", + code: ` + interface RequestBase {} + export interface Request extends RequestBase { + urls: [ + { + path: '/_snapshot/{repository}/{snapshot}' + methods: ['GET'] + }, + { + path: '/_snapshot/{repository}/_all' + methods: ['GET'] + } + ] + path_parts: { + repository: string + snapshot: string + } + query_parameters: {} + body: {} + } + `, + }, + { + name: "Non-Request interface without urls", + code: ` + export interface ResponseBody { + took: number + timed_out: boolean + } + `, + }, + { + name: "Response interface without urls", + code: ` + interface RequestBase {} + export interface Response { + body: { + result: string + } + } + `, + }, + ], + invalid: [ + { + name: "Request without urls property", + code: ` + interface RequestBase {} + export interface Request extends RequestBase { + path_parts: {} + query_parameters: {} + body: {} + } + `, + errors: [ + { + messageId: "missingUrls", + data: { + interfaceName: "Request", + }, + }, + ], + }, + { + name: "Named Request without urls", + code: ` + export interface Request { + path_parts: { + index: string + } + body: { + query: any + } + } + `, + errors: [ + { + messageId: "missingUrls", + data: { + interfaceName: "Request", + }, + }, + ], + }, + { + name: "Request extending RequestBase without urls", + code: ` + interface RequestBase {} + export interface SearchRequest extends RequestBase { + path_parts: { + index: string + } + query_parameters: { + q: string + } + } + `, + errors: [ + { + messageId: "missingUrls", + data: { + interfaceName: "SearchRequest", + }, + }, + ], + }, + ], +}); From fc8fcc3e1e1bd1ac3880bf8e763ccddee4bed5e5 Mon Sep 17 00:00:00 2001 From: margaretjgu Date: Thu, 6 Nov 2025 11:29:44 -0500 Subject: [PATCH 2/4] undo prettier --- validator/eslint-plugin-es-spec.js | 28 ++++++------ validator/rules/request-must-have-urls.js | 36 +++++++-------- validator/test/request-must-have-urls.test.js | 45 +++++++++---------- 3 files changed, 52 insertions(+), 57 deletions(-) diff --git a/validator/eslint-plugin-es-spec.js b/validator/eslint-plugin-es-spec.js index d01be4ed79..c56e8ace14 100644 --- a/validator/eslint-plugin-es-spec.js +++ b/validator/eslint-plugin-es-spec.js @@ -16,20 +16,20 @@ * specific language governing permissions and limitations * under the License. */ -import singleKeyDict from "./rules/single-key-dictionary-key-is-string.js"; -import dict from "./rules/dictionary-key-is-string.js"; -import noNativeTypes from "./rules/no-native-types.js"; -import invalidNodeTypes from "./rules/invalid-node-types.js"; -import noGenericNumber from "./rules/no-generic-number.js"; -import requestMustHaveUrls from "./rules/request-must-have-urls.js"; +import singleKeyDict from './rules/single-key-dictionary-key-is-string.js' +import dict from './rules/dictionary-key-is-string.js' +import noNativeTypes from './rules/no-native-types.js' +import invalidNodeTypes from './rules/invalid-node-types.js' +import noGenericNumber from './rules/no-generic-number.js' +import requestMustHaveUrls from './rules/request-must-have-urls.js' export default { rules: { - "single-key-dictionary-key-is-string": singleKeyDict, - "dictionary-key-is-string": dict, - "no-native-types": noNativeTypes, - "invalid-node-types": invalidNodeTypes, - "no-generic-number": noGenericNumber, - "request-must-have-urls": requestMustHaveUrls, - }, -}; + 'single-key-dictionary-key-is-string': singleKeyDict, + 'dictionary-key-is-string': dict, + 'no-native-types': noNativeTypes, + 'invalid-node-types': invalidNodeTypes, + 'no-generic-number': noGenericNumber, + 'request-must-have-urls': requestMustHaveUrls, + } +} diff --git a/validator/rules/request-must-have-urls.js b/validator/rules/request-must-have-urls.js index c244f563df..f2e624cdd5 100644 --- a/validator/rules/request-must-have-urls.js +++ b/validator/rules/request-must-have-urls.js @@ -16,24 +16,22 @@ * specific language governing permissions and limitations * under the License. */ -import { ESLintUtils } from "@typescript-eslint/utils"; +import { ESLintUtils } from '@typescript-eslint/utils'; -const createRule = ESLintUtils.RuleCreator( - (name) => `https://example.com/rule/${name}`, -); +const createRule = ESLintUtils.RuleCreator(name => `https://example.com/rule/${name}`) export default createRule({ - name: "request-must-have-urls", + name: 'request-must-have-urls', create(context) { return { TSInterfaceDeclaration(node) { const extendsRequestBase = node.extends?.some( (heritage) => - heritage.expression.type === "Identifier" && - heritage.expression.name === "RequestBase", + heritage.expression.type === 'Identifier' && + heritage.expression.name === 'RequestBase' ); - const isNamedRequest = node.id.name === "Request"; + const isNamedRequest = node.id.name === 'Request'; if (!extendsRequestBase && !isNamedRequest) { return; @@ -41,33 +39,31 @@ export default createRule({ const hasUrls = node.body.body.some( (member) => - member.type === "TSPropertySignature" && - member.key.type === "Identifier" && - member.key.name === "urls", + member.type === 'TSPropertySignature' && + member.key.type === 'Identifier' && + member.key.name === 'urls' ); if (!hasUrls) { context.report({ node, - messageId: "missingUrls", + messageId: 'missingUrls', data: { interfaceName: node.id.name, }, }); } }, - }; + } }, meta: { docs: { - description: - "All Request interfaces must have a urls property defining their endpoints", + description: 'All Request interfaces must have a urls property defining their endpoints', }, messages: { - missingUrls: - 'Request interface "{{interfaceName}}" must have a urls property with endpoint paths and methods', + missingUrls: 'Request interface "{{interfaceName}}" must have a urls property with endpoint paths and methods' }, - type: "problem", + type: 'problem', }, - defaultOptions: [], -}); + defaultOptions: [] +}) diff --git a/validator/test/request-must-have-urls.test.js b/validator/test/request-must-have-urls.test.js index 4474939e7b..f5b920fb1d 100644 --- a/validator/test/request-must-have-urls.test.js +++ b/validator/test/request-must-have-urls.test.js @@ -17,26 +17,25 @@ * under the License. */ -import { RuleTester } from "@typescript-eslint/rule-tester"; -import rule from "../rules/request-must-have-urls.js"; +import { RuleTester } from '@typescript-eslint/rule-tester'; +import rule from '../rules/request-must-have-urls.js'; const ruleTester = new RuleTester({ languageOptions: { parserOptions: { projectService: { - allowDefaultProject: ["*.ts*"], - defaultProject: "tsconfig.json", + allowDefaultProject: ['*.ts*'], + defaultProject: 'tsconfig.json', }, - tsconfigRootDir: new URL("../../specification/", import.meta.url) - .pathname, + tsconfigRootDir: new URL('../../specification/', import.meta.url).pathname, }, }, }); -ruleTester.run("request-must-have-urls", rule, { +ruleTester.run('request-must-have-urls', rule, { valid: [ { - name: "Request with urls property", + name: 'Request with urls property', code: ` interface RequestBase {} export interface Request extends RequestBase { @@ -53,7 +52,7 @@ ruleTester.run("request-must-have-urls", rule, { `, }, { - name: "Request with multiple URL patterns", + name: 'Request with multiple URL patterns', code: ` interface RequestBase {} export interface Request extends RequestBase { @@ -77,7 +76,7 @@ ruleTester.run("request-must-have-urls", rule, { `, }, { - name: "Non-Request interface without urls", + name: 'Non-Request interface without urls', code: ` export interface ResponseBody { took: number @@ -86,7 +85,7 @@ ruleTester.run("request-must-have-urls", rule, { `, }, { - name: "Response interface without urls", + name: 'Response interface without urls', code: ` interface RequestBase {} export interface Response { @@ -99,7 +98,7 @@ ruleTester.run("request-must-have-urls", rule, { ], invalid: [ { - name: "Request without urls property", + name: 'Request without urls property', code: ` interface RequestBase {} export interface Request extends RequestBase { @@ -110,15 +109,15 @@ ruleTester.run("request-must-have-urls", rule, { `, errors: [ { - messageId: "missingUrls", + messageId: 'missingUrls', data: { - interfaceName: "Request", - }, + interfaceName: 'Request' + } }, ], }, { - name: "Named Request without urls", + name: 'Named Request without urls', code: ` export interface Request { path_parts: { @@ -131,15 +130,15 @@ ruleTester.run("request-must-have-urls", rule, { `, errors: [ { - messageId: "missingUrls", + messageId: 'missingUrls', data: { - interfaceName: "Request", - }, + interfaceName: 'Request' + } }, ], }, { - name: "Request extending RequestBase without urls", + name: 'Request extending RequestBase without urls', code: ` interface RequestBase {} export interface SearchRequest extends RequestBase { @@ -153,10 +152,10 @@ ruleTester.run("request-must-have-urls", rule, { `, errors: [ { - messageId: "missingUrls", + messageId: 'missingUrls', data: { - interfaceName: "SearchRequest", - }, + interfaceName: 'SearchRequest' + } }, ], }, From e75eeb6d98b640d8cbb7d82db64833fb78a2ff69 Mon Sep 17 00:00:00 2001 From: margaretjgu Date: Thu, 6 Nov 2025 15:05:47 -0500 Subject: [PATCH 3/4] async query urls --- specification/esql/async_query/AsyncQueryRequest.ts | 6 ++++++ .../esql/async_query_delete/AsyncQueryDeleteRequest.ts | 6 ++++++ specification/esql/async_query_get/AsyncQueryGetRequest.ts | 6 ++++++ 3 files changed, 18 insertions(+) diff --git a/specification/esql/async_query/AsyncQueryRequest.ts b/specification/esql/async_query/AsyncQueryRequest.ts index 4118ebaf3d..020c182ef3 100644 --- a/specification/esql/async_query/AsyncQueryRequest.ts +++ b/specification/esql/async_query/AsyncQueryRequest.ts @@ -37,6 +37,12 @@ import { Dictionary } from '@spec_utils/Dictionary' * @index_privileges read */ export interface Request extends RequestBase { + urls: [ + { + path: '/_query/async' + methods: ['POST'] + } + ] query_parameters: { /** * If `true`, partial results will be returned if there are shard failures, but the query can continue to execute on other clusters and shards. diff --git a/specification/esql/async_query_delete/AsyncQueryDeleteRequest.ts b/specification/esql/async_query_delete/AsyncQueryDeleteRequest.ts index 9a78be3a87..cdc742a763 100644 --- a/specification/esql/async_query_delete/AsyncQueryDeleteRequest.ts +++ b/specification/esql/async_query_delete/AsyncQueryDeleteRequest.ts @@ -35,6 +35,12 @@ import { Id } from '@_types/common' * @ext_doc_id esql */ export interface Request extends RequestBase { + urls: [ + { + path: '/_query/async/{id}' + methods: ['DELETE'] + } + ] path_parts: { /** * The unique identifier of the query. diff --git a/specification/esql/async_query_get/AsyncQueryGetRequest.ts b/specification/esql/async_query_get/AsyncQueryGetRequest.ts index c7de13e57e..3c96b7aaaf 100644 --- a/specification/esql/async_query_get/AsyncQueryGetRequest.ts +++ b/specification/esql/async_query_get/AsyncQueryGetRequest.ts @@ -32,6 +32,12 @@ import { EsqlFormat } from '@esql/_types/QueryParameters' * @ext_doc_id esql */ export interface Request extends RequestBase { + urls: [ + { + path: '/_query/async/{id}' + methods: ['GET'] + } + ] path_parts: { /** * The unique identifier of the query. From 134503c9eda651782953114b76745f74c9ced9ab Mon Sep 17 00:00:00 2001 From: margaretjgu Date: Thu, 6 Nov 2025 15:19:02 -0500 Subject: [PATCH 4/4] fix violations --- .../esql/async_query_stop/AsyncQueryStopRequest.ts | 6 ++++++ .../cancel_migrate_reindex/MigrateCancelReindexRequest.ts | 6 ++++++ .../indices/create_from/MigrateCreateFromRequest.ts | 6 ++++++ .../MigrateGetReindexStatusRequest.ts | 6 ++++++ .../indices/migrate_reindex/MigrateReindexRequest.ts | 6 ++++++ .../repository_analyze/SnapshotAnalyzeRepositoryRequest.ts | 6 ++++++ 6 files changed, 36 insertions(+) diff --git a/specification/esql/async_query_stop/AsyncQueryStopRequest.ts b/specification/esql/async_query_stop/AsyncQueryStopRequest.ts index f7e975e956..caca864d0d 100644 --- a/specification/esql/async_query_stop/AsyncQueryStopRequest.ts +++ b/specification/esql/async_query_stop/AsyncQueryStopRequest.ts @@ -31,6 +31,12 @@ import { Id } from '@_types/common' * @ext_doc_id esql */ export interface Request extends RequestBase { + urls: [ + { + path: '/_query/async/{id}/stop' + methods: ['POST'] + } + ] path_parts: { /** * The unique identifier of the query. diff --git a/specification/indices/cancel_migrate_reindex/MigrateCancelReindexRequest.ts b/specification/indices/cancel_migrate_reindex/MigrateCancelReindexRequest.ts index 7a1f17dcf8..ab5d3fd207 100644 --- a/specification/indices/cancel_migrate_reindex/MigrateCancelReindexRequest.ts +++ b/specification/indices/cancel_migrate_reindex/MigrateCancelReindexRequest.ts @@ -31,6 +31,12 @@ import { Indices } from '@_types/common' * @doc_tag migration */ export interface Request extends RequestBase { + urls: [ + { + path: '/_migration/reindex/{index}/_cancel' + methods: ['POST'] + } + ] path_parts: { /** The index or data stream name */ index: Indices diff --git a/specification/indices/create_from/MigrateCreateFromRequest.ts b/specification/indices/create_from/MigrateCreateFromRequest.ts index 27a46ab5b3..5d8b2cfbda 100644 --- a/specification/indices/create_from/MigrateCreateFromRequest.ts +++ b/specification/indices/create_from/MigrateCreateFromRequest.ts @@ -33,6 +33,12 @@ import { IndexSettings } from '@indices/_types/IndexSettings' * @doc_tag migration */ export interface Request extends RequestBase { + urls: [ + { + path: '/_create_from/{source}/{dest}' + methods: ['PUT', 'POST'] + } + ] path_parts: { /** The source index or data stream name */ source: IndexName diff --git a/specification/indices/get_migrate_reindex_status/MigrateGetReindexStatusRequest.ts b/specification/indices/get_migrate_reindex_status/MigrateGetReindexStatusRequest.ts index 582de89597..6783803207 100644 --- a/specification/indices/get_migrate_reindex_status/MigrateGetReindexStatusRequest.ts +++ b/specification/indices/get_migrate_reindex_status/MigrateGetReindexStatusRequest.ts @@ -31,6 +31,12 @@ import { Indices } from '@_types/common' * @doc_tag migration */ export interface Request extends RequestBase { + urls: [ + { + path: '/_migration/reindex/{index}/_status' + methods: ['GET'] + } + ] path_parts: { /** The index or data stream name. */ index: Indices diff --git a/specification/indices/migrate_reindex/MigrateReindexRequest.ts b/specification/indices/migrate_reindex/MigrateReindexRequest.ts index dd9231ea88..3031840f15 100644 --- a/specification/indices/migrate_reindex/MigrateReindexRequest.ts +++ b/specification/indices/migrate_reindex/MigrateReindexRequest.ts @@ -32,6 +32,12 @@ import { IndexName } from '@_types/common' * @doc_tag migration */ export interface Request extends RequestBase { + urls: [ + { + path: '/_migration/reindex' + methods: ['POST'] + } + ] /** @codegen_name reindex */ body: MigrateReindex } diff --git a/specification/snapshot/repository_analyze/SnapshotAnalyzeRepositoryRequest.ts b/specification/snapshot/repository_analyze/SnapshotAnalyzeRepositoryRequest.ts index 377a6995e5..58fb95d6a2 100644 --- a/specification/snapshot/repository_analyze/SnapshotAnalyzeRepositoryRequest.ts +++ b/specification/snapshot/repository_analyze/SnapshotAnalyzeRepositoryRequest.ts @@ -133,6 +133,12 @@ import { Duration } from '@_types/Time' * @doc_id analyze-repository */ export interface Request extends RequestBase { + urls: [ + { + path: '/_snapshot/{repository}/_analyze' + methods: ['POST'] + } + ] path_parts: { /** * The name of the repository.