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:
Simon Larsen
2025-06-13 12:07:22 +01:00
parent d4225d35a0
commit 3234f730fe
7 changed files with 1025 additions and 860 deletions

View File

@@ -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);

View File

@@ -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("");
}
}

View File

@@ -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);
});

View File

@@ -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;
}
}

View File

@@ -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();
}

View File

@@ -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,
);
}
}
}

View File

@@ -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}`);
}
}