Skip to content
Open
406 changes: 405 additions & 1 deletion examples/test_spec/twilio_response_v1.yaml

Large diffs are not rendered by default.

38 changes: 34 additions & 4 deletions src/main/java/com/twilio/oai/TwilioPhpGenerator.java
Original file line number Diff line number Diff line change
Expand Up @@ -134,9 +134,27 @@ public OperationsMap postProcessOperationsWithModels(final OperationsMap objs, L
final List<CodegenOperation> opList = directoryStructureService.processOperations(results);
PhpApiResources apiResources = processCodegenOperations(opList);
results.put("resources", apiResources);

// Generate dynamic instance class files (one per response model)
generateDynamicInstanceFiles(apiResources, results);

return results;
}

private void generateDynamicInstanceFiles(PhpApiResources apiResources, OperationsMap objs) {
// Build base context with all properties needed by the template
Map<String, Object> baseContext = new HashMap<>();
baseContext.putAll(additionalProperties);
baseContext.put("resources", apiResources);
// domainName and version are already in additionalProperties (set by TwilioCodegenAdapter)

// Get output directory for API files
String outputDir = apiFileFolder();

// Generate the dynamic files
phpApiActionTemplate.generateDynamicFiles(baseContext, outputDir);
}

@Override
public String getName() {
return EnumConstants.Generator.TWILIO_PHP.getValue();
Expand All @@ -162,14 +180,26 @@ private PhpApiResources processCodegenOperations(List<CodegenOperation> opList)
CodegenModelResolver codegenModelResolver = new CodegenModelResolver(conventionMapper, modelFormatMap,
Arrays.asList(EnumConstants.JavaDataTypes.values()));
PhpPropertyResolver phpPropertyResolver = new PhpPropertyResolver(conventionMapper);
return new PhpApiResourceBuilder(phpApiActionTemplate, opList, this.allModels, twilioCodegen.getToggles(JSON_INGRESS), phpPropertyResolver)
.addVersionLessTemplates(openAPI, directoryStructureService)

// Create the builder and configure it
PhpApiResourceBuilder builder = new PhpApiResourceBuilder(phpApiActionTemplate, opList, this.allModels, twilioCodegen.getToggles(JSON_INGRESS), phpPropertyResolver);
builder.addVersionLessTemplates(openAPI, directoryStructureService)
.updateAdditionalProps(directoryStructureService)
.updateOperations(phpParameterResolver)
.updateResponseModel(phpPropertyResolver, codegenModelResolver)
.updateTemplate()
.updateApiPath()
.setImports(directoryStructureService)
.build();
.setImports(directoryStructureService);

// Build the apiResource
PhpApiResources apiResources = builder.build();

// Register dynamic templates with the full apiResource if there are multiple response models
// This must be done after build() so the apiResource has all properties set
if (builder.hasMultipleResponseModels()) {
phpApiActionTemplate.addDynamicTemplates(PhpApiActionTemplate.TEMPLATE_TYPE_INSTANCE_CLASS, apiResources);
}

return apiResources;
}
}
1 change: 1 addition & 0 deletions src/main/java/com/twilio/oai/api/ApiResourceBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public abstract class ApiResourceBuilder implements IApiResourceBuilder {
protected final Map<String, CodegenModel> modelTree = new TreeMap<>();
protected final List<CodegenParameter> requiredPathParams = new ArrayList<>();
protected Set<CodegenProperty> apiResponseModels = new LinkedHashSet<>();
protected Set<CodegenModel> responseInstanceModels = new LinkedHashSet<>();
protected final Map<String, Object> metaAPIProperties = new HashMap<>();
protected final List<CodegenOperation> listOperations = new ArrayList<>();
protected final List<CodegenOperation> instanceOperations = new ArrayList<>();
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/com/twilio/oai/api/ApiResources.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ public class ApiResources {
String recordKey;
String version;
List<CodegenProperty> responseModels;
Set<CodegenModel> responseInstanceModels;
List<CodegenParameter> requiredPathParams;
List<CodegenOperation> apiOperations;
Map<String, Object> metaProperties;
Expand All @@ -41,5 +42,6 @@ public ApiResources(ApiResourceBuilder apiResourceBuilder) {
if (ResourceCacheContext.get() != null && ResourceCacheContext.get().isV1()) {
isApiV1 = true;
}
responseInstanceModels = apiResourceBuilder.responseInstanceModels;
}
}
25 changes: 24 additions & 1 deletion src/main/java/com/twilio/oai/api/PhpApiResourceBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.twilio.oai.*;
import com.twilio.oai.common.EnumConstants;
import com.twilio.oai.java.cache.ResourceCacheContext;
import com.twilio.oai.resolver.IConventionMapper;
import com.twilio.oai.resolver.LanguageConventionResolver;
import com.twilio.oai.resolver.Resolver;
Expand Down Expand Up @@ -58,14 +59,33 @@ public PhpApiResourceBuilder updateTemplate() {
template.add(PhpApiActionTemplate.TEMPLATE_TYPE_OPTIONS);
template.add(PhpApiActionTemplate.TEMPLATE_TYPE_PAGE);
template.add(PhpApiActionTemplate.TEMPLATE_TYPE_LIST);
template.add(PhpApiActionTemplate.TEMPLATE_TYPE_INSTANCE);

// Only add regular instance template when there's 1 or fewer response models
// OR when isApiV1 is false (dynamic templates only work with API V1 standard)
// When there are multiple distinct response models AND isApiV1 is true, dynamic templates will be
// added after build() in the generator with the full apiResource
boolean isApiV1 = ResourceCacheContext.get() != null && ResourceCacheContext.get().isV1();
if (!isApiV1 || responseInstanceModels == null || responseInstanceModels.size() <= 1) {
template.add(PhpApiActionTemplate.TEMPLATE_TYPE_INSTANCE);
}

// if any operation in current op list(CRUDF) has application/json request body type
if (!nestedModels.isEmpty())
template.add(PhpApiActionTemplate.TEMPLATE_TYPE_MODELS);
});
return this;
}

