mirror of
https://github.com/OneUptime/oneuptime.git
synced 2026-04-06 00:32:12 +02:00
Refactor ToolInstaller and ProviderSpec for improved readability and consistency
- Updated import statements to use double quotes for consistency. - Refactored ToolInstaller methods to enhance readability and maintainability. - Improved error handling and logging in ToolInstaller methods. - Simplified the installation verification process for tools. - Enhanced the ProviderSpec class to maintain consistent formatting and error handling. - Refactored SpecificationConverter for better structure and clarity. - Improved the extraction and sanitization of resource names in SpecificationConverter. - Enhanced schema generation logic to accommodate various OpenAPI specifications.
This commit is contained in:
@@ -10,13 +10,13 @@ export async function generateOpenAPISpec(outputPath?: string): Promise<void> {
|
||||
|
||||
// Default to root directory if outputPath is not provided
|
||||
const finalOutputPath: string = outputPath || "./openapi.json";
|
||||
|
||||
|
||||
// Ensure the directory exists
|
||||
const directory = path.dirname(finalOutputPath);
|
||||
if (!fs.existsSync(directory)) {
|
||||
fs.mkdirSync(directory, { recursive: true });
|
||||
}
|
||||
|
||||
|
||||
fs.writeFileSync(finalOutputPath, JSON.stringify(spec, null, 2), "utf8");
|
||||
|
||||
const validationResult: ValidationResult = await validate(finalOutputPath);
|
||||
|
||||
@@ -1,146 +1,161 @@
|
||||
import { execSync } from 'child_process';
|
||||
import path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import { execSync } from "child_process";
|
||||
import path from "path";
|
||||
import * as fs from "fs";
|
||||
|
||||
interface FrameworkGeneratorOptions {
|
||||
specificationPath: string;
|
||||
outputPath: string;
|
||||
packageName?: string;
|
||||
specificationPath: string;
|
||||
outputPath: string;
|
||||
packageName?: string;
|
||||
}
|
||||
|
||||
interface FrameworkScaffoldOptions {
|
||||
type: 'data-source' | 'provider' | 'resource';
|
||||
name: string;
|
||||
outputDir: string;
|
||||
packageName?: string;
|
||||
force?: boolean;
|
||||
type: "data-source" | "provider" | "resource";
|
||||
name: string;
|
||||
outputDir: string;
|
||||
packageName?: string;
|
||||
force?: boolean;
|
||||
}
|
||||
|
||||
export default class FrameworkGenerator {
|
||||
private static readonly TOOL_NAME = 'tfplugingen-framework';
|
||||
private static readonly TOOL_NAME = "tfplugingen-framework";
|
||||
|
||||
/**
|
||||
* Generate Terraform Provider Framework code from a Provider Code Specification
|
||||
*/
|
||||
public static generateAll(options: FrameworkGeneratorOptions): void {
|
||||
this.generate('all', options);
|
||||
/**
|
||||
* Generate Terraform Provider Framework code from a Provider Code Specification
|
||||
*/
|
||||
public static generateAll(options: FrameworkGeneratorOptions): void {
|
||||
this.generate("all", options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate only data source code
|
||||
*/
|
||||
public static generateDataSources(options: FrameworkGeneratorOptions): void {
|
||||
this.generate("data-sources", options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate only resource code
|
||||
*/
|
||||
public static generateResources(options: FrameworkGeneratorOptions): void {
|
||||
this.generate("resources", options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate only provider code
|
||||
*/
|
||||
public static generateProvider(options: FrameworkGeneratorOptions): void {
|
||||
this.generate("provider", options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scaffold starter code for a data source, provider, or resource
|
||||
*/
|
||||
public static scaffold(options: FrameworkScaffoldOptions): void {
|
||||
const binaryPath = this.getTerraformFrameworkGeneratorPath();
|
||||
|
||||
let command = `"${binaryPath}" scaffold ${options.type} --name "${options.name}" --output-dir "${options.outputDir}"`;
|
||||
|
||||
if (options.packageName) {
|
||||
command += ` --package "${options.packageName}"`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate only data source code
|
||||
*/
|
||||
public static generateDataSources(options: FrameworkGeneratorOptions): void {
|
||||
this.generate('data-sources', options);
|
||||
if (options.force) {
|
||||
command += " --force";
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate only resource code
|
||||
*/
|
||||
public static generateResources(options: FrameworkGeneratorOptions): void {
|
||||
this.generate('resources', options);
|
||||
try {
|
||||
console.log(`🏗️ Scaffolding ${options.type}: ${options.name}`);
|
||||
console.log(`📁 Output directory: ${options.outputDir}`);
|
||||
console.log(`🔧 Running command: ${command}`);
|
||||
|
||||
execSync(command, { stdio: "inherit" });
|
||||
console.log(
|
||||
`✅ Successfully scaffolded ${options.type}: ${options.name}`,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(`❌ Error scaffolding ${options.type}:`, error);
|
||||
throw new Error(
|
||||
`Failed to scaffold ${options.type}: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private static generate(
|
||||
subcommand: "all" | "data-sources" | "resources" | "provider",
|
||||
options: FrameworkGeneratorOptions,
|
||||
): void {
|
||||
const binaryPath = this.getTerraformFrameworkGeneratorPath();
|
||||
|
||||
let command = `"${binaryPath}" generate ${subcommand} --input "${options.specificationPath}" --output "${options.outputPath}"`;
|
||||
|
||||
if (options.packageName) {
|
||||
command += ` --package "${options.packageName}"`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate only provider code
|
||||
*/
|
||||
public static generateProvider(options: FrameworkGeneratorOptions): void {
|
||||
this.generate('provider', options);
|
||||
try {
|
||||
console.log(`🔄 Generating ${subcommand} from specification...`);
|
||||
console.log(`📄 Input specification: ${options.specificationPath}`);
|
||||
console.log(`📁 Output directory: ${options.outputPath}`);
|
||||
console.log(`🔧 Running command: ${command}`);
|
||||
|
||||
// Ensure output directory exists
|
||||
if (!fs.existsSync(options.outputPath)) {
|
||||
fs.mkdirSync(options.outputPath, { recursive: true });
|
||||
}
|
||||
|
||||
execSync(command, { stdio: "inherit" });
|
||||
console.log(
|
||||
`✅ Successfully generated ${subcommand} at: ${options.outputPath}`,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(`❌ Error generating ${subcommand}:`, error);
|
||||
throw new Error(
|
||||
`Failed to generate ${subcommand}: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scaffold starter code for a data source, provider, or resource
|
||||
*/
|
||||
public static scaffold(options: FrameworkScaffoldOptions): void {
|
||||
const binaryPath = this.getTerraformFrameworkGeneratorPath();
|
||||
|
||||
let command = `"${binaryPath}" scaffold ${options.type} --name "${options.name}" --output-dir "${options.outputDir}"`;
|
||||
|
||||
if (options.packageName) {
|
||||
command += ` --package "${options.packageName}"`;
|
||||
}
|
||||
|
||||
if (options.force) {
|
||||
command += ' --force';
|
||||
}
|
||||
private static getTerraformFrameworkGeneratorPath(): string {
|
||||
// Get the Go path and construct the full path to the tfplugingen-framework binary
|
||||
const goPath = execSync("go env GOPATH", { encoding: "utf8" }).trim();
|
||||
return path.join(goPath, "bin", this.TOOL_NAME);
|
||||
}
|
||||
|
||||
try {
|
||||
console.log(`🏗️ Scaffolding ${options.type}: ${options.name}`);
|
||||
console.log(`📁 Output directory: ${options.outputDir}`);
|
||||
console.log(`🔧 Running command: ${command}`);
|
||||
|
||||
execSync(command, { stdio: 'inherit' });
|
||||
console.log(`✅ Successfully scaffolded ${options.type}: ${options.name}`);
|
||||
} catch (error) {
|
||||
console.error(`❌ Error scaffolding ${options.type}:`, error);
|
||||
throw new Error(`Failed to scaffold ${options.type}: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
}
|
||||
/**
|
||||
* Check if the framework generator tool is installed
|
||||
*/
|
||||
public static isInstalled(): boolean {
|
||||
try {
|
||||
const binaryPath = this.getTerraformFrameworkGeneratorPath();
|
||||
return fs.existsSync(binaryPath);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static generate(subcommand: 'all' | 'data-sources' | 'resources' | 'provider', options: FrameworkGeneratorOptions): void {
|
||||
const binaryPath = this.getTerraformFrameworkGeneratorPath();
|
||||
|
||||
let command = `"${binaryPath}" generate ${subcommand} --input "${options.specificationPath}" --output "${options.outputPath}"`;
|
||||
|
||||
if (options.packageName) {
|
||||
command += ` --package "${options.packageName}"`;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log(`🔄 Generating ${subcommand} from specification...`);
|
||||
console.log(`📄 Input specification: ${options.specificationPath}`);
|
||||
console.log(`📁 Output directory: ${options.outputPath}`);
|
||||
console.log(`🔧 Running command: ${command}`);
|
||||
|
||||
// Ensure output directory exists
|
||||
if (!fs.existsSync(options.outputPath)) {
|
||||
fs.mkdirSync(options.outputPath, { recursive: true });
|
||||
}
|
||||
|
||||
execSync(command, { stdio: 'inherit' });
|
||||
console.log(`✅ Successfully generated ${subcommand} at: ${options.outputPath}`);
|
||||
} catch (error) {
|
||||
console.error(`❌ Error generating ${subcommand}:`, error);
|
||||
throw new Error(`Failed to generate ${subcommand}: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
}
|
||||
}
|
||||
|
||||
private static getTerraformFrameworkGeneratorPath(): string {
|
||||
// Get the Go path and construct the full path to the tfplugingen-framework binary
|
||||
const goPath = execSync('go env GOPATH', { encoding: 'utf8' }).trim();
|
||||
return path.join(goPath, 'bin', this.TOOL_NAME);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the framework generator tool is installed
|
||||
*/
|
||||
public static isInstalled(): boolean {
|
||||
try {
|
||||
const binaryPath = this.getTerraformFrameworkGeneratorPath();
|
||||
return fs.existsSync(binaryPath);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Print usage information for the framework generator
|
||||
*/
|
||||
public static printUsageInfo(): void {
|
||||
console.log('📖 Terraform Plugin Framework Generator Usage:');
|
||||
console.log('');
|
||||
console.log('🔄 Generate Commands:');
|
||||
console.log(' generateAll() - Generate all provider code (data sources, resources, and provider)');
|
||||
console.log(' generateDataSources()- Generate only data source code');
|
||||
console.log(' generateResources() - Generate only resource code');
|
||||
console.log(' generateProvider() - Generate only provider code');
|
||||
console.log('');
|
||||
console.log('🏗️ Scaffold Commands:');
|
||||
console.log(' scaffold() - Create starter code for data source, provider, or resource');
|
||||
console.log('');
|
||||
console.log('📋 Requirements:');
|
||||
console.log(' - Provider Code Specification file (JSON format)');
|
||||
console.log(' - tfplugingen-framework tool installed');
|
||||
console.log(' - Go installed and properly configured');
|
||||
console.log('');
|
||||
}
|
||||
/**
|
||||
* Print usage information for the framework generator
|
||||
*/
|
||||
public static printUsageInfo(): void {
|
||||
console.log("📖 Terraform Plugin Framework Generator Usage:");
|
||||
console.log("");
|
||||
console.log("🔄 Generate Commands:");
|
||||
console.log(
|
||||
" generateAll() - Generate all provider code (data sources, resources, and provider)",
|
||||
);
|
||||
console.log(" generateDataSources()- Generate only data source code");
|
||||
console.log(" generateResources() - Generate only resource code");
|
||||
console.log(" generateProvider() - Generate only provider code");
|
||||
console.log("");
|
||||
console.log("🏗️ Scaffold Commands:");
|
||||
console.log(
|
||||
" scaffold() - Create starter code for data source, provider, or resource",
|
||||
);
|
||||
console.log("");
|
||||
console.log("📋 Requirements:");
|
||||
console.log(" - Provider Code Specification file (JSON format)");
|
||||
console.log(" - tfplugingen-framework tool installed");
|
||||
console.log(" - Go installed and properly configured");
|
||||
console.log("");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,71 +1,94 @@
|
||||
import { generateOpenAPISpec } from '../OpenAPI/GenerateSpec';
|
||||
import { ToolInstaller } from './InstallTools';
|
||||
import FrameworkGenerator from './FrameworkGenerator';
|
||||
import SpecificationConverter from './SpecificationConverter';
|
||||
import path from 'path';
|
||||
import { generateOpenAPISpec } from "../OpenAPI/GenerateSpec";
|
||||
import { ToolInstaller } from "./InstallTools";
|
||||
import FrameworkGenerator from "./FrameworkGenerator";
|
||||
import SpecificationConverter from "./SpecificationConverter";
|
||||
import path from "path";
|
||||
|
||||
async function main() {
|
||||
console.log('🚀 Starting Terraform Provider Generation Process...');
|
||||
|
||||
console.log("🚀 Starting Terraform Provider Generation Process...");
|
||||
|
||||
try {
|
||||
// 1. Generate OpenAPI spec
|
||||
console.log('\n📄 Step 1: Generating OpenAPI specification...');
|
||||
const openApiSpecPath = path.resolve(__dirname, '../../Terraform/openapi.json');
|
||||
console.log("\n📄 Step 1: Generating OpenAPI specification...");
|
||||
const openApiSpecPath = path.resolve(
|
||||
__dirname,
|
||||
"../../Terraform/openapi.json",
|
||||
);
|
||||
await generateOpenAPISpec(openApiSpecPath);
|
||||
|
||||
// 2. Convert OpenAPI spec to Provider Code Specification
|
||||
console.log('\n🔄 Step 2: Converting to Provider Code Specification...');
|
||||
const providerSpecPath = path.resolve(__dirname, '../../Terraform/provider-code-spec.json');
|
||||
console.log("\n🔄 Step 2: Converting to Provider Code Specification...");
|
||||
const providerSpecPath = path.resolve(
|
||||
__dirname,
|
||||
"../../Terraform/provider-code-spec.json",
|
||||
);
|
||||
SpecificationConverter.convertOpenAPIToProviderSpec({
|
||||
openApiSpecPath: openApiSpecPath,
|
||||
outputPath: providerSpecPath,
|
||||
providerName: 'oneuptime',
|
||||
providerName: "oneuptime",
|
||||
});
|
||||
|
||||
// 3. Install Framework Generator tool
|
||||
console.log('\n🔧 Step 3: Installing Terraform Plugin Framework Generator...');
|
||||
const frameworkInstallResult = await ToolInstaller.installTerraformPluginFrameworkGenerator();
|
||||
console.log(
|
||||
"\n🔧 Step 3: Installing Terraform Plugin Framework Generator...",
|
||||
);
|
||||
const frameworkInstallResult =
|
||||
await ToolInstaller.installTerraformPluginFrameworkGenerator();
|
||||
if (!frameworkInstallResult.success) {
|
||||
throw new Error(`Failed to install framework generator: ${frameworkInstallResult.message}`);
|
||||
throw new Error(
|
||||
`Failed to install framework generator: ${frameworkInstallResult.message}`,
|
||||
);
|
||||
}
|
||||
console.log(`✅ ${frameworkInstallResult.message}`);
|
||||
|
||||
// 4. Generate Terraform Provider Framework code
|
||||
console.log('\n🏗️ Step 4: Generating Terraform Provider Framework code...');
|
||||
const frameworkOutputPath = path.resolve(__dirname, '../../Terraform/terraform-provider-framework');
|
||||
|
||||
console.log(
|
||||
"\n🏗️ Step 4: Generating Terraform Provider Framework code...",
|
||||
);
|
||||
const frameworkOutputPath = path.resolve(
|
||||
__dirname,
|
||||
"../../Terraform/terraform-provider-framework",
|
||||
);
|
||||
|
||||
FrameworkGenerator.generateAll({
|
||||
specificationPath: providerSpecPath,
|
||||
outputPath: frameworkOutputPath,
|
||||
packageName: 'oneuptime', // Optional: specify a package name
|
||||
packageName: "oneuptime", // Optional: specify a package name
|
||||
});
|
||||
|
||||
console.log('\n🎉 Provider generation completed successfully!');
|
||||
console.log('\n📋 Generated Files:');
|
||||
console.log("\n🎉 Provider generation completed successfully!");
|
||||
console.log("\n📋 Generated Files:");
|
||||
console.log(` 📄 OpenAPI Spec: ${openApiSpecPath}`);
|
||||
console.log(` 📄 Provider Code Spec: ${providerSpecPath}`);
|
||||
console.log(` 📁 Framework Provider Code: ${frameworkOutputPath}`);
|
||||
|
||||
console.log('\n📖 Next Steps:');
|
||||
console.log(' 1. Review the generated Provider Code Specification');
|
||||
console.log(' 2. Customize the specification as needed for your use case');
|
||||
console.log(' 3. Use the Framework Generator to regenerate code after modifications');
|
||||
console.log(' 4. Implement the actual provider logic in the generated Go files');
|
||||
|
||||
FrameworkGenerator.printUsageInfo();
|
||||
|
||||
console.log("\n📖 Next Steps:");
|
||||
console.log(" 1. Review the generated Provider Code Specification");
|
||||
console.log(
|
||||
" 2. Customize the specification as needed for your use case",
|
||||
);
|
||||
console.log(
|
||||
" 3. Use the Framework Generator to regenerate code after modifications",
|
||||
);
|
||||
console.log(
|
||||
" 4. Implement the actual provider logic in the generated Go files",
|
||||
);
|
||||
|
||||
FrameworkGenerator.printUsageInfo();
|
||||
} catch (error) {
|
||||
console.error('\n❌ Error during provider generation:', error);
|
||||
console.error('\n🔍 Troubleshooting Tips:');
|
||||
console.error(' - Ensure Go is installed and properly configured');
|
||||
console.error(' - Check that GOPATH is set correctly');
|
||||
console.error(' - Verify internet connectivity for downloading tools');
|
||||
console.error(' - Make sure you have write permissions in the output directories');
|
||||
console.error("\n❌ Error during provider generation:", error);
|
||||
console.error("\n🔍 Troubleshooting Tips:");
|
||||
console.error(" - Ensure Go is installed and properly configured");
|
||||
console.error(" - Check that GOPATH is set correctly");
|
||||
console.error(" - Verify internet connectivity for downloading tools");
|
||||
console.error(
|
||||
" - Make sure you have write permissions in the output directories",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error('💥 Unexpected error:', err);
|
||||
console.error("💥 Unexpected error:", err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
@@ -1,165 +1,218 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as yaml from 'js-yaml';
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import * as yaml from "js-yaml";
|
||||
|
||||
export default class GeneratorConfig {
|
||||
/**
|
||||
* Generates a generator config for the Terraform provider and writes it to a file.
|
||||
* @param data - The data required to generate the config.
|
||||
* @param data.openApiSpecInJsonFilePath - The OpenAPI specification in JSON format.
|
||||
* @param data.outputPath - The path where the output file will be written.
|
||||
* @param data.outputFileName - The name of the output file.
|
||||
* @param data.providerName - The name of the Terraform provider.
|
||||
*
|
||||
* This implementation generates a minimal valid generator config for the OpenAPI provider spec generator.
|
||||
* You can extend this to add resources, data_sources, and schema options as needed.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Generates a generator config for the Terraform provider and writes it to a file.
|
||||
* @param data - The data required to generate the config.
|
||||
* @param data.openApiSpecInJsonFilePath - The OpenAPI specification in JSON format.
|
||||
* @param data.outputPath - The path where the output file will be written.
|
||||
* @param data.outputFileName - The name of the output file.
|
||||
* @param data.providerName - The name of the Terraform provider.
|
||||
*
|
||||
* This implementation generates a minimal valid generator config for the OpenAPI provider spec generator.
|
||||
* You can extend this to add resources, data_sources, and schema options as needed.
|
||||
*/
|
||||
public static generateGeneratorConfigAndWriteToFile(data: {
|
||||
openApiSpecInJsonFilePath: string;
|
||||
outputPath: string;
|
||||
outputFileName: string;
|
||||
providerName: string;
|
||||
}): void {
|
||||
// Read the OpenAPI spec JSON file
|
||||
const openApiSpec = JSON.parse(
|
||||
fs.readFileSync(data.openApiSpecInJsonFilePath, "utf-8"),
|
||||
);
|
||||
const config: any = {
|
||||
provider: {
|
||||
name: data.providerName,
|
||||
},
|
||||
resources: {},
|
||||
data_sources: {},
|
||||
};
|
||||
|
||||
public static generateGeneratorConfigAndWriteToFile(data: {
|
||||
openApiSpecInJsonFilePath: string,
|
||||
outputPath: string,
|
||||
outputFileName: string,
|
||||
providerName: string,
|
||||
}): void {
|
||||
// Read the OpenAPI spec JSON file
|
||||
const openApiSpec = JSON.parse(fs.readFileSync(data.openApiSpecInJsonFilePath, 'utf-8'));
|
||||
const config: any = {
|
||||
provider: {
|
||||
name: data.providerName
|
||||
},
|
||||
resources: {},
|
||||
data_sources: {}
|
||||
};
|
||||
// Parse OpenAPI paths to generate resources and data sources
|
||||
if (openApiSpec.paths) {
|
||||
for (const [pathKey, pathObj] of Object.entries(openApiSpec.paths)) {
|
||||
for (const [method, opRaw] of Object.entries(pathObj as any)) {
|
||||
const op = opRaw as any;
|
||||
if (
|
||||
!op ||
|
||||
typeof op !== "object" ||
|
||||
typeof op.operationId !== "string"
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Parse OpenAPI paths to generate resources and data sources
|
||||
if (openApiSpec.paths) {
|
||||
for (const [pathKey, pathObj] of Object.entries(openApiSpec.paths)) {
|
||||
for (const [method, opRaw] of Object.entries(pathObj as any)) {
|
||||
const op = opRaw as any;
|
||||
if (!op || typeof op !== 'object' || typeof op.operationId !== 'string') continue;
|
||||
|
||||
const operationId = op.operationId.toLowerCase();
|
||||
const isReadOperation = operationId.startsWith('get') || operationId.startsWith('list') || operationId.startsWith('count') || operationId.includes('read') || operationId.includes('fetch');
|
||||
const isCreateOperation = operationId.startsWith('create') || operationId.startsWith('add') || (method.toLowerCase() === 'post');
|
||||
const isUpdateOperation = operationId.startsWith('update') || operationId.startsWith('put') || (method.toLowerCase() === 'put');
|
||||
const isDeleteOperation = operationId.startsWith('delete') || operationId.includes('remove');
|
||||
const operationId = op.operationId.toLowerCase();
|
||||
const isReadOperation =
|
||||
operationId.startsWith("get") ||
|
||||
operationId.startsWith("list") ||
|
||||
operationId.startsWith("count") ||
|
||||
operationId.includes("read") ||
|
||||
operationId.includes("fetch");
|
||||
const isCreateOperation =
|
||||
operationId.startsWith("create") ||
|
||||
operationId.startsWith("add") ||
|
||||
method.toLowerCase() === "post";
|
||||
const isUpdateOperation =
|
||||
operationId.startsWith("update") ||
|
||||
operationId.startsWith("put") ||
|
||||
method.toLowerCase() === "put";
|
||||
const isDeleteOperation =
|
||||
operationId.startsWith("delete") || operationId.includes("remove");
|
||||
|
||||
if (isReadOperation) {
|
||||
// Generate data source for read operations
|
||||
const dsName = this.extractResourceNameFromPath(pathKey).toLowerCase();
|
||||
if (dsName) {
|
||||
if (!config.data_sources[dsName]) config.data_sources[dsName] = {};
|
||||
config.data_sources[dsName]['read'] = { path: pathKey, method: method.toUpperCase() };
|
||||
}
|
||||
|
||||
// Also add as resource read operation
|
||||
const resourceName = this.extractResourceNameFromPath(pathKey).toLowerCase();
|
||||
if (resourceName) {
|
||||
if (!config.resources[resourceName]) config.resources[resourceName] = {};
|
||||
config.resources[resourceName]['read'] = { path: pathKey, method: method.toUpperCase() };
|
||||
}
|
||||
} else if (isCreateOperation) {
|
||||
// Generate resource for create operations
|
||||
const resourceName = this.extractResourceNameFromPath(pathKey).toLowerCase();
|
||||
if (resourceName) {
|
||||
if (!config.resources[resourceName]) config.resources[resourceName] = {};
|
||||
config.resources[resourceName]['create'] = { path: pathKey, method: method.toUpperCase() };
|
||||
}
|
||||
} else if (isUpdateOperation) {
|
||||
// Generate resource for update operations
|
||||
const resourceName = this.extractResourceNameFromPath(pathKey).toLowerCase();
|
||||
if (resourceName) {
|
||||
if (!config.resources[resourceName]) config.resources[resourceName] = {};
|
||||
config.resources[resourceName]['update'] = { path: pathKey, method: method.toUpperCase() };
|
||||
}
|
||||
} else if (isDeleteOperation) {
|
||||
// Handle delete operations
|
||||
const resourceName = this.extractResourceNameFromPath(pathKey).toLowerCase();
|
||||
if (resourceName) {
|
||||
if (!config.resources[resourceName]) config.resources[resourceName] = {};
|
||||
config.resources[resourceName]['delete'] = { path: pathKey, method: method.toUpperCase() };
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isReadOperation) {
|
||||
// Generate data source for read operations
|
||||
const dsName =
|
||||
this.extractResourceNameFromPath(pathKey).toLowerCase();
|
||||
if (dsName) {
|
||||
if (!config.data_sources[dsName]) {
|
||||
config.data_sources[dsName] = {};
|
||||
}
|
||||
config.data_sources[dsName]["read"] = {
|
||||
path: pathKey,
|
||||
method: method.toUpperCase(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure every resource has both 'create' and 'read' operations
|
||||
// Remove resources that don't have the required operations
|
||||
const resourcesToRemove: string[] = [];
|
||||
|
||||
for (const [resourceName, resourceConfig] of Object.entries(config.resources)) {
|
||||
const resource = resourceConfig as any;
|
||||
|
||||
// If resource doesn't have 'create', try to use 'post' operation
|
||||
if (!resource.create && resource.post) {
|
||||
resource.create = resource.post;
|
||||
delete resource.post;
|
||||
// Also add as resource read operation
|
||||
const resourceName =
|
||||
this.extractResourceNameFromPath(pathKey).toLowerCase();
|
||||
if (resourceName) {
|
||||
if (!config.resources[resourceName]) {
|
||||
config.resources[resourceName] = {};
|
||||
}
|
||||
config.resources[resourceName]["read"] = {
|
||||
path: pathKey,
|
||||
method: method.toUpperCase(),
|
||||
};
|
||||
}
|
||||
|
||||
// If resource doesn't have 'read', try to find it in data sources
|
||||
if (!resource.read) {
|
||||
const matchingDataSource = config.data_sources[resourceName];
|
||||
if (matchingDataSource && matchingDataSource.read) {
|
||||
resource.read = matchingDataSource.read;
|
||||
}
|
||||
} else if (isCreateOperation) {
|
||||
// Generate resource for create operations
|
||||
const resourceName =
|
||||
this.extractResourceNameFromPath(pathKey).toLowerCase();
|
||||
if (resourceName) {
|
||||
if (!config.resources[resourceName]) {
|
||||
config.resources[resourceName] = {};
|
||||
}
|
||||
config.resources[resourceName]["create"] = {
|
||||
path: pathKey,
|
||||
method: method.toUpperCase(),
|
||||
};
|
||||
}
|
||||
|
||||
// If resource still doesn't have both 'create' and 'read', remove it
|
||||
if (!resource.create || !resource.read) {
|
||||
console.log(`Removing resource '${resourceName}' - missing required operations (create: ${!!resource.create}, read: ${!!resource.read})`);
|
||||
resourcesToRemove.push(resourceName);
|
||||
} else if (isUpdateOperation) {
|
||||
// Generate resource for update operations
|
||||
const resourceName =
|
||||
this.extractResourceNameFromPath(pathKey).toLowerCase();
|
||||
if (resourceName) {
|
||||
if (!config.resources[resourceName]) {
|
||||
config.resources[resourceName] = {};
|
||||
}
|
||||
config.resources[resourceName]["update"] = {
|
||||
path: pathKey,
|
||||
method: method.toUpperCase(),
|
||||
};
|
||||
}
|
||||
} else if (isDeleteOperation) {
|
||||
// Handle delete operations
|
||||
const resourceName =
|
||||
this.extractResourceNameFromPath(pathKey).toLowerCase();
|
||||
if (resourceName) {
|
||||
if (!config.resources[resourceName]) {
|
||||
config.resources[resourceName] = {};
|
||||
}
|
||||
config.resources[resourceName]["delete"] = {
|
||||
path: pathKey,
|
||||
method: method.toUpperCase(),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove invalid resources
|
||||
for (const resourceName of resourcesToRemove) {
|
||||
delete config.resources[resourceName];
|
||||
}
|
||||
|
||||
// Remove empty objects
|
||||
if (Object.keys(config.resources).length === 0) delete config.resources;
|
||||
if (Object.keys(config.data_sources).length === 0) delete config.data_sources;
|
||||
|
||||
// Convert the config object to YAML
|
||||
const yamlStr = yaml.dump(config, { noRefs: true, lineWidth: 120 });
|
||||
|
||||
// Ensure output directory exists
|
||||
if (!fs.existsSync(data.outputPath)) {
|
||||
fs.mkdirSync(data.outputPath, { recursive: true });
|
||||
}
|
||||
|
||||
// Write the YAML string to the output file
|
||||
const outputFile = path.join(data.outputPath, data.outputFileName);
|
||||
fs.writeFileSync(outputFile, yamlStr, 'utf-8');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract resource name from API path.
|
||||
* Converts paths like "/alert-custom-field" to "alertcustomfield"
|
||||
* and "/alert-custom-field/{id}" to "alertcustomfield"
|
||||
*/
|
||||
private static extractResourceNameFromPath(path: string): string {
|
||||
// Remove leading slash and anything after the first parameter
|
||||
const pathParts = path.replace(/^\//, '').split('/');
|
||||
let resourcePath = pathParts[0] || '';
|
||||
|
||||
// Handle paths that end with specific patterns like /count, /get-list, etc.
|
||||
if (resourcePath.includes('-count') || resourcePath.includes('-get-list')) {
|
||||
resourcePath = resourcePath.replace(/-count$|-get-list$/, '');
|
||||
// Ensure every resource has both 'create' and 'read' operations
|
||||
// Remove resources that don't have the required operations
|
||||
const resourcesToRemove: string[] = [];
|
||||
|
||||
for (const [resourceName, resourceConfig] of Object.entries(
|
||||
config.resources,
|
||||
)) {
|
||||
const resource = resourceConfig as any;
|
||||
|
||||
// If resource doesn't have 'create', try to use 'post' operation
|
||||
if (!resource.create && resource.post) {
|
||||
resource.create = resource.post;
|
||||
delete resource.post;
|
||||
}
|
||||
|
||||
// If resource doesn't have 'read', try to find it in data sources
|
||||
if (!resource.read) {
|
||||
const matchingDataSource = config.data_sources[resourceName];
|
||||
if (matchingDataSource && matchingDataSource.read) {
|
||||
resource.read = matchingDataSource.read;
|
||||
}
|
||||
|
||||
// Convert kebab-case to snake_case and remove special characters
|
||||
const resourceName = resourcePath
|
||||
.replace(/-/g, '') // Remove hyphens
|
||||
.replace(/[^a-zA-Z0-9]/g, '') // Remove any other special characters
|
||||
.toLowerCase();
|
||||
|
||||
return resourceName;
|
||||
}
|
||||
|
||||
// If resource still doesn't have both 'create' and 'read', remove it
|
||||
if (!resource.create || !resource.read) {
|
||||
console.log(
|
||||
`Removing resource '${resourceName}' - missing required operations (create: ${Boolean(resource.create)}, read: ${Boolean(resource.read)})`,
|
||||
);
|
||||
resourcesToRemove.push(resourceName);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove invalid resources
|
||||
for (const resourceName of resourcesToRemove) {
|
||||
delete config.resources[resourceName];
|
||||
}
|
||||
|
||||
// Remove empty objects
|
||||
if (Object.keys(config.resources).length === 0) {
|
||||
delete config.resources;
|
||||
}
|
||||
if (Object.keys(config.data_sources).length === 0) {
|
||||
delete config.data_sources;
|
||||
}
|
||||
|
||||
// Convert the config object to YAML
|
||||
const yamlStr = yaml.dump(config, { noRefs: true, lineWidth: 120 });
|
||||
|
||||
// Ensure output directory exists
|
||||
if (!fs.existsSync(data.outputPath)) {
|
||||
fs.mkdirSync(data.outputPath, { recursive: true });
|
||||
}
|
||||
|
||||
// Write the YAML string to the output file
|
||||
const outputFile = path.join(data.outputPath, data.outputFileName);
|
||||
fs.writeFileSync(outputFile, yamlStr, "utf-8");
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract resource name from API path.
|
||||
* Converts paths like "/alert-custom-field" to "alertcustomfield"
|
||||
* and "/alert-custom-field/{id}" to "alertcustomfield"
|
||||
*/
|
||||
private static extractResourceNameFromPath(path: string): string {
|
||||
// Remove leading slash and anything after the first parameter
|
||||
const pathParts = path.replace(/^\//, "").split("/");
|
||||
let resourcePath = pathParts[0] || "";
|
||||
|
||||
// Handle paths that end with specific patterns like /count, /get-list, etc.
|
||||
if (resourcePath.includes("-count") || resourcePath.includes("-get-list")) {
|
||||
resourcePath = resourcePath.replace(/-count$|-get-list$/, "");
|
||||
}
|
||||
|
||||
// Convert kebab-case to snake_case and remove special characters
|
||||
const resourceName = resourcePath
|
||||
.replace(/-/g, "") // Remove hyphens
|
||||
.replace(/[^a-zA-Z0-9]/g, "") // Remove any other special characters
|
||||
.toLowerCase();
|
||||
|
||||
return resourceName;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,191 +1,194 @@
|
||||
import { execSync } from 'child_process';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { execSync } from "child_process";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
|
||||
interface InstallResult {
|
||||
success: boolean;
|
||||
message: string;
|
||||
version?: string;
|
||||
success: boolean;
|
||||
message: string;
|
||||
version?: string;
|
||||
}
|
||||
|
||||
class ToolInstaller {
|
||||
private static readonly OPENAPI_TOOL_NAME = 'tfplugingen-openapi';
|
||||
private static readonly OPENAPI_TOOL_PACKAGE = 'github.com/hashicorp/terraform-plugin-codegen-openapi/cmd/tfplugingen-openapi@latest';
|
||||
|
||||
private static readonly FRAMEWORK_TOOL_NAME = 'tfplugingen-framework';
|
||||
private static readonly FRAMEWORK_TOOL_PACKAGE = 'github.com/hashicorp/terraform-plugin-codegen-framework/cmd/tfplugingen-framework@latest';
|
||||
private static readonly OPENAPI_TOOL_NAME = "tfplugingen-openapi";
|
||||
private static readonly OPENAPI_TOOL_PACKAGE =
|
||||
"github.com/hashicorp/terraform-plugin-codegen-openapi/cmd/tfplugingen-openapi@latest";
|
||||
|
||||
public static async installTerraformPluginCodegenOpenAPI(): Promise<InstallResult> {
|
||||
try {
|
||||
console.log('🔧 Installing Terraform Plugin Codegen OpenAPI...');
|
||||
|
||||
// Check if Go is installed
|
||||
if (!this.isGoInstalled()) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'Go is not installed. Please install Go first.'
|
||||
};
|
||||
}
|
||||
private static readonly FRAMEWORK_TOOL_NAME = "tfplugingen-framework";
|
||||
private static readonly FRAMEWORK_TOOL_PACKAGE =
|
||||
"github.com/hashicorp/terraform-plugin-codegen-framework/cmd/tfplugingen-framework@latest";
|
||||
|
||||
// Install the tool
|
||||
console.log(`📦 Running: go install ${this.OPENAPI_TOOL_PACKAGE}`);
|
||||
execSync(`go install ${this.OPENAPI_TOOL_PACKAGE}`, {
|
||||
stdio: 'inherit',
|
||||
timeout: 300000 // 5 minutes timeout
|
||||
});
|
||||
public static async installTerraformPluginCodegenOpenAPI(): Promise<InstallResult> {
|
||||
try {
|
||||
console.log("🔧 Installing Terraform Plugin Codegen OpenAPI...");
|
||||
|
||||
// Verify installation
|
||||
const version = this.getToolVersion(this.OPENAPI_TOOL_NAME);
|
||||
if (version) {
|
||||
console.log('✅ Installation successful!');
|
||||
return {
|
||||
success: true,
|
||||
message: `Successfully installed ${this.OPENAPI_TOOL_NAME}`,
|
||||
version: version
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
message: `Installation completed but ${this.OPENAPI_TOOL_NAME} is not available in PATH`
|
||||
};
|
||||
}
|
||||
// Check if Go is installed
|
||||
if (!this.isGoInstalled()) {
|
||||
return {
|
||||
success: false,
|
||||
message: "Go is not installed. Please install Go first.",
|
||||
};
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Installation failed:', error);
|
||||
return {
|
||||
success: false,
|
||||
message: `Installation failed: ${error instanceof Error ? error.message : 'Unknown error'}`
|
||||
};
|
||||
// Install the tool
|
||||
console.log(`📦 Running: go install ${this.OPENAPI_TOOL_PACKAGE}`);
|
||||
execSync(`go install ${this.OPENAPI_TOOL_PACKAGE}`, {
|
||||
stdio: "inherit",
|
||||
timeout: 300000, // 5 minutes timeout
|
||||
});
|
||||
|
||||
// Verify installation
|
||||
const version = this.getToolVersion(this.OPENAPI_TOOL_NAME);
|
||||
if (version) {
|
||||
console.log("✅ Installation successful!");
|
||||
return {
|
||||
success: true,
|
||||
message: `Successfully installed ${this.OPENAPI_TOOL_NAME}`,
|
||||
version: version,
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
message: `Installation completed but ${this.OPENAPI_TOOL_NAME} is not available in PATH`,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("❌ Installation failed:", error);
|
||||
return {
|
||||
success: false,
|
||||
message: `Installation failed: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public static async installTerraformPluginFrameworkGenerator(): Promise<InstallResult> {
|
||||
try {
|
||||
console.log("🔧 Installing Terraform Plugin Framework Generator...");
|
||||
|
||||
// Check if Go is installed
|
||||
if (!this.isGoInstalled()) {
|
||||
return {
|
||||
success: false,
|
||||
message: "Go is not installed. Please install Go first.",
|
||||
};
|
||||
}
|
||||
|
||||
// Install the tool
|
||||
console.log(`📦 Running: go install ${this.FRAMEWORK_TOOL_PACKAGE}`);
|
||||
execSync(`go install ${this.FRAMEWORK_TOOL_PACKAGE}`, {
|
||||
stdio: "inherit",
|
||||
timeout: 300000, // 5 minutes timeout
|
||||
});
|
||||
|
||||
// Verify installation
|
||||
const version = this.getToolVersion(this.FRAMEWORK_TOOL_NAME);
|
||||
if (version) {
|
||||
console.log("✅ Framework Generator installation successful!");
|
||||
return {
|
||||
success: true,
|
||||
message: `Successfully installed ${this.FRAMEWORK_TOOL_NAME}`,
|
||||
version: version,
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
message: `Installation completed but ${this.FRAMEWORK_TOOL_NAME} is not available in PATH`,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("❌ Framework Generator installation failed:", error);
|
||||
return {
|
||||
success: false,
|
||||
message: `Installation failed: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private static isGoInstalled(): boolean {
|
||||
try {
|
||||
execSync("go version", { stdio: "pipe" });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static getToolVersion(toolName: string): string | null {
|
||||
try {
|
||||
execSync(`${toolName} --help`, {
|
||||
encoding: "utf8",
|
||||
stdio: "pipe",
|
||||
});
|
||||
// The tool might not have a --version flag, so we check if it's available
|
||||
return "latest";
|
||||
} catch {
|
||||
try {
|
||||
// Try to find the binary in GOPATH/bin or GOBIN
|
||||
const goPath = this.getGoPath();
|
||||
const binaryPath = path.join(goPath, "bin", toolName);
|
||||
if (fs.existsSync(binaryPath)) {
|
||||
return "latest";
|
||||
}
|
||||
} catch {
|
||||
// Ignore error
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static async installTerraformPluginFrameworkGenerator(): Promise<InstallResult> {
|
||||
try {
|
||||
console.log('🔧 Installing Terraform Plugin Framework Generator...');
|
||||
|
||||
// Check if Go is installed
|
||||
if (!this.isGoInstalled()) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'Go is not installed. Please install Go first.'
|
||||
};
|
||||
}
|
||||
|
||||
// Install the tool
|
||||
console.log(`📦 Running: go install ${this.FRAMEWORK_TOOL_PACKAGE}`);
|
||||
execSync(`go install ${this.FRAMEWORK_TOOL_PACKAGE}`, {
|
||||
stdio: 'inherit',
|
||||
timeout: 300000 // 5 minutes timeout
|
||||
});
|
||||
|
||||
// Verify installation
|
||||
const version = this.getToolVersion(this.FRAMEWORK_TOOL_NAME);
|
||||
if (version) {
|
||||
console.log('✅ Framework Generator installation successful!');
|
||||
return {
|
||||
success: true,
|
||||
message: `Successfully installed ${this.FRAMEWORK_TOOL_NAME}`,
|
||||
version: version
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
message: `Installation completed but ${this.FRAMEWORK_TOOL_NAME} is not available in PATH`
|
||||
};
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Framework Generator installation failed:', error);
|
||||
return {
|
||||
success: false,
|
||||
message: `Installation failed: ${error instanceof Error ? error.message : 'Unknown error'}`
|
||||
};
|
||||
}
|
||||
private static getGoPath(): string {
|
||||
try {
|
||||
const goPath = execSync("go env GOPATH", { encoding: "utf8" }).trim();
|
||||
return goPath;
|
||||
} catch {
|
||||
// Default GOPATH
|
||||
const homeDir = process.env["HOME"] || process.env["USERPROFILE"] || "";
|
||||
return path.join(homeDir, "go");
|
||||
}
|
||||
}
|
||||
|
||||
private static isGoInstalled(): boolean {
|
||||
try {
|
||||
execSync('go version', { stdio: 'pipe' });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static getToolVersion(toolName: string): string | null {
|
||||
try {
|
||||
execSync(`${toolName} --help`, {
|
||||
encoding: 'utf8',
|
||||
stdio: 'pipe'
|
||||
});
|
||||
// The tool might not have a --version flag, so we check if it's available
|
||||
return 'latest';
|
||||
} catch {
|
||||
try {
|
||||
// Try to find the binary in GOPATH/bin or GOBIN
|
||||
const goPath = this.getGoPath();
|
||||
const binaryPath = path.join(goPath, 'bin', toolName);
|
||||
if (fs.existsSync(binaryPath)) {
|
||||
return 'latest';
|
||||
}
|
||||
} catch {
|
||||
// Ignore error
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static getGoPath(): string {
|
||||
try {
|
||||
const goPath = execSync('go env GOPATH', { encoding: 'utf8' }).trim();
|
||||
return goPath;
|
||||
} catch {
|
||||
// Default GOPATH
|
||||
const homeDir = process.env['HOME'] || process.env['USERPROFILE'] || '';
|
||||
return path.join(homeDir, 'go');
|
||||
}
|
||||
}
|
||||
|
||||
public static printInstallationInfo(): void {
|
||||
console.log('📋 Installation Information:');
|
||||
console.log(` OpenAPI Tool: ${this.OPENAPI_TOOL_NAME}`);
|
||||
console.log(` OpenAPI Package: ${this.OPENAPI_TOOL_PACKAGE}`);
|
||||
console.log(` Framework Tool: ${this.FRAMEWORK_TOOL_NAME}`);
|
||||
console.log(` Framework Package: ${this.FRAMEWORK_TOOL_PACKAGE}`);
|
||||
console.log(' Prerequisites: Go must be installed');
|
||||
console.log(' Usage: Use different methods to install the specific tool needed');
|
||||
console.log('');
|
||||
}
|
||||
public static printInstallationInfo(): void {
|
||||
console.log("📋 Installation Information:");
|
||||
console.log(` OpenAPI Tool: ${this.OPENAPI_TOOL_NAME}`);
|
||||
console.log(` OpenAPI Package: ${this.OPENAPI_TOOL_PACKAGE}`);
|
||||
console.log(` Framework Tool: ${this.FRAMEWORK_TOOL_NAME}`);
|
||||
console.log(` Framework Package: ${this.FRAMEWORK_TOOL_PACKAGE}`);
|
||||
console.log(" Prerequisites: Go must be installed");
|
||||
console.log(
|
||||
" Usage: Use different methods to install the specific tool needed",
|
||||
);
|
||||
console.log("");
|
||||
}
|
||||
}
|
||||
|
||||
// Main execution
|
||||
async function main(): Promise<void> {
|
||||
try {
|
||||
ToolInstaller.printInstallationInfo();
|
||||
|
||||
const result = await ToolInstaller.installTerraformPluginCodegenOpenAPI();
|
||||
|
||||
if (result.success) {
|
||||
console.log(`🎉 ${result.message}`);
|
||||
if (result.version) {
|
||||
console.log(`📌 Version: ${result.version}`);
|
||||
}
|
||||
|
||||
// Print usage instructions
|
||||
console.log('');
|
||||
console.log('📖 Usage Instructions:');
|
||||
console.log(' The tfplugingen-openapi tool is now available in your PATH');
|
||||
console.log(' You can use it to generate Terraform provider code from OpenAPI specs');
|
||||
console.log(' Example: tfplugingen-openapi generate --help');
|
||||
} else {
|
||||
console.error(`💥 ${result.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('🚨 Unexpected error:', error);
|
||||
process.exit(1);
|
||||
try {
|
||||
ToolInstaller.printInstallationInfo();
|
||||
|
||||
const result = await ToolInstaller.installTerraformPluginCodegenOpenAPI();
|
||||
|
||||
if (result.success) {
|
||||
console.log(`🎉 ${result.message}`);
|
||||
if (result.version) {
|
||||
console.log(`📌 Version: ${result.version}`);
|
||||
}
|
||||
|
||||
// Print usage instructions
|
||||
console.log("");
|
||||
console.log("📖 Usage Instructions:");
|
||||
console.log(
|
||||
" The tfplugingen-openapi tool is now available in your PATH",
|
||||
);
|
||||
console.log(
|
||||
" You can use it to generate Terraform provider code from OpenAPI specs",
|
||||
);
|
||||
console.log(" Example: tfplugingen-openapi generate --help");
|
||||
} else {
|
||||
console.error(`💥 ${result.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("🚨 Unexpected error:", error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Export for potential reuse
|
||||
@@ -193,5 +196,5 @@ export { ToolInstaller, InstallResult };
|
||||
|
||||
// Run if this file is executed directly
|
||||
if (require.main === module) {
|
||||
main();
|
||||
}
|
||||
main();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { execSync } from 'child_process';
|
||||
import path from 'path';
|
||||
import { execSync } from "child_process";
|
||||
import path from "path";
|
||||
|
||||
export default class ProviderSpec {
|
||||
public static generateTerraformProviderCode(options: {
|
||||
@@ -8,18 +8,30 @@ export default class ProviderSpec {
|
||||
outputPath: string;
|
||||
}): void {
|
||||
// Get the Go path and construct the full path to the tfplugingen-openapi binary
|
||||
const goPath = execSync('go env GOPATH', { encoding: 'utf8' }).trim();
|
||||
const tfplugigenBinaryPath = path.join(goPath, 'bin', 'tfplugingen-openapi');
|
||||
const goPath = execSync("go env GOPATH", { encoding: "utf8" }).trim();
|
||||
const tfplugigenBinaryPath = path.join(
|
||||
goPath,
|
||||
"bin",
|
||||
"tfplugingen-openapi",
|
||||
);
|
||||
|
||||
const command = `"${tfplugigenBinaryPath}" generate --config "${options.generatorConfigPath}" --output "${options.outputPath}" "${options.openApiSpecPath}"`;
|
||||
|
||||
try {
|
||||
execSync(command, { stdio: 'inherit' });
|
||||
execSync(command, { stdio: "inherit" });
|
||||
} catch (error) {
|
||||
console.error('Error executing Terraform provider code generation command:', error);
|
||||
throw new Error(`Failed to generate Terraform provider code: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
console.error(
|
||||
"Error executing Terraform provider code generation command:",
|
||||
error,
|
||||
);
|
||||
throw new Error(
|
||||
`Failed to generate Terraform provider code: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||
);
|
||||
}
|
||||
|
||||
console.log('Terraform provider code generated successfully at:', options.outputPath);
|
||||
console.log(
|
||||
"Terraform provider code generated successfully at:",
|
||||
options.outputPath,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,392 +1,451 @@
|
||||
import * as fs from 'fs';
|
||||
import * as fs from "fs";
|
||||
|
||||
interface OpenAPISpec {
|
||||
openapi: string;
|
||||
info: {
|
||||
title: string;
|
||||
version: string;
|
||||
description?: string;
|
||||
};
|
||||
paths: Record<string, any>;
|
||||
components?: {
|
||||
schemas?: Record<string, any>;
|
||||
};
|
||||
openapi: string;
|
||||
info: {
|
||||
title: string;
|
||||
version: string;
|
||||
description?: string;
|
||||
};
|
||||
paths: Record<string, any>;
|
||||
components?: {
|
||||
schemas?: Record<string, any>;
|
||||
};
|
||||
}
|
||||
|
||||
interface ProviderCodeSpecification {
|
||||
version: string;
|
||||
provider: {
|
||||
name: string;
|
||||
schema?: {
|
||||
attributes?: any[];
|
||||
blocks?: any[];
|
||||
};
|
||||
version: string;
|
||||
provider: {
|
||||
name: string;
|
||||
schema?: {
|
||||
attributes?: any[];
|
||||
blocks?: any[];
|
||||
};
|
||||
resources?: Array<{
|
||||
name: string;
|
||||
schema: {
|
||||
attributes?: any[];
|
||||
blocks?: any[];
|
||||
};
|
||||
}>;
|
||||
datasources?: Array<{
|
||||
name: string;
|
||||
schema: {
|
||||
attributes?: any[];
|
||||
blocks?: any[];
|
||||
};
|
||||
}>;
|
||||
};
|
||||
resources?: Array<{
|
||||
name: string;
|
||||
schema: {
|
||||
attributes?: any[];
|
||||
blocks?: any[];
|
||||
};
|
||||
}>;
|
||||
datasources?: Array<{
|
||||
name: string;
|
||||
schema: {
|
||||
attributes?: any[];
|
||||
blocks?: any[];
|
||||
};
|
||||
}>;
|
||||
}
|
||||
|
||||
export default class SpecificationConverter {
|
||||
/**
|
||||
* Convert OpenAPI specification to Provider Code Specification
|
||||
*/
|
||||
public static convertOpenAPIToProviderSpec(options: {
|
||||
openApiSpecPath: string;
|
||||
outputPath: string;
|
||||
providerName: string;
|
||||
}): void {
|
||||
try {
|
||||
console.log('🔄 Converting OpenAPI spec to Provider Code Specification...');
|
||||
console.log(`📄 Input OpenAPI spec: ${options.openApiSpecPath}`);
|
||||
console.log(`📁 Output path: ${options.outputPath}`);
|
||||
/**
|
||||
* Convert OpenAPI specification to Provider Code Specification
|
||||
*/
|
||||
public static convertOpenAPIToProviderSpec(options: {
|
||||
openApiSpecPath: string;
|
||||
outputPath: string;
|
||||
providerName: string;
|
||||
}): void {
|
||||
try {
|
||||
console.log(
|
||||
"🔄 Converting OpenAPI spec to Provider Code Specification...",
|
||||
);
|
||||
console.log(`📄 Input OpenAPI spec: ${options.openApiSpecPath}`);
|
||||
console.log(`📁 Output path: ${options.outputPath}`);
|
||||
|
||||
// Read OpenAPI specification
|
||||
const openApiContent = fs.readFileSync(options.openApiSpecPath, 'utf8');
|
||||
const openApiSpec: OpenAPISpec = JSON.parse(openApiContent);
|
||||
// Read OpenAPI specification
|
||||
const openApiContent = fs.readFileSync(options.openApiSpecPath, "utf8");
|
||||
const openApiSpec: OpenAPISpec = JSON.parse(openApiContent);
|
||||
|
||||
// Generate Provider Code Specification
|
||||
const providerSpec = this.generateProviderSpecification(openApiSpec, options.providerName);
|
||||
// Generate Provider Code Specification
|
||||
const providerSpec = this.generateProviderSpecification(
|
||||
openApiSpec,
|
||||
options.providerName,
|
||||
);
|
||||
|
||||
// Write specification to file
|
||||
const outputContent = JSON.stringify(providerSpec, null, 2);
|
||||
fs.writeFileSync(options.outputPath, outputContent, 'utf8');
|
||||
// Write specification to file
|
||||
const outputContent = JSON.stringify(providerSpec, null, 2);
|
||||
fs.writeFileSync(options.outputPath, outputContent, "utf8");
|
||||
|
||||
console.log('✅ Successfully converted OpenAPI spec to Provider Code Specification');
|
||||
console.log(`📝 Generated specification saved to: ${options.outputPath}`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Error converting specification:', error);
|
||||
throw new Error(`Failed to convert specification: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
}
|
||||
console.log(
|
||||
"✅ Successfully converted OpenAPI spec to Provider Code Specification",
|
||||
);
|
||||
console.log(`📝 Generated specification saved to: ${options.outputPath}`);
|
||||
} catch (error) {
|
||||
console.error("❌ Error converting specification:", error);
|
||||
throw new Error(
|
||||
`Failed to convert specification: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private static generateProviderSpecification(openApiSpec: OpenAPISpec, providerName: string): ProviderCodeSpecification {
|
||||
const providerSpec: ProviderCodeSpecification = {
|
||||
version: "0.1",
|
||||
provider: {
|
||||
name: providerName,
|
||||
schema: {
|
||||
attributes: [
|
||||
// Basic provider configuration attributes
|
||||
{
|
||||
name: "api_url",
|
||||
string: {
|
||||
optional_required: "optional",
|
||||
description: "The base URL for the API"
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "api_key",
|
||||
string: {
|
||||
optional_required: "optional",
|
||||
sensitive: true,
|
||||
description: "API key for authentication"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
resources: [],
|
||||
datasources: []
|
||||
};
|
||||
|
||||
// Extract resources and data sources from OpenAPI paths
|
||||
if (openApiSpec.paths) {
|
||||
const { resources, datasources } = this.extractResourcesAndDataSources(openApiSpec);
|
||||
providerSpec.resources = resources;
|
||||
providerSpec.datasources = datasources;
|
||||
}
|
||||
|
||||
return providerSpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize resource name to follow Terraform naming conventions
|
||||
* - Must start with lowercase letter or underscore
|
||||
* - Can only contain lowercase letters, numbers, and underscores
|
||||
* - Convert hyphens to underscores
|
||||
* - Convert to lowercase
|
||||
*/
|
||||
private static sanitizeResourceName(name: string): string {
|
||||
return name
|
||||
.toLowerCase()
|
||||
.replace(/-/g, '_') // Replace hyphens with underscores
|
||||
.replace(/[^a-z0-9_]/g, '_') // Replace any other invalid characters with underscores
|
||||
.replace(/^[0-9]/, '_$&') // If it starts with a number, prefix with underscore
|
||||
.replace(/_+/g, '_'); // Replace multiple consecutive underscores with single underscore
|
||||
}
|
||||
|
||||
private static extractResourcesAndDataSources(openApiSpec: OpenAPISpec): {
|
||||
resources: Array<{ name: string; schema: any }>;
|
||||
datasources: Array<{ name: string; schema: any }>;
|
||||
} {
|
||||
const resources: Array<{ name: string; schema: any }> = [];
|
||||
const datasources: Array<{ name: string; schema: any }> = [];
|
||||
|
||||
// Analyze OpenAPI paths to determine resources and data sources
|
||||
for (const [pathKey, pathValue] of Object.entries(openApiSpec.paths)) {
|
||||
if (!pathValue || typeof pathValue !== 'object') continue;
|
||||
|
||||
// Extract resource name from path (e.g., /api/v1/monitor -> monitor)
|
||||
const pathSegments = pathKey.split('/').filter(segment =>
|
||||
segment &&
|
||||
!segment.startsWith('{') &&
|
||||
segment !== 'api' &&
|
||||
!segment.match(/^v\d+$/)
|
||||
);
|
||||
|
||||
if (pathSegments.length === 0) continue;
|
||||
|
||||
const lastSegment = pathSegments[pathSegments.length - 1];
|
||||
if (!lastSegment) continue;
|
||||
|
||||
const resourceName = this.sanitizeResourceName(lastSegment);
|
||||
if (!resourceName) continue;
|
||||
|
||||
// Sanitize resource name to be Terraform-compatible
|
||||
const sanitizedResourceName = this.sanitizeResourceName(resourceName);
|
||||
|
||||
// Determine if this is a resource (has POST/PUT/DELETE) or data source (only GET)
|
||||
const methods = Object.keys(pathValue);
|
||||
const hasWriteOperations = methods.some(method =>
|
||||
['post', 'put', 'patch', 'delete'].includes(method.toLowerCase())
|
||||
);
|
||||
|
||||
const schema = this.generateSchemaFromPath(pathValue, openApiSpec.components?.schemas);
|
||||
|
||||
if (hasWriteOperations) {
|
||||
// This is a resource
|
||||
if (!resources.find(r => r.name === sanitizedResourceName)) {
|
||||
resources.push({
|
||||
name: sanitizedResourceName,
|
||||
schema: schema
|
||||
});
|
||||
}
|
||||
} else if (methods.includes('get')) {
|
||||
// This is a data source
|
||||
if (!datasources.find(d => d.name === sanitizedResourceName)) {
|
||||
datasources.push({
|
||||
name: sanitizedResourceName,
|
||||
schema: schema
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { resources, datasources };
|
||||
}
|
||||
|
||||
private static generateSchemaFromPath(pathSpec: any, schemas?: Record<string, any>): any {
|
||||
// Generate a basic schema structure
|
||||
// This is a simplified implementation - you may want to enhance this based on your specific needs
|
||||
const attributes = [
|
||||
private static generateProviderSpecification(
|
||||
openApiSpec: OpenAPISpec,
|
||||
providerName: string,
|
||||
): ProviderCodeSpecification {
|
||||
const providerSpec: ProviderCodeSpecification = {
|
||||
version: "0.1",
|
||||
provider: {
|
||||
name: providerName,
|
||||
schema: {
|
||||
attributes: [
|
||||
// Basic provider configuration attributes
|
||||
{
|
||||
name: "api_url",
|
||||
string: {
|
||||
optional_required: "optional",
|
||||
description: "The base URL for the API",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "api_key",
|
||||
string: {
|
||||
optional_required: "optional",
|
||||
sensitive: true,
|
||||
description: "API key for authentication",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
resources: [],
|
||||
datasources: [],
|
||||
};
|
||||
|
||||
// Extract resources and data sources from OpenAPI paths
|
||||
if (openApiSpec.paths) {
|
||||
const { resources, datasources } =
|
||||
this.extractResourcesAndDataSources(openApiSpec);
|
||||
providerSpec.resources = resources;
|
||||
providerSpec.datasources = datasources;
|
||||
}
|
||||
|
||||
return providerSpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize resource name to follow Terraform naming conventions
|
||||
* - Must start with lowercase letter or underscore
|
||||
* - Can only contain lowercase letters, numbers, and underscores
|
||||
* - Convert hyphens to underscores
|
||||
* - Convert to lowercase
|
||||
*/
|
||||
private static sanitizeResourceName(name: string): string {
|
||||
return name
|
||||
.toLowerCase()
|
||||
.replace(/-/g, "_") // Replace hyphens with underscores
|
||||
.replace(/[^a-z0-9_]/g, "_") // Replace any other invalid characters with underscores
|
||||
.replace(/^[0-9]/, "_$&") // If it starts with a number, prefix with underscore
|
||||
.replace(/_+/g, "_"); // Replace multiple consecutive underscores with single underscore
|
||||
}
|
||||
|
||||
private static extractResourcesAndDataSources(openApiSpec: OpenAPISpec): {
|
||||
resources: Array<{ name: string; schema: any }>;
|
||||
datasources: Array<{ name: string; schema: any }>;
|
||||
} {
|
||||
const resources: Array<{ name: string; schema: any }> = [];
|
||||
const datasources: Array<{ name: string; schema: any }> = [];
|
||||
|
||||
// Analyze OpenAPI paths to determine resources and data sources
|
||||
for (const [pathKey, pathValue] of Object.entries(openApiSpec.paths)) {
|
||||
if (!pathValue || typeof pathValue !== "object") {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Extract resource name from path (e.g., /api/v1/monitor -> monitor)
|
||||
const pathSegments = pathKey.split("/").filter((segment) => {
|
||||
return (
|
||||
segment &&
|
||||
!segment.startsWith("{") &&
|
||||
segment !== "api" &&
|
||||
!segment.match(/^v\d+$/)
|
||||
);
|
||||
});
|
||||
|
||||
if (pathSegments.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const lastSegment = pathSegments[pathSegments.length - 1];
|
||||
if (!lastSegment) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const resourceName = this.sanitizeResourceName(lastSegment);
|
||||
if (!resourceName) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Sanitize resource name to be Terraform-compatible
|
||||
const sanitizedResourceName = this.sanitizeResourceName(resourceName);
|
||||
|
||||
// Determine if this is a resource (has POST/PUT/DELETE) or data source (only GET)
|
||||
const methods = Object.keys(pathValue);
|
||||
const hasWriteOperations = methods.some((method) => {
|
||||
return ["post", "put", "patch", "delete"].includes(
|
||||
method.toLowerCase(),
|
||||
);
|
||||
});
|
||||
|
||||
const schema = this.generateSchemaFromPath(
|
||||
pathValue,
|
||||
openApiSpec.components?.schemas,
|
||||
);
|
||||
|
||||
if (hasWriteOperations) {
|
||||
// This is a resource
|
||||
if (
|
||||
!resources.find((r) => {
|
||||
return r.name === sanitizedResourceName;
|
||||
})
|
||||
) {
|
||||
resources.push({
|
||||
name: sanitizedResourceName,
|
||||
schema: schema,
|
||||
});
|
||||
}
|
||||
} else if (methods.includes("get")) {
|
||||
// This is a data source
|
||||
if (
|
||||
!datasources.find((d) => {
|
||||
return d.name === sanitizedResourceName;
|
||||
})
|
||||
) {
|
||||
datasources.push({
|
||||
name: sanitizedResourceName,
|
||||
schema: schema,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { resources, datasources };
|
||||
}
|
||||
|
||||
private static generateSchemaFromPath(
|
||||
pathSpec: any,
|
||||
schemas?: Record<string, any>,
|
||||
): any {
|
||||
// Generate a basic schema structure
|
||||
// This is a simplified implementation - you may want to enhance this based on your specific needs
|
||||
const attributes = [
|
||||
{
|
||||
name: "id",
|
||||
string: {
|
||||
computed_optional_required: "computed",
|
||||
description: "The unique identifier",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "name",
|
||||
string: {
|
||||
computed_optional_required: "required",
|
||||
description: "The name of the resource",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "description",
|
||||
string: {
|
||||
computed_optional_required: "optional",
|
||||
description: "The description of the resource",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// Try to extract more attributes from request/response schemas if available
|
||||
if (pathSpec.post?.requestBody?.content?.["application/json"]?.schema) {
|
||||
const requestSchema =
|
||||
pathSpec.post.requestBody.content["application/json"].schema;
|
||||
const extractedAttributes = this.extractAttributesFromSchema(
|
||||
requestSchema,
|
||||
schemas,
|
||||
);
|
||||
attributes.push(...extractedAttributes);
|
||||
}
|
||||
|
||||
return {
|
||||
attributes: attributes,
|
||||
};
|
||||
}
|
||||
|
||||
private static extractAttributesFromSchema(
|
||||
schema: any,
|
||||
_allSchemas?: Record<string, any>,
|
||||
): any[] {
|
||||
const attributes: any[] = [];
|
||||
|
||||
if (schema.properties) {
|
||||
for (const [propName, propSchema] of Object.entries(schema.properties)) {
|
||||
if (typeof propSchema !== "object") {
|
||||
continue;
|
||||
}
|
||||
|
||||
const attribute = this.convertPropertyToAttribute(
|
||||
propName,
|
||||
propSchema as any,
|
||||
);
|
||||
if (attribute) {
|
||||
attributes.push(attribute);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return attributes;
|
||||
}
|
||||
|
||||
private static convertPropertyToAttribute(
|
||||
name: string,
|
||||
schema: any,
|
||||
): any | null {
|
||||
// Skip id field as it's typically computed
|
||||
if (name === "id") {
|
||||
return null;
|
||||
}
|
||||
|
||||
let attributeType: any;
|
||||
const computedOptionalRequired = "optional";
|
||||
|
||||
switch (schema.type) {
|
||||
case "string":
|
||||
attributeType = { string: {} };
|
||||
break;
|
||||
case "integer":
|
||||
case "number":
|
||||
attributeType = { int64: {} };
|
||||
break;
|
||||
case "boolean":
|
||||
attributeType = { bool: {} };
|
||||
break;
|
||||
case "array":
|
||||
if (schema.items?.type === "string") {
|
||||
attributeType = {
|
||||
list: {
|
||||
element_type: { string: {} },
|
||||
},
|
||||
};
|
||||
}
|
||||
break;
|
||||
case "object":
|
||||
// For objects, create a simplified structure
|
||||
attributeType = {
|
||||
object: {
|
||||
attribute_types: [
|
||||
{
|
||||
name: "value",
|
||||
string: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
break;
|
||||
default:
|
||||
// Default to string for unknown types
|
||||
attributeType = { string: {} };
|
||||
}
|
||||
|
||||
if (!attributeType) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const typeKey = Object.keys(attributeType)[0];
|
||||
if (!typeKey) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
name: name,
|
||||
[typeKey]: {
|
||||
...attributeType[typeKey],
|
||||
computed_optional_required: computedOptionalRequired,
|
||||
description: schema.description || `The ${name} field`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a basic Provider Code Specification template
|
||||
*/
|
||||
public static generateBasicTemplate(options: {
|
||||
providerName: string;
|
||||
outputPath: string;
|
||||
}): void {
|
||||
const basicSpec: ProviderCodeSpecification = {
|
||||
version: "0.1",
|
||||
provider: {
|
||||
name: options.providerName,
|
||||
schema: {
|
||||
attributes: [
|
||||
{
|
||||
name: "api_url",
|
||||
string: {
|
||||
optional_required: "optional",
|
||||
description: "The base URL for the API",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "api_key",
|
||||
string: {
|
||||
optional_required: "optional",
|
||||
sensitive: true,
|
||||
description: "API key for authentication",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
resources: [
|
||||
{
|
||||
name: "example_resource",
|
||||
schema: {
|
||||
attributes: [
|
||||
{
|
||||
name: "id",
|
||||
string: {
|
||||
computed_optional_required: "computed",
|
||||
description: "The unique identifier"
|
||||
}
|
||||
},
|
||||
{
|
||||
computed_optional_required: "computed",
|
||||
description: "The unique identifier",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "name",
|
||||
string: {
|
||||
computed_optional_required: "required",
|
||||
description: "The name of the resource"
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "description",
|
||||
string: {
|
||||
computed_optional_required: "optional",
|
||||
description: "The description of the resource"
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
// Try to extract more attributes from request/response schemas if available
|
||||
if (pathSpec.post?.requestBody?.content?.['application/json']?.schema) {
|
||||
const requestSchema = pathSpec.post.requestBody.content['application/json'].schema;
|
||||
const extractedAttributes = this.extractAttributesFromSchema(requestSchema, schemas);
|
||||
attributes.push(...extractedAttributes);
|
||||
}
|
||||
|
||||
return {
|
||||
attributes: attributes
|
||||
};
|
||||
}
|
||||
|
||||
private static extractAttributesFromSchema(schema: any, _allSchemas?: Record<string, any>): any[] {
|
||||
const attributes: any[] = [];
|
||||
|
||||
if (schema.properties) {
|
||||
for (const [propName, propSchema] of Object.entries(schema.properties)) {
|
||||
if (typeof propSchema !== 'object') continue;
|
||||
|
||||
const attribute = this.convertPropertyToAttribute(propName, propSchema as any);
|
||||
if (attribute) {
|
||||
attributes.push(attribute);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return attributes;
|
||||
}
|
||||
|
||||
private static convertPropertyToAttribute(name: string, schema: any): any | null {
|
||||
// Skip id field as it's typically computed
|
||||
if (name === 'id') return null;
|
||||
|
||||
let attributeType: any;
|
||||
let computedOptionalRequired = 'optional';
|
||||
|
||||
switch (schema.type) {
|
||||
case 'string':
|
||||
attributeType = { string: {} };
|
||||
break;
|
||||
case 'integer':
|
||||
case 'number':
|
||||
attributeType = { int64: {} };
|
||||
break;
|
||||
case 'boolean':
|
||||
attributeType = { bool: {} };
|
||||
break;
|
||||
case 'array':
|
||||
if (schema.items?.type === 'string') {
|
||||
attributeType = {
|
||||
list: {
|
||||
element_type: { string: {} }
|
||||
}
|
||||
};
|
||||
}
|
||||
break;
|
||||
case 'object':
|
||||
// For objects, create a simplified structure
|
||||
attributeType = {
|
||||
object: {
|
||||
attribute_types: [
|
||||
{
|
||||
name: "value",
|
||||
string: {}
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
break;
|
||||
default:
|
||||
// Default to string for unknown types
|
||||
attributeType = { string: {} };
|
||||
}
|
||||
|
||||
if (!attributeType) return null;
|
||||
|
||||
const typeKey = Object.keys(attributeType)[0];
|
||||
if (!typeKey) return null;
|
||||
|
||||
return {
|
||||
name: name,
|
||||
[typeKey]: {
|
||||
...attributeType[typeKey],
|
||||
computed_optional_required: computedOptionalRequired,
|
||||
description: schema.description || `The ${name} field`
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a basic Provider Code Specification template
|
||||
*/
|
||||
public static generateBasicTemplate(options: {
|
||||
providerName: string;
|
||||
outputPath: string;
|
||||
}): void {
|
||||
const basicSpec: ProviderCodeSpecification = {
|
||||
version: "0.1",
|
||||
provider: {
|
||||
name: options.providerName,
|
||||
schema: {
|
||||
attributes: [
|
||||
{
|
||||
name: "api_url",
|
||||
string: {
|
||||
optional_required: "optional",
|
||||
description: "The base URL for the API"
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "api_key",
|
||||
string: {
|
||||
optional_required: "optional",
|
||||
sensitive: true,
|
||||
description: "API key for authentication"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
resources: [
|
||||
{
|
||||
name: "example_resource",
|
||||
schema: {
|
||||
attributes: [
|
||||
{
|
||||
name: "id",
|
||||
string: {
|
||||
computed_optional_required: "computed",
|
||||
description: "The unique identifier"
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "name",
|
||||
string: {
|
||||
computed_optional_required: "required",
|
||||
description: "The name of the resource"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
computed_optional_required: "required",
|
||||
description: "The name of the resource",
|
||||
},
|
||||
},
|
||||
],
|
||||
datasources: [
|
||||
{
|
||||
name: "example_data_source",
|
||||
schema: {
|
||||
attributes: [
|
||||
{
|
||||
name: "id",
|
||||
string: {
|
||||
computed_optional_required: "computed",
|
||||
description: "The unique identifier"
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "name",
|
||||
string: {
|
||||
computed_optional_required: "required",
|
||||
description: "The name to search for"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
},
|
||||
},
|
||||
],
|
||||
datasources: [
|
||||
{
|
||||
name: "example_data_source",
|
||||
schema: {
|
||||
attributes: [
|
||||
{
|
||||
name: "id",
|
||||
string: {
|
||||
computed_optional_required: "computed",
|
||||
description: "The unique identifier",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "name",
|
||||
string: {
|
||||
computed_optional_required: "required",
|
||||
description: "The name to search for",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const content = JSON.stringify(basicSpec, null, 2);
|
||||
fs.writeFileSync(options.outputPath, content, 'utf8');
|
||||
|
||||
console.log('✅ Generated basic Provider Code Specification template');
|
||||
console.log(`📝 Template saved to: ${options.outputPath}`);
|
||||
}
|
||||
const content = JSON.stringify(basicSpec, null, 2);
|
||||
fs.writeFileSync(options.outputPath, content, "utf8");
|
||||
|
||||
console.log("✅ Generated basic Provider Code Specification template");
|
||||
console.log(`📝 Template saved to: ${options.outputPath}`);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user