-
Notifications
You must be signed in to change notification settings - Fork 8.6k
[OAS] extract schemas as referenced components #249978
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
TinaHeiligers
wants to merge
44
commits into
elastic:main
from
TinaHeiligers:kbn24462_componentize_oas
Closed
Changes from all commits
Commits
Show all changes
44 commits
Select commit
Hold shift + click to select a range
7a3999b
psuedocode
TinaHeiligers fc4b46d
componentize sketch with initial tests
TinaHeiligers 0843d9c
Create util functions for easier testing
TinaHeiligers cf6ce2a
process all objects and convert to components with correct name
TinaHeiligers 720d808
Process in temp file, extract schemas first, then recursively extract…
TinaHeiligers db2856a
Reset main output yaml files
TinaHeiligers a9d5e52
Revert api_docs changes from componentize sketch commit
TinaHeiligers 6a1790d
Address feedback 1
TinaHeiligers 670e11c
deduplicate docs
TinaHeiligers e0ba544
Merge branch 'main' into kbn24462_componentize_oas
TinaHeiligers e1f647d
refactor tests
TinaHeiligers c80146a
Extract all objects with structural fields including properties, addi…
TinaHeiligers f80ed68
Implements a component name generator to handle pre-existing components
TinaHeiligers 75698ee
Changes from make api-docs
kibanamachine 16f297a
(jl) refactor test componentize test strategy
jloleysens f9df069
change endpoint for connectors
TinaHeiligers bc7c5d0
skips tests known to fail from incorrect assumptions
TinaHeiligers 840af7d
Update componentization_strategies.md
TinaHeiligers 6cf2fc6
(jl) do not expect primitives to be extracted
jloleysens f3c1432
Extract empty objects
TinaHeiligers 081e70f
Changes from make api-docs
kibanamachine 9ca9296
Merge branch 'kbn24462_componentize_oas' of github.com:TinaHeiligers/…
TinaHeiligers f96475d
Extract empty objects, move all shared const to single file
TinaHeiligers 780a78c
deletes duplicate file
TinaHeiligers 22522b0
Changes from make api-docs
kibanamachine d3b4e0f
code cleanup with reusable functions
TinaHeiligers 8cd3bde
Merge branch 'kbn24462_componentize_oas' of github.com:TinaHeiligers/…
TinaHeiligers b8087da
more cleanup
TinaHeiligers 0415a4e
get main yml
TinaHeiligers cc2cc63
update make command
TinaHeiligers dd24cc2
Run component extraction
TinaHeiligers 3240cad
Changes from yarn openapi:bundle
kibanamachine 3269917
standardize logging to tooling log
TinaHeiligers d071831
fix logging
TinaHeiligers 02b13e2
Changes from make api-docs
kibanamachine ded1faf
Merge branch 'main' into kbn24462_componentize_oas
TinaHeiligers 4f7848a
fix extract refs
TinaHeiligers 350753a
Prevent circular references
TinaHeiligers 9bdea38
fix test
TinaHeiligers a6d8ed8
Merge upstream/main - accept upstream version of generated YAML files
TinaHeiligers c160bfb
update makefile, resolve conflicts in oas outputs
TinaHeiligers fa4139d
Changes from make api-docs
kibanamachine 104fc4e
Allow script to continue after auto-commit, following the same patter…
TinaHeiligers 7296baa
Merge branch 'kbn24462_componentize_oas' of github.com:TinaHeiligers/…
TinaHeiligers File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| /* | ||
| * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
| * or more contributor license agreements. Licensed under the "Elastic License | ||
| * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side | ||
| * Public License v 1"; you may not use this file except in compliance with, at | ||
| * your election, the "Elastic License 2.0", the "GNU Affero General Public | ||
| * License v3.0 only", or the "Server Side Public License, v 1". | ||
| */ | ||
|
|
||
| module.exports = { | ||
| preset: '@kbn/test/jest_node', | ||
| rootDir: '../', | ||
| roots: ['<rootDir>/oas_docs'], | ||
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,163 @@ | ||
| /* | ||
| * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
| * or more contributor license agreements. Licensed under the "Elastic License | ||
| * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side | ||
| * Public License v 1"; you may not use this file except in compliance with, at | ||
| * your election, the "Elastic License 2.0", the "GNU Affero General Public | ||
| * License v3.0 only", or the "Server Side Public License, v 1". | ||
| */ | ||
| const cleanAndNormalizePath = (pathStr) => { | ||
| return pathStr | ||
| .trim() | ||
| .replace(/^[\/]+/, '') // remove leading slashes | ||
| .replace(/[\/]+$/, '') // remove trailing slashes | ||
| .replace(/^(internal\/api\/|internal\/|api\/)/, '') // remove api prefixes | ||
| .replace(/\{[^}]*\}/g, '') // remove path parameters like {id}, {rule_id} | ||
| .replace(/[\?\*]/g, '') // remove ? * | ||
| .replace(/[\/\_\-]+/g, '/') // normalize separators and collapse multiple | ||
| .replace(/\/_/g, '/') // remove leading underscores after slash | ||
| .replace(/^_+|_+$/g, '') // remove leading/trailing underscores | ||
| .split('/') | ||
| .filter((segment) => segment.length > 0 && segment !== '_') | ||
| .map((segment) => { | ||
| // Convert segment to PascalCase | ||
| return segment | ||
| .split(/[\-\_]/) // split on hyphens and underscores | ||
| .filter((word) => word.length > 0) // remove empty words | ||
| .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) | ||
| .join(''); | ||
| }) | ||
| .join(''); | ||
| }; | ||
|
|
||
| function toPascalCase(str) { | ||
| return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase(); | ||
| } | ||
|
|
||
| function fromPropertyPaths(propertyPath) { | ||
| return propertyPath.map((p) => p.charAt(0).toUpperCase() + p.slice(1)); | ||
| } | ||
|
|
||
| /** | ||
| * Creates a component name generator with collision detection. | ||
| * | ||
| * Naming Strategy: | ||
| * - Converts API paths to PascalCase (e.g., /api/actions/connector -> ApiActionsConnector) | ||
| * - Removes path parameters ({id}, {rule_id}) while preserving meaningful segments | ||
| * - Adds HTTP method in PascalCase (Get, Post, Put, etc.) | ||
| * - Adds "Request" or "Response" based on context | ||
| * - Includes response codes (200, 404, etc.) | ||
| * - Handles nested property paths for detailed naming | ||
| * - Adds 1-based indexing for composition types (oneOf/anyOf/allOf) | ||
| * - Ensures uniqueness by appending numeric suffixes on collision | ||
| * | ||
| * @returns {Function} generateName function | ||
| * | ||
| * @example | ||
| * const nameGen = createComponentNameGenerator(); | ||
| * | ||
| * // Basic response schema | ||
| * nameGen({ method: 'get', path: '/api/actions/connector/{id}', isRequest: false, responseCode: '200' }) | ||
| * // => 'ApiActionsConnector_Get_Response_200' | ||
| * | ||
| * // OneOf/AnyOf indexing | ||
| * nameGen({ method: 'get', path: '/api/actions/connector', isRequest: false, responseCode: '200' }, 'oneOf', 0) | ||
| * // => 'ApiActionsConnector_Get_Response_200_1' | ||
| * | ||
| * // Property schemas | ||
| * nameGen({ method: 'get', path: '/api/actions/connector/{id}', isRequest: false, responseCode: '200', propertyPath: ['config'] }, 'property') | ||
| * // => 'ApiActionsConnector_Get_Response_200_Config' | ||
| * | ||
| * // Complex paths with parameters and underscores | ||
| * nameGen({ method: 'get', path: '/api/alerting/rule/{rule_id}/alert/{alert_id}/_unmute', isRequest: false, responseCode: '200' }) | ||
| * // => 'ApiAlertingRuleAlertUnmute_Get_Response_200' | ||
| * | ||
| * // Existing components with nested schemas (from overlays) | ||
| * nameGen({ parentComponentName: 'BedRockConfig', propertyPath: ['apiKey'], isRequest: undefined }, 'property') | ||
| * // => 'BedRockConfig_ApiKey' | ||
| */ | ||
| const createComponentNameGenerator = () => { | ||
| const nameMap = new Map(); | ||
| /** | ||
| * Generates a unique component name based on context and composition type. | ||
| * | ||
| * @param {Object} context - Contextual information for naming | ||
| * @param {string|null} context.method - HTTP method (get, post, etc.) or null for components | ||
| * @param {string|null} context.path - API path (/api/test) or null for components | ||
| * @param {boolean|undefined} context.isRequest - true for request, false for response, undefined for components | ||
| * @param {string|null} context.responseCode - HTTP response code (200, 404, etc.) or null | ||
| * @param {Array<string>} [context.propertyPath=[]] - Path of nested properties | ||
| * @param {string|null} [context.parentComponentName=null] - Parent component name for existing components | ||
| * @param {string} [compositionType] - Type: 'oneOf', 'anyOf', 'allOf', 'property', 'arrayItem', 'additionalProperty' | ||
| * @param {number} [index] - Index for composition types (0-based, converted to 1-based in name) | ||
| * @returns {string} Generated unique component name | ||
| */ | ||
| return function generateName(context, compositionType, index) { | ||
| const { | ||
| method, | ||
| path, | ||
| isRequest, | ||
| responseCode, | ||
| propertyPath = [], | ||
| parentComponentName = null, | ||
| } = context; | ||
|
|
||
| // Convert path to PascalCase API name | ||
| const buildApiName = (pathStr) => { | ||
| if (!pathStr) return ''; | ||
| // Clean and normalize path | ||
| const cleanPath = cleanAndNormalizePath(pathStr); | ||
| return 'Api' + cleanPath; | ||
| }; | ||
|
|
||
| const parts = []; | ||
| // Use parent component name as prefix for nested schemas in existing components | ||
| if (parentComponentName) { | ||
| parts.push(parentComponentName); | ||
| } else { | ||
| parts.push(buildApiName(path)); | ||
| } | ||
| if (method) { | ||
| parts.push(toPascalCase(method)); | ||
| } | ||
| // Add request/response type | ||
| if (isRequest !== undefined) { | ||
| parts.push(isRequest ? 'Request' : 'Response'); | ||
| } | ||
|
|
||
| // Add response code | ||
| if (responseCode) { | ||
| parts.push(responseCode); | ||
| } | ||
|
|
||
| // Add property path (for nested objects) | ||
| if (propertyPath && propertyPath.length > 0) { | ||
| parts.push(...fromPropertyPaths(propertyPath)); | ||
| } | ||
|
|
||
| // Add composition type suffixes | ||
| if (compositionType === 'property') { | ||
| // Property objects already have their name in propertyPath, nothing to append | ||
| } else if (compositionType === 'arrayItem') { | ||
| parts.push('Item'); | ||
| } else if (compositionType === 'additionalProperty') { | ||
| parts.push('Value'); | ||
| } else if (compositionType && index !== undefined) { | ||
| // For oneOf, anyOf, allOf - add 1-based index | ||
| parts.push(`${index + 1}`); | ||
| } | ||
|
|
||
| let name = parts.filter(Boolean).join('_'); | ||
|
|
||
| // Ensure uniqueness | ||
| const cachedCount = nameMap.get(name) ?? 0; | ||
| nameMap.set(name, cachedCount + 1); | ||
| if (cachedCount > 0) { | ||
| name = `${name}_${cachedCount}`; | ||
| } | ||
|
|
||
| return name; | ||
| }; | ||
| }; | ||
|
|
||
| module.exports = { createComponentNameGenerator }; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,131 @@ | ||
| /* | ||
| * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
| * or more contributor license agreements. Licensed under the "Elastic License | ||
| * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side | ||
| * Public License v 1"; you may not use this file except in compliance with, at | ||
| * your election, the "Elastic License 2.0", the "GNU Affero General Public | ||
| * License v3.0 only", or the "Server Side Public License, v 1". | ||
| */ | ||
| const { createComponentNameGenerator } = require('./component_name_generator'); | ||
|
|
||
| describe('createComponentNameGenerator', () => { | ||
| let nameGen; | ||
| beforeEach(() => { | ||
| nameGen = createComponentNameGenerator(); | ||
| }); | ||
|
|
||
| test('generates response schema name for /api/foo/connector/{id} GET 200', () => { | ||
| const context = { | ||
| method: 'get', | ||
| path: '/api/actions/connector/{id}', | ||
| isRequest: false, | ||
| responseCode: '200', | ||
| }; | ||
| const name = nameGen(context); | ||
| expect(name).toBe('ApiActionsConnector_Get_Response_200'); | ||
| }); | ||
|
|
||
| test('generates indexed oneOf names for /api/actions/connector GET 200', () => { | ||
| const context = { | ||
| method: 'get', | ||
| path: '/api/actions/connector', | ||
| isRequest: false, | ||
| responseCode: '200', | ||
| }; | ||
| const names = []; | ||
| for (let i = 0; i < 3; i++) { | ||
| names.push(nameGen(context, 'oneOf', i)); | ||
| } | ||
| names.forEach((name, index) => { | ||
| expect(name).toBe(`ApiActionsConnector_Get_Response_200_${index + 1}`); | ||
| }); | ||
| }); | ||
|
|
||
| test('generates property schema names', () => { | ||
| const context = { | ||
| method: 'get', | ||
| path: '/api/actions/connector/{id}', | ||
| isRequest: false, | ||
| responseCode: '200', | ||
| propertyPath: ['config'], | ||
| }; | ||
| expect(nameGen(context, 'property')).toBe('ApiActionsConnector_Get_Response_200_Config'); | ||
| }); | ||
|
|
||
| test('generates unique names for duplicate contexts', () => { | ||
| const context = { | ||
| method: 'post', | ||
| path: '/api/test', | ||
| isRequest: true, | ||
| }; | ||
| expect(nameGen(context)).toBe('ApiTest_Post_Request'); | ||
| expect(nameGen(context)).toBe('ApiTest_Post_Request_1'); | ||
| }); | ||
|
|
||
| test('derives operation ID from path when not provided', () => { | ||
| const context = { | ||
| method: 'get', | ||
| path: '/api/alerting/rule/{rule_id}/alert/{alert_id}/_unmute', | ||
| isRequest: false, | ||
| responseCode: '200', | ||
| }; | ||
| expect(nameGen(context)).toBe('ApiAlertingRuleAlertUnmute_Get_Response_200'); | ||
| }); | ||
|
|
||
| test('handles complex path with multiple parameters and underscores', () => { | ||
| const context = { | ||
| method: 'post', | ||
| path: '/api/security/role/{role_name}/field_security/{field_name}', | ||
| isRequest: true, | ||
| }; | ||
| expect(nameGen(context)).toBe('ApiSecurityRoleFieldSecurity_Post_Request'); | ||
| }); | ||
|
|
||
| test('handles array item composition type', () => { | ||
| const context = { | ||
| method: 'get', | ||
| path: '/api/cases', | ||
| isRequest: false, | ||
| responseCode: '200', | ||
| propertyPath: ['items'], | ||
| }; | ||
| const name = nameGen(context, 'arrayItem'); | ||
| expect(name).toBe('ApiCases_Get_Response_200_Items_Item'); | ||
| }); | ||
|
|
||
| test('handles additional properties composition type', () => { | ||
| const context = { | ||
| method: 'get', | ||
| path: '/api/dashboard/{id}', | ||
| isRequest: false, | ||
| responseCode: '200', | ||
| propertyPath: ['metadata'], | ||
| }; | ||
| const name = nameGen(context, 'additionalProperty'); | ||
| expect(name).toBe('ApiDashboard_Get_Response_200_Metadata_Value'); | ||
| }); | ||
|
|
||
| test('handles allOf composition type with index', () => { | ||
| const context = { | ||
| method: 'patch', | ||
| path: '/api/fleet/agents', | ||
| isRequest: true, | ||
| }; | ||
| const name1 = nameGen(context, 'allOf', 0); | ||
| const name2 = nameGen(context, 'allOf', 1); | ||
| expect(name1).toBe('ApiFleetAgents_Patch_Request_1'); | ||
| expect(name2).toBe('ApiFleetAgents_Patch_Request_2'); | ||
| }); | ||
|
|
||
| test('handles nested property paths', () => { | ||
| const context = { | ||
| method: 'put', | ||
| path: '/api/saved_objects/{type}/{id}', | ||
| isRequest: false, | ||
| responseCode: '200', | ||
| propertyPath: ['attributes', 'visualization', 'visState'], | ||
| }; | ||
| const name = nameGen(context, 'property'); | ||
| expect(name).toBe('ApiSavedObjects_Put_Response_200_Attributes_Visualization_VisState'); | ||
| }); | ||
| }); |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.