/**
* Returns true if this builder has multiple distinct response models that require
* separate instance class files AND isApiV1 is true.
* Dynamic instance templates are only generated for API V1 standard specs.
*/
public boolean hasMultipleResponseModels() {
boolean isApiV1 = ResourceCacheContext.get() != null && ResourceCacheContext.get().isV1();
return isApiV1 && responseInstanceModels != null && responseInstanceModels.size() > 1;
}

@Override
public PhpApiResources build() {
return new PhpApiResources(this);
Expand Down Expand Up @@ -378,6 +398,7 @@ private static Set<String> extractConditionalParams(CodegenOperation operation)
@Override
public ApiResourceBuilder updateResponseModel(Resolver<CodegenProperty> codegenPropertyResolver) {
List<CodegenModel> responseModels = new ArrayList<>();
Set<CodegenModel> responseInstanceModels = new HashSet<>();
codegenOperationList.forEach(codegenOperation -> {
codegenOperation.responses
.stream()
Expand All @@ -392,8 +413,10 @@ public ApiResourceBuilder updateResponseModel(Resolver<CodegenProperty> codegenP
item.vars.forEach(e -> codegenPropertyResolver.resolve(e, this));
item.allVars.forEach(e -> codegenPropertyResolver.resolve(e, this));
responseModels.add(item);
responseInstanceModels.add(item);
});
});
this.responseInstanceModels = responseInstanceModels;
this.apiResponseModels = getDistinctResponseModel(responseModels);
return this;
}
Expand Down
175 changes: 171 additions & 4 deletions src/main/java/com/twilio/oai/template/AbstractApiActionTemplate.java
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
package com.twilio.oai.template;

import java.io.File;
import java.util.List;
import java.util.Map;

import com.samskivert.mustache.Mustache;
import com.samskivert.mustache.Template;
import org.openapitools.codegen.CodegenConfig;
import org.openapitools.codegen.CodegenModel;
import org.openapitools.codegen.SupportingFile;

import java.io.*;
import java.lang.reflect.Field;
import java.nio.charset.StandardCharsets;
import java.util.*;

