Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
7a3999b
psuedocode
TinaHeiligers Jan 16, 2026
fc4b46d
componentize sketch with initial tests
TinaHeiligers Jan 22, 2026
0843d9c
Create util functions for easier testing
TinaHeiligers Jan 21, 2026
cf6ce2a
process all objects and convert to components with correct name
TinaHeiligers Jan 21, 2026
720d808
Process in temp file, extract schemas first, then recursively extract…
TinaHeiligers Jan 22, 2026
db2856a
Reset main output yaml files
TinaHeiligers Jan 22, 2026
a9d5e52
Revert api_docs changes from componentize sketch commit
TinaHeiligers Jan 22, 2026
6a1790d
Address feedback 1
TinaHeiligers Jan 22, 2026
670e11c
deduplicate docs
TinaHeiligers Jan 22, 2026
e0ba544
Merge branch 'main' into kbn24462_componentize_oas
TinaHeiligers Jan 22, 2026
e1f647d
refactor tests
TinaHeiligers Jan 22, 2026
c80146a
Extract all objects with structural fields including properties, addi…
TinaHeiligers Jan 23, 2026
f80ed68
Implements a component name generator to handle pre-existing components
TinaHeiligers Jan 23, 2026
75698ee
Changes from make api-docs
kibanamachine Jan 23, 2026
16f297a
(jl) refactor test componentize test strategy
jloleysens Jan 23, 2026
f9df069
change endpoint for connectors
TinaHeiligers Jan 25, 2026
bc7c5d0
skips tests known to fail from incorrect assumptions
TinaHeiligers Jan 26, 2026
840af7d
Update componentization_strategies.md
TinaHeiligers Jan 26, 2026
6cf2fc6
(jl) do not expect primitives to be extracted
jloleysens Jan 26, 2026
f3c1432
Extract empty objects
TinaHeiligers Jan 27, 2026
081e70f
Changes from make api-docs
kibanamachine Jan 27, 2026
9ca9296
Merge branch 'kbn24462_componentize_oas' of github.com:TinaHeiligers/…
TinaHeiligers Jan 28, 2026
f96475d
Extract empty objects, move all shared const to single file
TinaHeiligers Jan 27, 2026
780a78c
deletes duplicate file
TinaHeiligers Jan 28, 2026
22522b0
Changes from make api-docs
kibanamachine Jan 28, 2026
d3b4e0f
code cleanup with reusable functions
TinaHeiligers Jan 28, 2026
8cd3bde
Merge branch 'kbn24462_componentize_oas' of github.com:TinaHeiligers/…
TinaHeiligers Jan 29, 2026
b8087da
more cleanup
TinaHeiligers Jan 29, 2026
0415a4e
get main yml
TinaHeiligers Feb 2, 2026
cc2cc63
update make command
TinaHeiligers Feb 2, 2026
dd24cc2
Run component extraction
TinaHeiligers Feb 2, 2026
3240cad
Changes from yarn openapi:bundle
kibanamachine Feb 2, 2026
3269917
standardize logging to tooling log
TinaHeiligers Feb 2, 2026
d071831
fix logging
TinaHeiligers Feb 2, 2026
02b13e2
Changes from make api-docs
kibanamachine Feb 2, 2026
ded1faf
Merge branch 'main' into kbn24462_componentize_oas
TinaHeiligers Feb 3, 2026
4f7848a
fix extract refs
TinaHeiligers Feb 5, 2026
350753a
Prevent circular references
TinaHeiligers Feb 5, 2026
9bdea38
fix test
TinaHeiligers Feb 5, 2026
a6d8ed8
Merge upstream/main - accept upstream version of generated YAML files
TinaHeiligers Feb 5, 2026
c160bfb
update makefile, resolve conflicts in oas outputs
TinaHeiligers Feb 5, 2026
fa4139d
Changes from make api-docs
kibanamachine Feb 5, 2026
104fc4e
Allow script to continue after auto-commit, following the same patter…
TinaHeiligers Feb 6, 2026
7296baa
Merge branch 'kbn24462_componentize_oas' of github.com:TinaHeiligers/…
TinaHeiligers Feb 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .buildkite/scripts/steps/openapi_bundling/final_merge.sh
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,4 @@ make api-docs

cd "$cur_dir"

check_for_changed_files "make api-docs" true
check_for_changed_files "make api-docs" true || true
6 changes: 5 additions & 1 deletion oas_docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,11 @@ Append pre-existing bundles not extracted from code using [`kbn-openapi-bundler`

To add more files into the final bundle, edit the appropriate `oas_docs/scripts/merge*.js` files.

### Step 2
### Step 3

Convert inline schemas to component references that follow a specific naming pattern. See the ["Scripts"](#scripts) section for more details.

### Step 3

Apply any final overalys to the document that might include examples or final tweaks (see the ["Scripts"](#scripts) section for more details).

Expand Down
14 changes: 14 additions & 0 deletions oas_docs/jest.config.js
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'],
};
163 changes: 163 additions & 0 deletions oas_docs/lib/component_name_generator.js
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) {
Comment thread
jloleysens marked this conversation as resolved.
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 };
131 changes: 131 additions & 0 deletions oas_docs/lib/component_name_generator.test.js
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');
});
});
Loading