feat: Capture OpenAPI format information and preserve original values for binary fields in resource generation

This commit is contained in:
Simon Larsen
2025-06-26 12:34:10 +01:00
parent fccfb5b026
commit df4bc5ce12
3 changed files with 56 additions and 4 deletions

View File

@@ -570,6 +570,7 @@ export class OpenAPIParser {
const prop: any = propSchema as any;
let propType: string = prop.type || "string";
let propFormat: string | undefined = prop.format; // Capture the format field
let description: string = prop.description || "";
let example: any = prop.example;
let defaultValue: any = prop.default;
@@ -579,6 +580,7 @@ export class OpenAPIParser {
const resolvedProp: any = this.resolveSchemaRef(prop.$ref);
if (resolvedProp) {
propType = resolvedProp.type || "string";
propFormat = resolvedProp.format || propFormat; // Also capture format from resolved refs
description = resolvedProp.description || description;
example = resolvedProp.example || example;
defaultValue = resolvedProp.default || defaultValue;
@@ -661,6 +663,7 @@ export class OpenAPIParser {
example: example, // Extract example from OpenAPI schema
default: defaultValue, // Extract default value from OpenAPI schema
isComplexObject: propType === "object", // Flag to indicate this string field is actually a complex object
...(propFormat && { format: propFormat }), // Only include format if it exists
};
}
}

View File

@@ -592,6 +592,8 @@ func (r *${resourceTypeName}Resource) Create(ctx context.Context, req resource.C
return
}
${this.generateOriginalValueStorage(resource)}
// Create API request body
${resourceVarName}Request := map[string]interface{}{
"data": map[string]interface{}{
@@ -614,7 +616,7 @@ ${this.generateRequestBody(resource)}
}
// Update the model with response data
${this.generateResponseMapping(resource, resourceVarName + "Response")}
${this.generateResponseMapping(resource, resourceVarName + "Response", true)}
// Write logs using the tflog package
tflog.Trace(ctx, "created a resource")
@@ -693,7 +695,7 @@ ${this.generateSelectParameter(resource)}
}
// Update the model with response data
${this.generateResponseMapping(resource, resourceVarName + "Response")}
${this.generateResponseMapping(resource, resourceVarName + "Response", false)}
// Save updated data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
@@ -829,7 +831,7 @@ ${this.generateSelectParameter(resource)}
}
// Update the model with response data from the Read operation
${this.generateResponseMapping(resource, "readResponse")}
${this.generateResponseMapping(resource, "readResponse", false)}
// Save updated data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
@@ -1122,6 +1124,7 @@ func (r *${resourceTypeName}Resource) Delete(ctx context.Context, req resource.D
private generateResponseMapping(
resource: TerraformResource,
responseVar: string,
isCreateMethod: boolean = false,
): string {
const mappings: string[] = [];
@@ -1168,6 +1171,9 @@ func (r *${resourceTypeName}Resource) Delete(ctx context.Context, req resource.D
`dataMap["${apiFieldName}"]`,
attr.default !== undefined && attr.default !== null, // hasDefault
attr.isComplexObject || false, // isComplexObject
attr.format, // format
isCreateMethod, // isCreateMethod
sanitizedName, // fieldName for original value preservation
);
mappings.push(` ${setter}`);
}
@@ -1189,10 +1195,35 @@ func (r *${resourceTypeName}Resource) Delete(ctx context.Context, req resource.D
responseValue: string,
hasDefault: boolean = false,
isComplexObject: boolean = false,
format?: string,
isCreateMethod: boolean = false,
originalFieldName?: string,
): string {
switch (terraformType) {
case "string":
if (isComplexObject) {
// Handle binary format fields (like base64 file content) specially
if (format === "binary") {
// For binary fields, treat the response as a simple string without complex object processing
if (isCreateMethod && originalFieldName) {
// In Create method, preserve original value if API doesn't return the file content
return `if val, ok := ${responseValue}.(string); ok {
${fieldName} = types.StringValue(val)
} else {
// Preserve original value from the request since API doesn't return file content
${fieldName} = types.StringValue(original${StringUtils.toPascalCase(originalFieldName)}Value)
}`;
} else {
// In Read/Update methods, preserve existing value if not present in API response
// This prevents drift detection when API doesn't return binary content
return `if val, ok := ${responseValue}.(string); ok {
${fieldName} = types.StringValue(val)
} else {
// Keep existing value to prevent drift - API doesn't return binary content
// ${fieldName} value is already set from the existing state
}`;
}
} else if (isComplexObject) {
// For complex object strings, convert API object response to JSON string
return `if val, ok := ${responseValue}.(map[string]interface{}); ok {
if jsonBytes, err := json.Marshal(val); err == nil {
@@ -1384,4 +1415,21 @@ ${resourceFunctions}
resourceListContent,
);
}
private generateOriginalValueStorage(resource: TerraformResource): string {
const storage: string[] = [];
// Find binary format fields and store their original values
for (const [name, attr] of Object.entries(resource.schema)) {
if (attr.format === "binary") {
const sanitizedName: string = this.sanitizeAttributeName(name);
const fieldName: string = StringUtils.toPascalCase(sanitizedName);
storage.push(` // Store the original ${sanitizedName} value since API won't return it`);
storage.push(` original${fieldName}Value := data.${fieldName}.ValueString()`);
storage.push(``);
}
}
return storage.join("\n");
}
}

View File

@@ -114,6 +114,7 @@ export interface TerraformAttribute {
apiFieldName?: string; // Original OpenAPI field name for API requests
example?: any; // Example value from OpenAPI spec
isComplexObject?: boolean; // Flag to indicate this string field is actually a complex object
format?: string; // OpenAPI format information (e.g., "binary", "date-time", etc.)
}
export interface GoType {