public abstract class AbstractApiActionTemplate implements IApiActionTemplate {
public static final String API_TEMPLATE = "api";
public static final String NESTED_MODELS = "nested_models";
Expand All @@ -15,6 +19,22 @@ public abstract class AbstractApiActionTemplate implements IApiActionTemplate {
private final Map<String, List<String>> templates = mapping();
protected final CodegenConfig codegen;

// Store dynamic template data
protected final Map<String, DynamicTemplateData> dynamicTemplates = new LinkedHashMap<>();

// Inner class to hold dynamic template configuration
protected static class DynamicTemplateData {
String templateFile;
String fileSuffix;
Object apiResource; // The full apiResource object (e.g., PhpApiResources)

DynamicTemplateData(String templateFile, String fileSuffix, Object apiResource) {
this.templateFile = templateFile;
this.fileSuffix = fileSuffix;
this.apiResource = apiResource;
}
}

protected AbstractApiActionTemplate(CodegenConfig defaultCodegen) {
this.codegen = initialise(defaultCodegen);
}
Expand All @@ -33,6 +53,7 @@ public void clean() {
for (final List<String> entry : templates.values()) {
codegen.apiTemplateFiles().remove(entry.get(0));
}
dynamicTemplates.clear();
}

@Override
Expand All @@ -41,6 +62,152 @@ public void add(String template) {
codegen.apiTemplateFiles().put(templateStrings.get(0), templateStrings.get(1));
}

@Override
public void addDynamicTemplates(String templateType, Object apiResource) {
List<String> templateStrings = templates.get(templateType);
if (templateStrings != null && apiResource != null) {
// Check if apiResource has responseInstanceModels with more than 1 model
Set<CodegenModel> models = getResponseInstanceModels(apiResource);
if (models != null && models.size() > 1) {
dynamicTemplates.put(templateType, new DynamicTemplateData(
templateStrings.get(0), // template file
templateStrings.get(1), // file suffix
apiResource
));
}
}
}

@Override
public void generateDynamicFiles(Map<String, Object> baseContext, String outputDir) {
for (Map.Entry<String, DynamicTemplateData> entry : dynamicTemplates.entrySet()) {
DynamicTemplateData data = entry.getValue();

// Extract responseInstanceModels from apiResource
Set<CodegenModel> models = getResponseInstanceModels(data.apiResource);
if (models == null || models.isEmpty()) {
continue;
}

for (CodegenModel model : models) {
try {
// Build context with apiResource as "resources"
// The template uses {{#resources}} wrapper, so we need to provide the apiResource
// but with responseInstanceModels containing only the current model
Map<String, Object> context = new HashMap<>(baseContext);

// Create a wrapper that provides all apiResource properties
// but overrides responseInstanceModels with just the current model
Map<String, Object> resourcesMap = new HashMap<>();
copyObjectProperties(data.apiResource, resourcesMap);
// Override responseInstanceModels with single model
resourcesMap.put("responseInstanceModels", Collections.singleton(model));
context.put("resources", resourcesMap);

// Read and compile template
String templateContent = readResourceTemplate(data.templateFile);
Template template = Mustache.compiler()
.withLoader(name -> new StringReader(readResourceTemplate(name + ".mustache")))
.defaultValue("")
.compile(templateContent);

// Render
String rendered = template.execute(context);

// Write file: {outputDir}/{ModelClassname}{suffix}
String filename = outputDir + File.separator + model.classname + data.fileSuffix;
writeFile(filename, rendered);

System.out.println("Generated dynamic file: " + filename);

} catch (Exception e) {
System.err.println("Error generating dynamic file for " + model.classname + ": " + e.getMessage());
e.printStackTrace();
}
}
}
}

/**
* Extracts responseInstanceModels from an apiResource object using reflection.
*/
@SuppressWarnings("unchecked")
private Set<CodegenModel> getResponseInstanceModels(Object apiResource) {
if (apiResource == null) return null;

try {
Field field = findField(apiResource.getClass(), "responseInstanceModels");
if (field != null) {
field.setAccessible(true);
Object value = field.get(apiResource);
if (value instanceof Set) {
return (Set<CodegenModel>) value;
}
}
} catch (Exception e) {
// Ignore and return null
}
return null;
}

/**
* Finds a field by name in the class hierarchy.
*/
private Field findField(Class<?> clazz, String fieldName) {
while (clazz != null) {
try {
return clazz.getDeclaredField(fieldName);
} catch (NoSuchFieldException e) {
clazz = clazz.getSuperclass();
}
}
return null;
}

/**
* Copies all properties from an object to a map for mustache rendering.
* Uses reflection to access fields from the object and its superclasses.
*/
private void copyObjectProperties(Object source, Map<String, Object> targetMap) {
if (source == null) return;

Class<?> clazz = source.getClass();
while (clazz != null) {
for (Field field : clazz.getDeclaredFields()) {
try {
field.setAccessible(true);
Object value = field.get(source);
if (value != null) {
targetMap.put(field.getName(), value);
}
} catch (IllegalAccessException e) {
// Skip inaccessible fields
}
}
clazz = clazz.getSuperclass();
}
}

protected String readResourceTemplate(String templatePath) {
String fullPath = codegen.templateDir() + "/" + templatePath;
try (InputStream is = getClass().getClassLoader().getResourceAsStream(fullPath)) {
if (is == null) {
throw new RuntimeException("Template not found: " + fullPath);
}
return new String(is.readAllBytes(), StandardCharsets.UTF_8);
} catch (IOException e) {
throw new RuntimeException("Cannot read template: " + fullPath, e);
}
}

protected void writeFile(String filename, String content) throws IOException {
File file = new File(filename);
file.getParentFile().mkdirs();
try (FileWriter writer = new FileWriter(file, StandardCharsets.UTF_8)) {
writer.write(content);
}
}

@Override
public void addSupportVersion() {
final List<String> templateStrings = templates.get(VERSION_TEMPLATE);
Expand Down
8 changes: 8 additions & 0 deletions src/main/java/com/twilio/oai/template/IApiActionTemplate.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
package com.twilio.oai.template;

import java.util.Map;

public interface IApiActionTemplate {
void clean();
void add(String template);
void addSupportVersion();

// Method for registering dynamic templates that generate multiple files from apiResource
void addDynamicTemplates(String templateType, Object apiResource);

// Method to generate the dynamic files
void generateDynamicFiles(Map<String, Object> baseContext, String outputDir);
}
Loading