1422 lines
40 KiB
TypeScript
1422 lines
40 KiB
TypeScript
/**
|
|
* OpenAPI to Hugo Data Converter
|
|
*
|
|
* Converts OpenAPI v3 specifications into Hugo-compatible data files.
|
|
* Generates both YAML and JSON versions of spec fragments grouped by path.
|
|
*
|
|
* @module openapi-paths-to-hugo-data
|
|
*/
|
|
|
|
import * as yaml from 'js-yaml';
|
|
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
|
|
/**
|
|
* Related link with title and href (from x-influxdata-related)
|
|
*/
|
|
interface RelatedLink {
|
|
title: string;
|
|
href: string;
|
|
}
|
|
|
|
/**
|
|
* OpenAPI path item object
|
|
*/
|
|
interface PathItem {
|
|
get?: Operation;
|
|
post?: Operation;
|
|
put?: Operation;
|
|
patch?: Operation;
|
|
delete?: Operation;
|
|
options?: Operation;
|
|
head?: Operation;
|
|
trace?: Operation;
|
|
parameters?: Parameter[];
|
|
[key: string]: unknown;
|
|
}
|
|
|
|
/**
|
|
* OpenAPI operation object
|
|
*/
|
|
interface Operation {
|
|
operationId?: string;
|
|
summary?: string;
|
|
description?: string;
|
|
tags?: string[];
|
|
parameters?: Parameter[];
|
|
requestBody?: RequestBody;
|
|
responses?: Record<string, Response>;
|
|
externalDocs?: ExternalDocs;
|
|
/** Compatibility version for migration context (v1 or v2) */
|
|
'x-compatibility-version'?: string;
|
|
/** Related documentation links as plain URLs */
|
|
'x-influxdatadocs-related'?: string[];
|
|
/** Related documentation links with title and href */
|
|
'x-related'?: RelatedLink[];
|
|
[key: string]: unknown;
|
|
}
|
|
|
|
/**
|
|
* OpenAPI parameter object
|
|
*/
|
|
interface Parameter {
|
|
name: string;
|
|
in: 'query' | 'header' | 'path' | 'cookie';
|
|
description?: string;
|
|
required?: boolean;
|
|
schema?: Schema;
|
|
[key: string]: unknown;
|
|
}
|
|
|
|
/**
|
|
* OpenAPI request body object
|
|
*/
|
|
interface RequestBody {
|
|
description?: string;
|
|
content?: Record<string, MediaType>;
|
|
required?: boolean;
|
|
[key: string]: unknown;
|
|
}
|
|
|
|
/**
|
|
* OpenAPI response object
|
|
*/
|
|
interface Response {
|
|
description: string;
|
|
content?: Record<string, MediaType>;
|
|
headers?: Record<string, Header>;
|
|
[key: string]: unknown;
|
|
}
|
|
|
|
/**
|
|
* OpenAPI media type object
|
|
*/
|
|
interface MediaType {
|
|
schema?: Schema;
|
|
example?: unknown;
|
|
examples?: Record<string, Example>;
|
|
[key: string]: unknown;
|
|
}
|
|
|
|
/**
|
|
* OpenAPI schema object
|
|
*/
|
|
interface Schema {
|
|
type?: string;
|
|
format?: string;
|
|
description?: string;
|
|
properties?: Record<string, Schema>;
|
|
items?: Schema;
|
|
required?: string[];
|
|
[key: string]: unknown;
|
|
}
|
|
|
|
/**
|
|
* OpenAPI header object
|
|
*/
|
|
interface Header {
|
|
description?: string;
|
|
schema?: Schema;
|
|
[key: string]: unknown;
|
|
}
|
|
|
|
/**
|
|
* OpenAPI example object
|
|
*/
|
|
interface Example {
|
|
summary?: string;
|
|
description?: string;
|
|
value?: unknown;
|
|
[key: string]: unknown;
|
|
}
|
|
|
|
/**
|
|
* OpenAPI document structure
|
|
*/
|
|
interface OpenAPIDocument {
|
|
openapi: string;
|
|
info: Info;
|
|
paths: Record<string, PathItem>;
|
|
components?: Components;
|
|
servers?: Server[];
|
|
tags?: Tag[];
|
|
description?: string;
|
|
'x-pathGroup'?: string;
|
|
[key: string]: unknown;
|
|
}
|
|
|
|
/**
|
|
* OpenAPI info object
|
|
*/
|
|
interface Info {
|
|
title: string;
|
|
version: string;
|
|
description?: string;
|
|
termsOfService?: string;
|
|
contact?: Contact;
|
|
license?: License;
|
|
[key: string]: unknown;
|
|
}
|
|
|
|
/**
|
|
* OpenAPI contact object
|
|
*/
|
|
interface Contact {
|
|
name?: string;
|
|
url?: string;
|
|
email?: string;
|
|
[key: string]: unknown;
|
|
}
|
|
|
|
/**
|
|
* OpenAPI license object
|
|
*/
|
|
interface License {
|
|
name: string;
|
|
url?: string;
|
|
[key: string]: unknown;
|
|
}
|
|
|
|
/**
|
|
* OpenAPI components object
|
|
*/
|
|
interface Components {
|
|
schemas?: Record<string, Schema>;
|
|
responses?: Record<string, Response>;
|
|
parameters?: Record<string, Parameter>;
|
|
requestBodies?: Record<string, RequestBody>;
|
|
headers?: Record<string, Header>;
|
|
securitySchemes?: Record<string, SecurityScheme>;
|
|
[key: string]: unknown;
|
|
}
|
|
|
|
/**
|
|
* OpenAPI security scheme object
|
|
*/
|
|
interface SecurityScheme {
|
|
type: string;
|
|
description?: string;
|
|
[key: string]: unknown;
|
|
}
|
|
|
|
/**
|
|
* OpenAPI server object
|
|
*/
|
|
interface Server {
|
|
url: string;
|
|
description?: string;
|
|
variables?: Record<string, ServerVariable>;
|
|
[key: string]: unknown;
|
|
}
|
|
|
|
/**
|
|
* OpenAPI server variable object
|
|
*/
|
|
interface ServerVariable {
|
|
default: string;
|
|
enum?: string[];
|
|
description?: string;
|
|
[key: string]: unknown;
|
|
}
|
|
|
|
/**
|
|
* OpenAPI tag object
|
|
*/
|
|
interface Tag {
|
|
name: string;
|
|
description?: string;
|
|
externalDocs?: ExternalDocs;
|
|
/** Indicates this is a conceptual/supplementary tag (no operations) */
|
|
'x-traitTag'?: boolean;
|
|
/** Related documentation links as plain URLs */
|
|
'x-influxdatadocs-related'?: string[];
|
|
/** Related documentation links with title and href */
|
|
'x-related'?: RelatedLink[];
|
|
[key: string]: unknown;
|
|
}
|
|
|
|
/**
|
|
* Operation metadata for TOC generation
|
|
*/
|
|
interface OperationMeta {
|
|
operationId: string;
|
|
method: string;
|
|
path: string;
|
|
summary: string;
|
|
tags: string[];
|
|
/** Compatibility version (v1 or v2) for migration context */
|
|
compatVersion?: string;
|
|
/** External documentation link */
|
|
externalDocs?: {
|
|
description: string;
|
|
url: string;
|
|
};
|
|
/** Related documentation links (plain URLs) */
|
|
related?: string[];
|
|
/** Related documentation links with title and href */
|
|
relatedLinks?: RelatedLink[];
|
|
}
|
|
|
|
/**
|
|
* OpenAPI external docs object
|
|
*/
|
|
interface ExternalDocs {
|
|
url: string;
|
|
description?: string;
|
|
[key: string]: unknown;
|
|
}
|
|
|
|
/**
|
|
* Article metadata for Hugo
|
|
*/
|
|
interface Article {
|
|
path: string;
|
|
fields: {
|
|
name: string;
|
|
describes: string[];
|
|
title?: string;
|
|
description?: string;
|
|
tags?: string[];
|
|
source?: string;
|
|
staticFilePath?: string;
|
|
/** Related documentation links (plain URLs or {title, href} objects) */
|
|
related?: (string | RelatedLink)[];
|
|
/** OpenAPI tags from operations (for Hugo frontmatter) */
|
|
apiTags?: string[];
|
|
/** Menu display name (actual endpoint path, different from Hugo path) */
|
|
menuName?: string;
|
|
/** OpenAPI tag name (for tag-based articles) */
|
|
tag?: string;
|
|
/** Whether this is a conceptual tag (x-traitTag) */
|
|
isConceptual?: boolean;
|
|
/** Whether to show security schemes section */
|
|
showSecuritySchemes?: boolean;
|
|
/** Tag description from OpenAPI spec */
|
|
tagDescription?: string;
|
|
/** Sidebar navigation group */
|
|
menuGroup?: string;
|
|
/** Operations metadata for TOC generation */
|
|
operations?: OperationMeta[];
|
|
/** Page weight for ordering in navigation */
|
|
weight?: number;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Article collection for Hugo data files
|
|
*/
|
|
interface ArticleCollection {
|
|
articles: Article[];
|
|
}
|
|
|
|
/**
|
|
* Options for generating Hugo data
|
|
*/
|
|
export interface GenerateHugoDataOptions {
|
|
/** Path to the OpenAPI spec file */
|
|
specFile: string;
|
|
/** Output path for generated OpenAPI path fragments */
|
|
dataOutPath: string;
|
|
/** Output path for article metadata */
|
|
articleOutPath: string;
|
|
}
|
|
|
|
/**
|
|
* Options for writing OpenAPI article data
|
|
*/
|
|
interface WriteOpenapiArticleDataOptions {
|
|
/** File pattern to match when filtering files */
|
|
filePattern?: string;
|
|
}
|
|
|
|
/**
|
|
* Read a YAML file and parse it
|
|
*
|
|
* @param filepath - Path to the YAML file
|
|
* @param encoding - File encoding (default: 'utf8')
|
|
* @returns Parsed YAML content
|
|
*/
|
|
function readFile(
|
|
filepath: string,
|
|
encoding: BufferEncoding = 'utf8'
|
|
): OpenAPIDocument {
|
|
const content = fs.readFileSync(filepath, encoding);
|
|
return yaml.load(content) as OpenAPIDocument;
|
|
}
|
|
|
|
/**
|
|
* Write data to a YAML file
|
|
*
|
|
* @param data - Data to write
|
|
* @param outputTo - Output file path
|
|
*/
|
|
function writeDataFile(data: unknown, outputTo: string): void {
|
|
fs.writeFileSync(outputTo, yaml.dump(data));
|
|
}
|
|
|
|
/**
|
|
* Write data to a JSON file
|
|
*
|
|
* @param data - Data to write
|
|
* @param outputTo - Output file path
|
|
*/
|
|
function writeJsonFile(data: unknown, outputTo: string): void {
|
|
fs.writeFileSync(outputTo, JSON.stringify(data, null, 2));
|
|
}
|
|
|
|
/**
|
|
* OpenAPI utility functions
|
|
*/
|
|
const openapiUtils = {
|
|
/**
|
|
* Check if a path fragment is a placeholder (e.g., {id})
|
|
*
|
|
* @param str - Path fragment to check
|
|
* @returns True if the fragment is a placeholder
|
|
*/
|
|
isPlaceholderFragment(str: string): boolean {
|
|
const placeholderRegex = /^\{.*\}$/;
|
|
return placeholderRegex.test(str);
|
|
},
|
|
};
|
|
|
|
/**
|
|
* Convert tag name to URL-friendly slug
|
|
*
|
|
* @param tagName - Tag name (e.g., "Write data", "Processing engine")
|
|
* @returns URL-friendly slug (e.g., "write-data", "processing-engine")
|
|
*/
|
|
function slugifyTag(tagName: string): string {
|
|
return tagName
|
|
.toLowerCase()
|
|
.replace(/[^a-z0-9]+/g, '-')
|
|
.replace(/^-|-$/g, '');
|
|
}
|
|
|
|
/**
|
|
* Menu group mappings for tag-based navigation
|
|
* Maps OpenAPI tags to sidebar groups
|
|
*/
|
|
const TAG_MENU_GROUPS: Record<string, string> = {
|
|
// Concepts group
|
|
'Quick start': 'Concepts',
|
|
Authentication: 'Concepts',
|
|
'Headers and parameters': 'Concepts',
|
|
'Response codes': 'Concepts',
|
|
// Data Operations group
|
|
'Write data': 'Data Operations',
|
|
'Query data': 'Data Operations',
|
|
'Cache data': 'Data Operations',
|
|
// Administration group
|
|
Database: 'Administration',
|
|
Table: 'Administration',
|
|
Token: 'Administration',
|
|
// Processing Engine group
|
|
'Processing engine': 'Processing Engine',
|
|
// Server group
|
|
'Server information': 'Server',
|
|
// Compatibility group
|
|
'Compatibility endpoints': 'Compatibility',
|
|
};
|
|
|
|
/**
|
|
* Get menu group for a tag
|
|
*
|
|
* @param tagName - Tag name
|
|
* @returns Menu group name or 'Other' if not mapped
|
|
*/
|
|
function getMenuGroupForTag(tagName: string): string {
|
|
return TAG_MENU_GROUPS[tagName] || 'Other';
|
|
}
|
|
|
|
/**
|
|
* HTTP methods to check for operations
|
|
*/
|
|
const HTTP_METHODS = [
|
|
'get',
|
|
'post',
|
|
'put',
|
|
'patch',
|
|
'delete',
|
|
'options',
|
|
'head',
|
|
'trace',
|
|
] as const;
|
|
|
|
/**
|
|
* Extract all operations from an OpenAPI document grouped by tag
|
|
*
|
|
* @param openapi - OpenAPI document
|
|
* @returns Map of tag name to operations with that tag
|
|
*/
|
|
function extractOperationsByTag(
|
|
openapi: OpenAPIDocument
|
|
): Map<string, OperationMeta[]> {
|
|
const tagOperations = new Map<string, OperationMeta[]>();
|
|
|
|
Object.entries(openapi.paths).forEach(([pathKey, pathItem]) => {
|
|
HTTP_METHODS.forEach((method) => {
|
|
const operation = pathItem[method] as Operation | undefined;
|
|
if (operation) {
|
|
const opMeta: OperationMeta = {
|
|
operationId: operation.operationId || `${method}-${pathKey}`,
|
|
method: method.toUpperCase(),
|
|
path: pathKey,
|
|
summary: operation.summary || '',
|
|
tags: operation.tags || [],
|
|
};
|
|
|
|
// Extract compatibility version if present
|
|
if (operation['x-compatibility-version']) {
|
|
opMeta.compatVersion = operation['x-compatibility-version'];
|
|
}
|
|
|
|
// Extract externalDocs if present
|
|
if (operation.externalDocs) {
|
|
opMeta.externalDocs = {
|
|
description: operation.externalDocs.description || '',
|
|
url: operation.externalDocs.url,
|
|
};
|
|
}
|
|
|
|
// Extract x-influxdatadocs-related if present
|
|
if (
|
|
operation['x-influxdatadocs-related'] &&
|
|
Array.isArray(operation['x-influxdatadocs-related'])
|
|
) {
|
|
opMeta.related = operation['x-influxdatadocs-related'];
|
|
}
|
|
|
|
// Extract x-related (title/href objects) if present
|
|
if (operation['x-related'] && Array.isArray(operation['x-related'])) {
|
|
opMeta.relatedLinks = operation['x-related'] as RelatedLink[];
|
|
}
|
|
|
|
// Add operation to each of its tags
|
|
(operation.tags || []).forEach((tag) => {
|
|
if (!tagOperations.has(tag)) {
|
|
tagOperations.set(tag, []);
|
|
}
|
|
tagOperations.get(tag)!.push(opMeta);
|
|
});
|
|
}
|
|
});
|
|
});
|
|
|
|
return tagOperations;
|
|
}
|
|
|
|
/**
|
|
* Write OpenAPI specs grouped by tag to separate files
|
|
* Generates both YAML and JSON versions per tag
|
|
*
|
|
* @param openapi - OpenAPI document
|
|
* @param prefix - Filename prefix for output files
|
|
* @param outPath - Output directory path
|
|
*/
|
|
function writeTagOpenapis(
|
|
openapi: OpenAPIDocument,
|
|
prefix: string,
|
|
outPath: string
|
|
): void {
|
|
const tagOperations = extractOperationsByTag(openapi);
|
|
|
|
// Process each tag
|
|
tagOperations.forEach((operations, tagName) => {
|
|
// Deep copy openapi
|
|
const doc: OpenAPIDocument = JSON.parse(JSON.stringify(openapi));
|
|
|
|
// Filter paths to only include those with operations for this tag
|
|
const filteredPaths: Record<string, PathItem> = {};
|
|
Object.entries(openapi.paths).forEach(([pathKey, pathItem]) => {
|
|
const filteredPathItem: PathItem = {};
|
|
let hasOperations = false;
|
|
|
|
HTTP_METHODS.forEach((method) => {
|
|
const operation = pathItem[method] as Operation | undefined;
|
|
if (operation?.tags?.includes(tagName)) {
|
|
// Clone the operation and restrict tags to only this tag
|
|
// This prevents the operation from being rendered multiple times
|
|
// (once per tag) when an operation belongs to multiple tags
|
|
const filteredOperation = { ...operation, tags: [tagName] };
|
|
filteredPathItem[method] = filteredOperation;
|
|
hasOperations = true;
|
|
}
|
|
});
|
|
|
|
// Include path-level parameters if we have operations
|
|
if (hasOperations) {
|
|
if (pathItem.parameters) {
|
|
filteredPathItem.parameters = pathItem.parameters;
|
|
}
|
|
filteredPaths[pathKey] = filteredPathItem;
|
|
}
|
|
});
|
|
|
|
doc.paths = filteredPaths;
|
|
|
|
// Filter tags to only include this tag (and trait tags for context)
|
|
if (doc.tags) {
|
|
doc.tags = doc.tags.filter(
|
|
(tag) => tag.name === tagName || tag['x-traitTag']
|
|
);
|
|
}
|
|
|
|
// Update info
|
|
const tagSlug = slugifyTag(tagName);
|
|
doc.info.title = tagName;
|
|
doc.info.description = `API reference for ${tagName}`;
|
|
doc['x-tagGroup'] = tagName;
|
|
|
|
try {
|
|
if (!fs.existsSync(outPath)) {
|
|
fs.mkdirSync(outPath, { recursive: true });
|
|
}
|
|
|
|
const baseFilename = `${prefix}${tagSlug}`;
|
|
const yamlPath = path.resolve(outPath, `${baseFilename}.yaml`);
|
|
const jsonPath = path.resolve(outPath, `${baseFilename}.json`);
|
|
|
|
writeDataFile(doc, yamlPath);
|
|
writeJsonFile(doc, jsonPath);
|
|
|
|
console.log(
|
|
`Generated tag spec: ${baseFilename}.yaml (${Object.keys(filteredPaths).length} paths, ${operations.length} operations)`
|
|
);
|
|
} catch (err) {
|
|
console.error(`Error writing tag group ${tagName}:`, err);
|
|
}
|
|
});
|
|
|
|
// Also create specs for conceptual tags (x-traitTag) without operations
|
|
(openapi.tags || []).forEach((tag) => {
|
|
if (tag['x-traitTag'] && !tagOperations.has(tag.name)) {
|
|
const doc: OpenAPIDocument = JSON.parse(JSON.stringify(openapi));
|
|
doc.paths = {};
|
|
doc.tags = [tag];
|
|
doc.info.title = tag.name;
|
|
doc.info.description = tag.description || `API reference for ${tag.name}`;
|
|
doc['x-tagGroup'] = tag.name;
|
|
|
|
const tagSlug = slugifyTag(tag.name);
|
|
|
|
try {
|
|
const baseFilename = `${prefix}${tagSlug}`;
|
|
const yamlPath = path.resolve(outPath, `${baseFilename}.yaml`);
|
|
const jsonPath = path.resolve(outPath, `${baseFilename}.json`);
|
|
|
|
writeDataFile(doc, yamlPath);
|
|
writeJsonFile(doc, jsonPath);
|
|
|
|
console.log(`Generated conceptual tag spec: ${baseFilename}.yaml`);
|
|
} catch (err) {
|
|
console.error(`Error writing conceptual tag ${tag.name}:`, err);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Convert API path to filename-safe slug
|
|
*
|
|
* @param apiPath - API path (e.g., "/api/v3/configure/token/admin")
|
|
* @returns Filename-safe slug (e.g., "api-v3-configure-token-admin")
|
|
*/
|
|
function pathToFileSlug(apiPath: string): string {
|
|
return apiPath
|
|
.replace(/^\//, '') // Remove leading slash
|
|
.replace(/\//g, '-') // Replace slashes with dashes
|
|
.replace(/[{}]/g, '') // Remove curly braces from path params
|
|
.replace(/-+/g, '-') // Collapse multiple dashes
|
|
.replace(/-$/, ''); // Remove trailing dash
|
|
}
|
|
|
|
/**
|
|
* Write path-specific OpenAPI specs (one file per exact API path)
|
|
*
|
|
* Each file contains all HTTP methods for a single path, enabling
|
|
* operation pages to filter by method only (no path prefix conflicts).
|
|
*
|
|
* @param openapi - OpenAPI document
|
|
* @param outPath - Output directory path (e.g., "static/openapi/{product}/paths")
|
|
* @returns Map of API path to spec file path (for use in frontmatter)
|
|
*/
|
|
export function writePathSpecificSpecs(
|
|
openapi: OpenAPIDocument,
|
|
outPath: string
|
|
): Map<string, string> {
|
|
const pathSpecFiles = new Map<string, string>();
|
|
|
|
if (!fs.existsSync(outPath)) {
|
|
fs.mkdirSync(outPath, { recursive: true });
|
|
}
|
|
|
|
Object.entries(openapi.paths).forEach(([apiPath, pathItem]) => {
|
|
// Deep clone pathItem to avoid mutating original
|
|
const clonedPathItem: PathItem = JSON.parse(JSON.stringify(pathItem));
|
|
|
|
// Limit each operation to a single tag to prevent duplicate rendering
|
|
// Operations with multiple tags would be rendered once per tag
|
|
const usedTags = new Set<string>();
|
|
HTTP_METHODS.forEach((method) => {
|
|
const operation = clonedPathItem[method] as Operation | undefined;
|
|
if (operation?.tags && operation.tags.length > 0) {
|
|
// Select the most specific tag to avoid duplicate rendering
|
|
// Prefer "Auth token" over "Authentication" for token-related operations
|
|
let primaryTag = operation.tags[0];
|
|
if (operation.tags.includes('Auth token')) {
|
|
primaryTag = 'Auth token';
|
|
}
|
|
operation.tags = [primaryTag];
|
|
usedTags.add(primaryTag);
|
|
}
|
|
});
|
|
|
|
// Create spec with just this path (all its methods)
|
|
// Include global security requirements so auth info displays correctly
|
|
const pathSpec: OpenAPIDocument = {
|
|
openapi: openapi.openapi,
|
|
info: {
|
|
...openapi.info,
|
|
title: apiPath,
|
|
description: `API reference for ${apiPath}`,
|
|
},
|
|
paths: { [apiPath]: clonedPathItem },
|
|
components: openapi.components, // Include for $ref resolution
|
|
servers: openapi.servers,
|
|
security: openapi.security, // Global security requirements
|
|
};
|
|
|
|
// Filter spec-level tags to only include those used by operations
|
|
if (openapi.tags) {
|
|
pathSpec.tags = openapi.tags.filter(
|
|
(tag) => usedTags.has(tag.name) && !tag['x-traitTag']
|
|
);
|
|
}
|
|
|
|
// Write files
|
|
const slug = pathToFileSlug(apiPath);
|
|
const yamlPath = path.resolve(outPath, `${slug}.yaml`);
|
|
const jsonPath = path.resolve(outPath, `${slug}.json`);
|
|
|
|
writeDataFile(pathSpec, yamlPath);
|
|
writeJsonFile(pathSpec, jsonPath);
|
|
|
|
// Store the web-accessible path (without "static/" prefix)
|
|
// Hugo serves files from static/ at the root, so we extract the path after 'static/'
|
|
const staticMatch = yamlPath.match(/static\/(.+)$/);
|
|
const webPath = staticMatch ? `/${staticMatch[1]}` : yamlPath;
|
|
pathSpecFiles.set(apiPath, webPath);
|
|
});
|
|
|
|
console.log(
|
|
`Generated ${pathSpecFiles.size} path-specific specs in ${outPath}`
|
|
);
|
|
|
|
return pathSpecFiles;
|
|
}
|
|
|
|
/**
|
|
* Write OpenAPI specs grouped by path to separate files
|
|
* Generates both YAML and JSON versions
|
|
*
|
|
* @param openapi - OpenAPI document
|
|
* @param prefix - Filename prefix for output files
|
|
* @param outPath - Output directory path
|
|
*/
|
|
function writePathOpenapis(
|
|
openapi: OpenAPIDocument,
|
|
prefix: string,
|
|
outPath: string
|
|
): void {
|
|
const pathGroups: Record<string, Record<string, PathItem>> = {};
|
|
|
|
// Group paths by their base path (first 3-4 segments, excluding placeholders)
|
|
Object.keys(openapi.paths)
|
|
.sort()
|
|
.forEach((p) => {
|
|
const delimiter = '/';
|
|
let key = p.split(delimiter);
|
|
|
|
// Check if this is an item path (ends with a placeholder)
|
|
let isItemPath = openapiUtils.isPlaceholderFragment(key[key.length - 1]);
|
|
if (isItemPath) {
|
|
key = key.slice(0, -1);
|
|
}
|
|
|
|
// Take first 4 segments
|
|
key = key.slice(0, 4);
|
|
|
|
// Check if the last segment is still a placeholder
|
|
isItemPath = openapiUtils.isPlaceholderFragment(key[key.length - 1]);
|
|
if (isItemPath) {
|
|
key = key.slice(0, -1);
|
|
}
|
|
|
|
const groupKey = key.join('/');
|
|
pathGroups[groupKey] = pathGroups[groupKey] || {};
|
|
pathGroups[groupKey][p] = openapi.paths[p];
|
|
});
|
|
|
|
// Write each path group to separate YAML and JSON files
|
|
Object.keys(pathGroups).forEach((pg) => {
|
|
// Deep copy openapi
|
|
const doc: OpenAPIDocument = JSON.parse(JSON.stringify(openapi));
|
|
doc.paths = pathGroups[pg];
|
|
|
|
// Collect tags used by operations in this path group
|
|
const usedTags = new Set<string>();
|
|
Object.values(doc.paths).forEach((pathItem: PathItem) => {
|
|
const httpMethods = [
|
|
'get',
|
|
'post',
|
|
'put',
|
|
'patch',
|
|
'delete',
|
|
'options',
|
|
'head',
|
|
'trace',
|
|
];
|
|
httpMethods.forEach((method) => {
|
|
const operation = pathItem[method] as Operation | undefined;
|
|
if (operation?.tags) {
|
|
operation.tags.forEach((tag) => usedTags.add(tag));
|
|
}
|
|
});
|
|
});
|
|
|
|
// Filter tags to only include those used by operations in this path group
|
|
// Exclude x-traitTag tags (supplementary documentation tags)
|
|
if (doc.tags) {
|
|
doc.tags = doc.tags.filter(
|
|
(tag) => usedTags.has(tag.name) && !tag['x-traitTag']
|
|
);
|
|
}
|
|
|
|
// Simplify info for path-specific docs
|
|
doc.info.title = pg;
|
|
doc.info.description = `API reference for ${pg}`;
|
|
doc['x-pathGroup'] = pg;
|
|
|
|
try {
|
|
if (!fs.existsSync(outPath)) {
|
|
fs.mkdirSync(outPath, { recursive: true });
|
|
}
|
|
|
|
const baseFilename = `${prefix}${pg.replaceAll('/', '-').replace(/^-/, '')}`;
|
|
const yamlPath = path.resolve(outPath, `${baseFilename}.yaml`);
|
|
const jsonPath = path.resolve(outPath, `${baseFilename}.json`);
|
|
|
|
// Write both YAML and JSON versions
|
|
writeDataFile(doc, yamlPath);
|
|
writeJsonFile(doc, jsonPath);
|
|
|
|
console.log(`Generated: ${baseFilename}.yaml and ${baseFilename}.json`);
|
|
} catch (err) {
|
|
console.error(`Error writing path group ${pg}:`, err);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Create article metadata for a path group
|
|
*
|
|
* @param openapi - OpenAPI document with x-pathGroup
|
|
* @returns Article metadata object
|
|
*/
|
|
function createArticleDataForPathGroup(openapi: OpenAPIDocument): Article {
|
|
const article: Article = {
|
|
path: '',
|
|
fields: {
|
|
name: openapi['x-pathGroup'] || '',
|
|
describes: Object.keys(openapi.paths),
|
|
},
|
|
};
|
|
|
|
/**
|
|
* Convert OpenAPI path to Hugo-friendly article path
|
|
* Legacy endpoints (without /api/ prefix) go under api/ directly
|
|
* Versioned endpoints (with /api/vN/) keep their structure
|
|
*
|
|
* @param p - Path to convert (e.g., '/health', '/api/v3/query_sql')
|
|
* @returns Path suitable for Hugo content directory (e.g., 'api/health', 'api/v3/query_sql')
|
|
*/
|
|
const toHugoPath = (p: string): string => {
|
|
if (!p) {
|
|
return '';
|
|
}
|
|
// If path doesn't start with /api/, it's a legacy endpoint
|
|
// Place it directly under api/ to avoid collision with /api/v1/* paths
|
|
if (!p.startsWith('/api/')) {
|
|
// /health -> api/health
|
|
// /write -> api/write
|
|
return `api${p}`;
|
|
}
|
|
// /api/v1/health -> api/v1/health
|
|
// /api/v2/write -> api/v2/write
|
|
// /api/v3/query_sql -> api/v3/query_sql
|
|
return p.replace(/^\//, '');
|
|
};
|
|
|
|
/**
|
|
* Convert path to tag-friendly format (dashes instead of slashes)
|
|
*
|
|
* @param p - Path to convert
|
|
* @returns Tag-friendly path
|
|
*/
|
|
const toTagPath = (p: string): string => {
|
|
if (!p) {
|
|
return '';
|
|
}
|
|
return p.replace(/^\//, '').replaceAll('/', '-');
|
|
};
|
|
|
|
const pathGroup = openapi['x-pathGroup'] || '';
|
|
article.path = toHugoPath(pathGroup);
|
|
// Store original path for menu display (shows actual endpoint path)
|
|
article.fields.menuName = pathGroup;
|
|
article.fields.title = openapi.info?.title;
|
|
article.fields.description = openapi.description;
|
|
|
|
const pathGroupFrags = path.parse(openapi['x-pathGroup'] || '');
|
|
article.fields.tags = [pathGroupFrags?.dir, pathGroupFrags?.name]
|
|
.filter(Boolean)
|
|
.map((t) => toTagPath(t));
|
|
|
|
// Extract x-relatedLinks and OpenAPI tags from path items or operations
|
|
const relatedLinks: string[] = [];
|
|
const apiTags: string[] = [];
|
|
const httpMethods = [
|
|
'get',
|
|
'post',
|
|
'put',
|
|
'patch',
|
|
'delete',
|
|
'options',
|
|
'head',
|
|
'trace',
|
|
];
|
|
|
|
Object.values(openapi.paths).forEach((pathItem: PathItem) => {
|
|
// Check path-level x-relatedLinks
|
|
if (
|
|
pathItem['x-relatedLinks'] &&
|
|
Array.isArray(pathItem['x-relatedLinks'])
|
|
) {
|
|
relatedLinks.push(
|
|
...(pathItem['x-relatedLinks'] as string[]).filter(
|
|
(link) => !relatedLinks.includes(link)
|
|
)
|
|
);
|
|
}
|
|
|
|
// Check operation-level x-relatedLinks and tags
|
|
httpMethods.forEach((method) => {
|
|
const operation = pathItem[method] as Operation | undefined;
|
|
if (operation) {
|
|
// Extract x-relatedLinks
|
|
if (
|
|
operation['x-relatedLinks'] &&
|
|
Array.isArray(operation['x-relatedLinks'])
|
|
) {
|
|
relatedLinks.push(
|
|
...(operation['x-relatedLinks'] as string[]).filter(
|
|
(link) => !relatedLinks.includes(link)
|
|
)
|
|
);
|
|
}
|
|
// Extract OpenAPI tags from operation
|
|
if (operation.tags && Array.isArray(operation.tags)) {
|
|
operation.tags.forEach((tag) => {
|
|
if (!apiTags.includes(tag)) {
|
|
apiTags.push(tag);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
// Only add related if there are links
|
|
if (relatedLinks.length > 0) {
|
|
article.fields.related = relatedLinks;
|
|
}
|
|
|
|
// Add OpenAPI tags from operations (for Hugo frontmatter)
|
|
if (apiTags.length > 0) {
|
|
article.fields.apiTags = apiTags;
|
|
}
|
|
|
|
return article;
|
|
}
|
|
|
|
/**
|
|
* Write OpenAPI article metadata to Hugo data files
|
|
* Generates articles.yml and articles.json
|
|
*
|
|
* @param sourcePath - Path to directory containing OpenAPI fragment files
|
|
* @param targetPath - Output path for article data
|
|
* @param opts - Options including file pattern filter
|
|
*/
|
|
function writeOpenapiArticleData(
|
|
sourcePath: string,
|
|
targetPath: string,
|
|
opts: WriteOpenapiArticleDataOptions
|
|
): void {
|
|
/**
|
|
* Check if path is a file
|
|
*/
|
|
const isFile = (filePath: string): boolean => {
|
|
return fs.lstatSync(filePath).isFile();
|
|
};
|
|
|
|
/**
|
|
* Check if filename matches pattern
|
|
*/
|
|
const matchesPattern = (filePath: string): boolean => {
|
|
return opts.filePattern
|
|
? path.parse(filePath).name.startsWith(opts.filePattern)
|
|
: true;
|
|
};
|
|
|
|
try {
|
|
const articles = fs
|
|
.readdirSync(sourcePath)
|
|
.map((fileName) => path.join(sourcePath, fileName))
|
|
.filter(matchesPattern)
|
|
.filter(isFile)
|
|
.filter(
|
|
(filePath) => filePath.endsWith('.yaml') || filePath.endsWith('.yml')
|
|
) // Only process YAML files
|
|
.map((filePath) => {
|
|
const openapi = readFile(filePath);
|
|
const article = createArticleDataForPathGroup(openapi);
|
|
article.fields.source = filePath;
|
|
// Hugo omits "/static" from the URI when serving files stored in "./static"
|
|
article.fields.staticFilePath = filePath.replace(/^static\//, '/');
|
|
return article;
|
|
});
|
|
|
|
if (!fs.existsSync(targetPath)) {
|
|
fs.mkdirSync(targetPath, { recursive: true });
|
|
}
|
|
|
|
const articleCollection: ArticleCollection = { articles };
|
|
|
|
// Write both YAML and JSON versions
|
|
const yamlPath = path.resolve(targetPath, 'articles.yml');
|
|
const jsonPath = path.resolve(targetPath, 'articles.json');
|
|
|
|
writeDataFile(articleCollection, yamlPath);
|
|
writeJsonFile(articleCollection, jsonPath);
|
|
|
|
console.log(`Generated ${articles.length} articles in ${targetPath}`);
|
|
} catch (e) {
|
|
console.error('Error writing article data:', e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sanitize markdown description by removing fragment links and ReDoc directives
|
|
*
|
|
* Handles three cases:
|
|
* 1. OpenAPI fragment links: [text](#section/...) -> text (removes the link entirely)
|
|
* 2. Relative links with fragments: [text](/path/#anchor) -> [text](/path/) (keeps link, removes fragment)
|
|
* 3. ReDoc injection directives: <!-- ReDoc-Inject: ... --> (removes entirely)
|
|
*
|
|
* This sanitization is necessary because fragment links don't work when article
|
|
* descriptions are rendered via the {{< children >}} shortcode on parent pages.
|
|
*
|
|
* @param description - Markdown description that may contain fragment links
|
|
* @returns Sanitized description suitable for children shortcode rendering
|
|
*/
|
|
function sanitizeDescription(description: string | undefined): string {
|
|
if (!description) {
|
|
return '';
|
|
}
|
|
|
|
let sanitized = description;
|
|
|
|
// Remove ReDoc injection directives (e.g., <!-- ReDoc-Inject: <security-definitions> -->)
|
|
sanitized = sanitized.replace(/<!--\s*ReDoc-Inject:.*?-->/g, '');
|
|
|
|
// Handle markdown links:
|
|
// 1. OpenAPI fragment links (#section/..., #operation/..., #tag/...) -> replace with just the text
|
|
// 2. Relative links with fragments (/path/#anchor) -> keep link but remove fragment
|
|
sanitized = sanitized.replace(
|
|
/\[([^\]]+)\]\(([^)]+)\)/g,
|
|
(match, text, url) => {
|
|
// Case 1: OpenAPI fragment links (starts with #section/, #operation/, #tag/)
|
|
if (url.match(/^#(section|operation|tag)\//)) {
|
|
return text; // Just return the link text, no markdown link
|
|
}
|
|
|
|
// Case 2: Relative link with fragment (starts with /, contains #)
|
|
if (url.startsWith('/') && url.includes('#')) {
|
|
const urlWithoutFragment = url.split('#')[0];
|
|
if (urlWithoutFragment === '/' || urlWithoutFragment === '') {
|
|
return text;
|
|
}
|
|
return `[${text}](${urlWithoutFragment})`;
|
|
}
|
|
|
|
// Case 3: Keep other links as-is (external links, non-fragment links)
|
|
return match;
|
|
}
|
|
);
|
|
|
|
// Clean up extra whitespace left by directive removals
|
|
sanitized = sanitized.replace(/\n\n\n+/g, '\n\n').trim();
|
|
|
|
return sanitized;
|
|
}
|
|
|
|
/**
|
|
* Create article data for a tag-based grouping
|
|
*
|
|
* @param openapi - OpenAPI document with x-tagGroup
|
|
* @param operations - Operations for this tag
|
|
* @param tagMeta - Tag metadata from OpenAPI spec
|
|
* @returns Article metadata object
|
|
*/
|
|
function createArticleDataForTag(
|
|
openapi: OpenAPIDocument,
|
|
operations: OperationMeta[],
|
|
tagMeta?: Tag
|
|
): Article {
|
|
const tagName = (openapi['x-tagGroup'] as string) || '';
|
|
const tagSlug = slugifyTag(tagName);
|
|
const isConceptual = tagMeta?.['x-traitTag'] === true;
|
|
|
|
const article: Article = {
|
|
path: `api/${tagSlug}`,
|
|
fields: {
|
|
name: tagName,
|
|
describes: Object.keys(openapi.paths),
|
|
title: tagName,
|
|
description: sanitizeDescription(
|
|
tagMeta?.description ||
|
|
openapi.info?.description ||
|
|
`API reference for ${tagName}`
|
|
),
|
|
tag: tagName,
|
|
isConceptual,
|
|
menuGroup: getMenuGroupForTag(tagName),
|
|
operations: operations.map((op) => ({
|
|
operationId: op.operationId,
|
|
method: op.method,
|
|
path: op.path,
|
|
summary: op.summary,
|
|
tags: op.tags,
|
|
...(op.compatVersion && { compatVersion: op.compatVersion }),
|
|
...(op.externalDocs && { externalDocs: op.externalDocs }),
|
|
})),
|
|
},
|
|
};
|
|
|
|
// Add tag description for conceptual pages (sanitized for children shortcode)
|
|
if (tagMeta?.description) {
|
|
article.fields.tagDescription = sanitizeDescription(tagMeta.description);
|
|
}
|
|
|
|
// Show security schemes section on Authentication pages
|
|
if (tagName === 'Authentication') {
|
|
article.fields.showSecuritySchemes = true;
|
|
}
|
|
|
|
// Set custom weight for Quick start to appear first in nav
|
|
if (tagName === 'Quick start') {
|
|
article.fields.weight = 1;
|
|
}
|
|
|
|
// Set default weight for consistent sorting (articles without explicit weight)
|
|
if (article.fields.weight === undefined) {
|
|
article.fields.weight = 100;
|
|
}
|
|
|
|
// Aggregate related links from multiple sources into article-level related
|
|
// This populates Hugo frontmatter `related` field for "Related content" links
|
|
// Supports both plain URL strings and {title, href} objects
|
|
const relatedItems: (string | RelatedLink)[] = [];
|
|
const seenHrefs = new Set<string>();
|
|
|
|
// Helper to add a link, deduplicating by href
|
|
const addRelated = (item: string | RelatedLink): void => {
|
|
const href = typeof item === 'string' ? item : item.href;
|
|
if (!seenHrefs.has(href)) {
|
|
seenHrefs.add(href);
|
|
relatedItems.push(item);
|
|
}
|
|
};
|
|
|
|
// Tag-level x-related ({title, href} objects)
|
|
if (tagMeta?.['x-related']) {
|
|
(tagMeta['x-related'] as RelatedLink[]).forEach(addRelated);
|
|
}
|
|
|
|
// Tag-level x-influxdatadocs-related (plain URLs)
|
|
if (tagMeta?.['x-influxdatadocs-related']) {
|
|
(tagMeta['x-influxdatadocs-related'] as string[]).forEach(addRelated);
|
|
}
|
|
|
|
// Tag-level externalDocs (standard OpenAPI field — single link)
|
|
if (tagMeta?.externalDocs?.url) {
|
|
addRelated({
|
|
title: tagMeta.externalDocs.description || tagMeta.externalDocs.url,
|
|
href: tagMeta.externalDocs.url,
|
|
});
|
|
}
|
|
|
|
// Operation-level related links
|
|
operations.forEach((op) => {
|
|
if (op.relatedLinks) {
|
|
op.relatedLinks.forEach(addRelated);
|
|
}
|
|
if (op.related) {
|
|
op.related.forEach(addRelated);
|
|
}
|
|
if (op.externalDocs?.url) {
|
|
addRelated({
|
|
title: op.externalDocs.description || op.externalDocs.url,
|
|
href: op.externalDocs.url,
|
|
});
|
|
}
|
|
});
|
|
|
|
if (relatedItems.length > 0) {
|
|
article.fields.related = relatedItems;
|
|
}
|
|
|
|
return article;
|
|
}
|
|
|
|
/**
|
|
* Write tag-based OpenAPI article metadata to Hugo data files
|
|
* Generates articles.yml and articles.json
|
|
*
|
|
* @param sourcePath - Path to directory containing tag-based OpenAPI fragment files
|
|
* @param targetPath - Output path for article data
|
|
* @param openapi - Original OpenAPI document (for tag metadata)
|
|
* @param opts - Options including file pattern filter
|
|
*/
|
|
function writeOpenapiTagArticleData(
|
|
sourcePath: string,
|
|
targetPath: string,
|
|
openapi: OpenAPIDocument,
|
|
opts: WriteOpenapiArticleDataOptions
|
|
): void {
|
|
const isFile = (filePath: string): boolean => {
|
|
return fs.lstatSync(filePath).isFile();
|
|
};
|
|
|
|
const matchesPattern = (filePath: string): boolean => {
|
|
return opts.filePattern
|
|
? path.parse(filePath).name.startsWith(opts.filePattern)
|
|
: true;
|
|
};
|
|
|
|
// Create tag metadata lookup
|
|
const tagMetaMap = new Map<string, Tag>();
|
|
(openapi.tags || []).forEach((tag) => {
|
|
tagMetaMap.set(tag.name, tag);
|
|
});
|
|
|
|
try {
|
|
const articles = fs
|
|
.readdirSync(sourcePath)
|
|
.map((fileName) => path.join(sourcePath, fileName))
|
|
.filter(matchesPattern)
|
|
.filter(isFile)
|
|
.filter(
|
|
(filePath) => filePath.endsWith('.yaml') || filePath.endsWith('.yml')
|
|
)
|
|
.map((filePath) => {
|
|
const tagOpenapi = readFile(filePath);
|
|
const tagName =
|
|
(tagOpenapi['x-tagGroup'] as string) || tagOpenapi.info?.title || '';
|
|
const tagMeta = tagMetaMap.get(tagName);
|
|
|
|
// Extract operations from the tag-filtered spec
|
|
const operations: OperationMeta[] = [];
|
|
Object.entries(tagOpenapi.paths).forEach(([pathKey, pathItem]) => {
|
|
HTTP_METHODS.forEach((method) => {
|
|
const operation = pathItem[method] as Operation | undefined;
|
|
if (operation) {
|
|
const opMeta: OperationMeta = {
|
|
operationId: operation.operationId || `${method}-${pathKey}`,
|
|
method: method.toUpperCase(),
|
|
path: pathKey,
|
|
summary: operation.summary || '',
|
|
tags: operation.tags || [],
|
|
};
|
|
|
|
// Extract compatibility version if present
|
|
if (operation['x-compatibility-version']) {
|
|
opMeta.compatVersion = operation['x-compatibility-version'];
|
|
}
|
|
|
|
// Extract externalDocs if present
|
|
if (operation.externalDocs) {
|
|
opMeta.externalDocs = {
|
|
description: operation.externalDocs.description || '',
|
|
url: operation.externalDocs.url,
|
|
};
|
|
}
|
|
|
|
// Extract x-influxdatadocs-related if present
|
|
if (
|
|
operation['x-influxdatadocs-related'] &&
|
|
Array.isArray(operation['x-influxdatadocs-related'])
|
|
) {
|
|
opMeta.related = operation['x-influxdatadocs-related'];
|
|
}
|
|
|
|
// Extract x-related (title/href objects)
|
|
if (
|
|
operation['x-related'] &&
|
|
Array.isArray(operation['x-related'])
|
|
) {
|
|
opMeta.relatedLinks = operation['x-related'] as RelatedLink[];
|
|
}
|
|
|
|
operations.push(opMeta);
|
|
}
|
|
});
|
|
});
|
|
|
|
const article = createArticleDataForTag(
|
|
tagOpenapi,
|
|
operations,
|
|
tagMeta
|
|
);
|
|
article.fields.source = filePath;
|
|
article.fields.staticFilePath = filePath.replace(/^static\//, '/');
|
|
return article;
|
|
});
|
|
|
|
if (!fs.existsSync(targetPath)) {
|
|
fs.mkdirSync(targetPath, { recursive: true });
|
|
}
|
|
|
|
const articleCollection: ArticleCollection = { articles };
|
|
|
|
// Write both YAML and JSON versions
|
|
const yamlPath = path.resolve(targetPath, 'articles.yml');
|
|
const jsonPath = path.resolve(targetPath, 'articles.json');
|
|
|
|
writeDataFile(articleCollection, yamlPath);
|
|
writeJsonFile(articleCollection, jsonPath);
|
|
|
|
console.log(
|
|
`Generated ${articles.length} tag-based articles in ${targetPath}`
|
|
);
|
|
} catch (e) {
|
|
console.error('Error writing tag article data:', e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Options for generating Hugo data by tag
|
|
*/
|
|
export interface GenerateHugoDataByTagOptions extends GenerateHugoDataOptions {
|
|
/** Whether to also generate path-based files (for backwards compatibility) */
|
|
includePaths?: boolean;
|
|
}
|
|
|
|
/**
|
|
* Generate Hugo data files from an OpenAPI specification grouped by tag
|
|
*
|
|
* This function:
|
|
* 1. Reads the OpenAPI spec file
|
|
* 2. Groups operations by their OpenAPI tags
|
|
* 3. Writes each tag group to separate YAML and JSON files
|
|
* 4. Generates tag-based article metadata for Hugo
|
|
*
|
|
* @param options - Generation options
|
|
*/
|
|
export function generateHugoDataByTag(
|
|
options: GenerateHugoDataByTagOptions
|
|
): void {
|
|
const filenamePrefix = `${path.parse(options.specFile).name}-`;
|
|
const sourceFile = readFile(options.specFile, 'utf8');
|
|
|
|
// Optionally generate path-based files for backwards compatibility
|
|
if (options.includePaths) {
|
|
console.log(
|
|
`\nGenerating OpenAPI path files in ${options.dataOutPath}....`
|
|
);
|
|
writePathOpenapis(sourceFile, filenamePrefix, options.dataOutPath);
|
|
}
|
|
|
|
// Generate tag-based files
|
|
const tagOutPath = options.includePaths
|
|
? path.join(options.dataOutPath, 'tags')
|
|
: options.dataOutPath;
|
|
|
|
console.log(`\nGenerating OpenAPI tag files in ${tagOutPath}....`);
|
|
writeTagOpenapis(sourceFile, filenamePrefix, tagOutPath);
|
|
|
|
console.log(
|
|
`\nGenerating OpenAPI tag article data in ${options.articleOutPath}...`
|
|
);
|
|
writeOpenapiTagArticleData(tagOutPath, options.articleOutPath, sourceFile, {
|
|
filePattern: filenamePrefix,
|
|
});
|
|
|
|
console.log('\nTag-based generation complete!\n');
|
|
}
|
|
|
|
/**
|
|
* Generate Hugo data files from an OpenAPI specification
|
|
*
|
|
* This function:
|
|
* 1. Reads the OpenAPI spec file
|
|
* 2. Groups paths by their base path
|
|
* 3. Writes each group to separate YAML and JSON files
|
|
* 4. Generates article metadata for Hugo
|
|
*
|
|
* @param options - Generation options
|
|
*/
|
|
export function generateHugoData(options: GenerateHugoDataOptions): void {
|
|
const filenamePrefix = `${path.parse(options.specFile).name}-`;
|
|
|
|
const sourceFile = readFile(options.specFile, 'utf8');
|
|
|
|
console.log(`\nGenerating OpenAPI path files in ${options.dataOutPath}....`);
|
|
writePathOpenapis(sourceFile, filenamePrefix, options.dataOutPath);
|
|
|
|
console.log(
|
|
`\nGenerating OpenAPI article data in ${options.articleOutPath}...`
|
|
);
|
|
writeOpenapiArticleData(options.dataOutPath, options.articleOutPath, {
|
|
filePattern: filenamePrefix,
|
|
});
|
|
|
|
console.log('\nGeneration complete!\n');
|
|
}
|
|
|
|
/**
|
|
* Generate path-specific OpenAPI specs from a spec file
|
|
*
|
|
* Convenience wrapper that reads the spec file and generates path-specific specs.
|
|
*
|
|
* @param specFile - Path to OpenAPI spec file
|
|
* @param outPath - Output directory for path-specific specs
|
|
* @returns Map of API path to spec file web path (for use in frontmatter)
|
|
*/
|
|
export function generatePathSpecificSpecs(
|
|
specFile: string,
|
|
outPath: string
|
|
): Map<string, string> {
|
|
const openapi = readFile(specFile, 'utf8');
|
|
return writePathSpecificSpecs(openapi, outPath);
|
|
}
|
|
|
|
// CommonJS export for backward compatibility
|
|
module.exports = {
|
|
generateHugoData,
|
|
generateHugoDataByTag,
|
|
generatePathSpecificSpecs,
|
|
writePathSpecificSpecs,
|
|
};
|