@apistudio/apim-cli
Version:
CLI for API Management Products
235 lines (194 loc) • 7.57 kB
JavaScript
/**
* Script to generate TypeScript interfaces for all schemas in combined-source.json
*/
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
// Get the directory name of the current module
const __filename = fileURLToPath(import.meta.url);
const __dirname = "../";
// Read the combined-source.json file
const combinedSourcePath = path.join(__dirname, 'generated', 'combined-source.json');
const defaultVersionPath = path.join(__dirname, 'generated', 'defaultVersion.json');
// Check if files exist
if (!fs.existsSync(combinedSourcePath)) {
console.error(`Error: ${combinedSourcePath} does not exist`);
process.exit(1);
}
if (!fs.existsSync(defaultVersionPath)) {
console.error(`Error: ${defaultVersionPath} does not exist`);
process.exit(1);
}
// Read the files
const combinedSource = JSON.parse(fs.readFileSync(combinedSourcePath, 'utf8'));
const defaultVersions = JSON.parse(fs.readFileSync(defaultVersionPath, 'utf8'));
// Output file path
const outputPath = path.join(__dirname, 'src', 'api-model-kinds.ts');
// Helper function to convert JSON schema type to TypeScript type
function convertType(schemaType, format) {
if (schemaType === 'string') {
if (format === 'date-time') return 'Date';
return 'string';
}
if (schemaType === 'integer' || schemaType === 'number') return 'number';
if (schemaType === 'boolean') return 'boolean';
if (schemaType === 'array') return 'any[]';
if (schemaType === 'object') return 'Record<string, any>';
return 'any';
}
// Helper function to make a valid TypeScript property name
function makeValidPropertyName(propName) {
// If the property name contains special characters, wrap it in quotes
if (/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(propName) && !propName.includes('-')) {
return propName;
} else {
return `'${propName}'`;
}
}
// Helper function to convert schema property to TypeScript property
function convertProperty(propName, propSchema, required = []) {
const isRequired = required.includes(propName);
const questionMark = isRequired ? '' : '?';
// Handle property names with special characters
const safePropName = makeValidPropertyName(propName);
if (propSchema.type === 'array') {
if (propSchema.items && propSchema.items.type) {
return `${safePropName}${questionMark}: ${convertType(propSchema.items.type)}[]`;
}
return `${safePropName}${questionMark}: any[]`;
}
if (propSchema.type === 'object') {
if (propSchema.properties) {
const nestedProps = Object.entries(propSchema.properties).map(([nestedName, nestedSchema]) => {
const nestedRequired = propSchema.required || [];
return convertProperty(nestedName, nestedSchema, nestedRequired);
}).join(';\n ');
return `${safePropName}${questionMark}: {\n ${nestedProps}\n }`;
}
return `${safePropName}${questionMark}: Record<string, any>`;
}
return `${safePropName}${questionMark}: ${convertType(propSchema.type, propSchema.format)}`;
}
// Helper function to make a valid TypeScript interface name
function makeValidInterfaceName(kind, apiVersion) {
// Replace dots, slashes, and hyphens with underscores
const safeKind = kind.replace(/[^a-zA-Z0-9_]/g, '_');
const safeApiVersion = apiVersion.replace(/[^a-zA-Z0-9_]/g, '_');
return `${safeKind}_${safeApiVersion}`;
}
// Generate TypeScript interfaces from schemas
function generateInterfaces() {
const interfaces = [];
const kinds = new Set();
const interfaceNameMap = new Map(); // Map to track original to safe interface names
// Process each schema
for (const [schemaKey, schema] of Object.entries(combinedSource)) {
// Skip non-object schemas
if (schema.type !== 'object') continue;
// Extract kind and apiVersion from schema key
const match = schemaKey.match(/^(.+)_(.+)\.json$/);
if (!match) continue;
const apiVersion = match[1].replace(/_/g, '/');
const kindLower = match[2];
// Find the actual kind name with correct casing
let kind = null;
for (const k of Object.keys(defaultVersions)) {
if (k.toLowerCase() === kindLower) {
kind = k;
kinds.add(k);
break;
}
}
if (!kind) continue;
// Generate interface name - replace dots and slashes with underscores to make it valid TypeScript
const safeInterfaceName = makeValidInterfaceName(kind, apiVersion);
interfaceNameMap.set(`${kind}_${apiVersion}`, safeInterfaceName);
// Generate interface properties
let properties = '';
if (schema.properties) {
// Ensure kind property is typed as KindType
properties = ` kind: KindType;\n apiVersion: string;\n`;
// Add other properties
const otherProps = Object.entries(schema.properties)
.filter(([propName]) => propName !== 'kind' && propName !== 'apiVersion')
.map(([propName, propSchema]) => {
const required = schema.required || [];
return ` ${convertProperty(propName, propSchema, required)}`;
})
.join(';\n');
if (otherProps) {
properties += otherProps + ';';
}
}
// Create interface
const interfaceCode = `export interface ${safeInterfaceName} {\n${properties}\n}`;
interfaces.push(interfaceCode);
}
// Generate union type for all kinds
const kindsArray = Array.from(kinds);
const kindUnionType = `export type KindType = ${kindsArray.map(k => `'${k}'`).join(' | ')};`;
// Generate KindApiVersionType union
const kindApiVersionUnion = `export type KindApiVersionType = ${Array.from(interfaceNameMap.values()).map(name => name).join(' | ')};`;
// Generate base ApiResource interface
const apiResourceInterface = `export interface ApiResource {
kind: KindType;
apiVersion: string;
metadata: {
name: string;
version: string;
description?: string;
namespace?: string;
tags?: string[];
type?: string;
labels?: {
gatewayTypes?: string[];
};
};
spec: Record<string, any>;
}`;
// Generate utility functions
const utilityFunctions = `
/**
* Type guard to check if a resource is of a specific kind and API version
*/
export function isResourceOfType<T extends ApiResource>(
resource: ApiResource,
kind: KindType,
apiVersion: string
): resource is T {
return resource.kind === kind && resource.apiVersion === apiVersion;
}
/**
* Get the TypeScript interface name for a given kind and API version
*/
export function getInterfaceName(kind: KindType, apiVersion: string): string {
// Replace dots, slashes, and hyphens with underscores for valid TypeScript interface name
const safeKind = kind.replace(/[^a-zA-Z0-9_]/g, '_');
const safeApiVersion = apiVersion.replace(/[^a-zA-Z0-9_]/g, '_');
return \`\${safeKind}_\${safeApiVersion}\`;
}
/**
* Map of original kind_apiVersion to TypeScript interface names
*/
export const interfaceNameMap = {
${Array.from(interfaceNameMap.entries()).map(([original, safe]) => ` "${original}": "${safe}"`).join(',\n')}
};
`;
// Combine all code
const code = `/**
* Generated TypeScript interfaces for API resources
* DO NOT EDIT MANUALLY - This file is generated
*/
${kindUnionType}
${apiResourceInterface}
${kindApiVersionUnion}
${interfaces.join('\n\n')}
${utilityFunctions}
`;
// Write to file
fs.writeFileSync(outputPath, code);
console.log(`Generated TypeScript interfaces for ${interfaces.length} schemas in ${outputPath}`);
}
// Run the generator
generateInterfaces();
// Made with Bob