Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .github/workflows/samples-ocaml.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,16 @@ on:
- 'samples/client/petstore/ocaml-fake-petstore/**'
- 'samples/client/petstore/ocaml-oneOf-primitive/**'
- 'samples/client/petstore/ocaml-additional-properties/**'
- 'samples/client/petstore/ocaml-enum-in-composed-schema/**'
- 'samples/client/petstore/ocaml-recursion-test/**'
pull_request:
paths:
- 'samples/client/petstore/ocaml/**'
- 'samples/client/petstore/ocaml-fake-petstore/**'
- 'samples/client/petstore/ocaml-oneOf-primitive/**'
- 'samples/client/petstore/ocaml-additional-properties/**'
- 'samples/client/petstore/ocaml-enum-in-composed-schema/**'
- 'samples/client/petstore/ocaml-recursion-test/**'

jobs:
build:
Expand All @@ -26,12 +30,14 @@ jobs:
- 'samples/client/petstore/ocaml-fake-petstore/'
- 'samples/client/petstore/ocaml-oneOf-primitive/'
- 'samples/client/petstore/ocaml-additional-properties/'
- 'samples/client/petstore/ocaml-enum-in-composed-schema/'
- 'samples/client/petstore/ocaml-recursion-test/'
steps:
- uses: actions/checkout@v5
- name: Set-up OCaml
uses: ocaml/setup-ocaml@v3
with:
ocaml-compiler: 5
ocaml-compiler: 5.3
- name: Install
run: opam install . --deps-only --with-test
working-directory: ${{ matrix.sample }}
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,8 @@ samples/client/petstore/ocaml/_build/
samples/client/petstore/ocaml-fake-petstore/_build/
samples/client/petstore/ocaml-oneOf-primitive/_build/
samples/client/petstore/ocaml-additional-properties/_build/
samples/client/petstore/ocaml-enum-in-composed-schema/_build/
samples/client/petstore/ocaml-recursion-test/_build/

# jetbrain http client
samples/client/jetbrains/adyen/checkout71/http/client/Apis/http-client.private.env.json
6 changes: 6 additions & 0 deletions bin/configs/ocaml-enum-in-composed-schema.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
generatorName: ocaml
outputDir: samples/client/petstore/ocaml-enum-in-composed-schema
inputSpec: modules/openapi-generator/src/test/resources/3_0/ocaml/enum-in-composed-schema.yaml
templateDir: modules/openapi-generator/src/main/resources/ocaml
additionalProperties:
packageName: petstore_client
6 changes: 6 additions & 0 deletions bin/configs/ocaml-recursion-test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
generatorName: ocaml
outputDir: samples/client/petstore/ocaml-recursion-test
inputSpec: modules/openapi-generator/src/test/resources/3_0/ocaml/direct-recursion.yaml
templateDir: modules/openapi-generator/src/main/resources/ocaml
additionalProperties:
packageName: recursion_test
2 changes: 1 addition & 1 deletion docs/generators/ocaml.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ title: Documentation for the ocaml Generator
| generator type | CLIENT | |
| generator language | OCaml | |
| generator default templating engine | mustache | |
| helpTxt | Generates an OCaml client library (beta). | |
| helpTxt | Generates an OCaml client library. | |

## CONFIG OPTIONS
These options may be applied as additional-properties (cli) or configOptions (plugins). Refer to [configuration docs](https://openapi-generator.tech/docs/configuration) for more details.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,16 +51,18 @@ public class OCamlClientCodegen extends DefaultCodegen implements CodegenConfig

static final String X_MODEL_MODULE = "x-model-module";

@Setter protected String packageName = "openapi";
@Setter protected String packageVersion = "1.0.0";
@Setter
protected String packageName = "openapi";
@Setter
protected String packageVersion = "1.0.0";
protected String apiDocPath = "docs/";
protected String modelDocPath = "docs/";
protected String apiFolder = "src/apis";
protected String modelFolder = "src/models";

private Map<String, List<String>> enumNames = new HashMap<>();
private Map<String, Schema> enumHash = new HashMap<>();
private Map<String, String> enumUniqNames;
private Map<Set<String>, List<String>> enumNames = new HashMap<>();
private Map<Set<String>, Schema> enumHash = new HashMap<>();
private Map<Set<String>, String> enumUniqNames;

@Override
public CodegenType getTag() {
Expand All @@ -74,7 +76,7 @@ public String getName() {

@Override
public String getHelp() {
return "Generates an OCaml client library (beta).";
return "Generates an OCaml client library.";
}

public OCamlClientCodegen() {
Expand Down Expand Up @@ -233,6 +235,105 @@ public Map<String, ModelsMap> postProcessAllModels(Map<String, ModelsMap> supero

}

/**
* Add support for direct recursive types (e.g., A -> A).
* This does *not* support mutually recursive types (e.g., A -> B -> A), as this is a much more complex beast in OCaml (since mutually recursive types must live in the same file).
*/
@Override
public ModelsMap postProcessModels(ModelsMap objs) {
objs = super.postProcessModels(objs);

for (ModelMap mo : objs.getModels()) {
CodegenModel cm = mo.getModel();

// Check if any property is a self-reference
boolean hasSelfRef = cm.allVars.stream()
.anyMatch(prop -> prop.isSelfReference);

if (hasSelfRef) {
// Collect names of self-referencing properties
Set<String> selfRefPropNames = cm.allVars.stream()
.filter(p -> p.isSelfReference)
.map(p -> p.name)
.collect(Collectors.toSet());

// The property lists (vars, allVars, etc.) contain DIFFERENT objects
// Match by name since isSelfReference might only be set in allVars
List<List<CodegenProperty>> allPropertyLists = Arrays.asList(
cm.allVars, cm.vars, cm.requiredVars, cm.optionalVars,
cm.readOnlyVars, cm.readWriteVars, cm.parentVars
);

for (List<CodegenProperty> propList : allPropertyLists) {
for (CodegenProperty prop : propList) {
if (selfRefPropNames.contains(prop.name)) {
if (prop.isContainer && prop.items != null) {
// For containers, update items and reconstruct the container type
prop.items.dataType = "t";
prop.items.datatypeWithEnum = "t";
if (prop.items.baseType != null) {
prop.items.baseType = "t";
}
if (prop.items.complexType != null) {
prop.items.complexType = "t";
}

// Reconstruct the container type based on the updated items
if (prop.isArray) {
prop.dataType = "t list";
prop.datatypeWithEnum = "t list";
} else if (prop.isMap) {
prop.dataType = "(string * t) list";
prop.datatypeWithEnum = "(string * t) list";
}
} else {
// For non-containers, just replace the type directly
prop.dataType = "t";
prop.datatypeWithEnum = "t";
}

// Update baseType and complexType for all cases
if (prop.baseType != null) {
prop.baseType = "t";
}
if (prop.complexType != null) {
prop.complexType = "t";
}
}
}
}
}

// Fix enum references in composed schemas (anyOf, oneOf, allOf)
if (cm.getComposedSchemas() != null) {
fixEnumReferencesInComposedSchemas(cm.getComposedSchemas().getAnyOf());
fixEnumReferencesInComposedSchemas(cm.getComposedSchemas().getOneOf());
fixEnumReferencesInComposedSchemas(cm.getComposedSchemas().getAllOf());
}
}

return objs;
}

private void fixEnumReferencesInComposedSchemas(List<CodegenProperty> schemas) {
if (schemas == null) {
return;
}

for (CodegenProperty schema : schemas) {
// If this schema is an enum, add Enums. prefix to datatypeWithEnum
if (schema.isEnum) {
if (!schema.datatypeWithEnum.startsWith("Enums.")) {
schema.datatypeWithEnum = "Enums." + schema.datatypeWithEnum;
}
// Also update dataType for the variant constructor
if (!schema.dataType.startsWith("Enums.")) {
schema.dataType = "Enums." + schema.dataType;
}
}
}
}

private void enrichPropertiesWithEnumDefaultValues(List<CodegenProperty> properties) {
for (CodegenProperty property : properties) {
if (property.get_enum() != null && property.get_enum().size() == 1) {
Expand Down Expand Up @@ -276,8 +377,10 @@ protected void updateDataTypeWithEnumForArray(CodegenProperty property) {
}

@SuppressWarnings("unchecked")
private String hashEnum(Schema schema) {
return ((List<Object>) schema.getEnum()).stream().map(String::valueOf).collect(Collectors.joining(","));
private Set<String> hashEnum(Schema schema) {
return ((List<Object>) schema.getEnum()).stream()
.map(String::valueOf)
.collect(Collectors.toCollection(TreeSet::new));
}

private boolean isEnumSchema(Schema schema) {
Expand All @@ -290,7 +393,7 @@ private void collectEnumSchemas(String parentName, String sName, Schema schema)
} else if (ModelUtils.isMapSchema(schema) && schema.getAdditionalProperties() instanceof Schema) {
collectEnumSchemas(parentName, sName, (Schema) schema.getAdditionalProperties());
} else if (isEnumSchema(schema)) {
String h = hashEnum(schema);
Set<String> h = hashEnum(schema);
if (!enumHash.containsKey(h)) {
enumHash.put(h, schema);
enumNames.computeIfAbsent(h, k -> new ArrayList<>()).add(sName.toLowerCase(Locale.ROOT));
Expand All @@ -299,6 +402,8 @@ private void collectEnumSchemas(String parentName, String sName, Schema schema)
}
}
}
// Note: Composed schemas (anyOf, allOf, oneOf) are handled in the Map-based method
// via collectEnumSchemasFromComposed() which properly processes their structure
}

private void collectEnumSchemas(String sName, Schema schema) {
Expand Down Expand Up @@ -327,6 +432,47 @@ private void collectEnumSchemas(String parentName, Map<String, Schema> schemas)
collectEnumSchemas(pName, ModelUtils.getSchemaItems(schema));
}
}

// Handle composed schemas (anyOf, allOf, oneOf) - recursively process their structure
collectEnumSchemasFromComposed(pName, schema);
}
}

private void collectEnumSchemasFromComposed(String parentName, Schema schema) {
if (schema.getAnyOf() != null) {
collectEnumSchemasFromList(parentName, schema.getAnyOf());
}

if (schema.getAllOf() != null) {
collectEnumSchemasFromList(parentName, schema.getAllOf());
}

if (schema.getOneOf() != null) {
collectEnumSchemasFromList(parentName, schema.getOneOf());
}
}

private void collectEnumSchemasFromList(String parentName, List<Schema> schemas) {
int index = 0;
for (Schema composedSchema : schemas) {
// Check if the composed schema itself is an enum
if (isEnumSchema(composedSchema)) {
String enumName = composedSchema.getName() != null ? composedSchema.getName() : "any_of_" + index;
collectEnumSchemas(parentName, enumName, composedSchema);
}

if (composedSchema.getProperties() != null) {
collectEnumSchemas(parentName, composedSchema.getProperties());
}
if (composedSchema.getAdditionalProperties() != null && composedSchema.getAdditionalProperties() instanceof Schema) {
collectEnumSchemas(parentName, composedSchema.getName(), (Schema) composedSchema.getAdditionalProperties());
}
if (ModelUtils.isArraySchema(composedSchema) && ModelUtils.getSchemaItems(composedSchema) != null) {
collectEnumSchemas(parentName, composedSchema.getName(), ModelUtils.getSchemaItems(composedSchema));
}
// Recursively handle nested composed schemas
collectEnumSchemasFromComposed(parentName, composedSchema);
index++;
}
}

Expand Down Expand Up @@ -378,8 +524,8 @@ private String sanitizeOCamlTypeName(String name) {
}

private void computeEnumUniqNames() {
Map<String, String> definitiveNames = new HashMap<>();
for (String h : enumNames.keySet()) {
Map<String, Set<String>> definitiveNames = new HashMap<>();
for (Set<String> h : enumNames.keySet()) {
boolean hasDefName = false;
List<String> nameCandidates = enumNames.get(h);
for (String name : nameCandidates) {
Expand Down Expand Up @@ -600,13 +746,13 @@ public String getTypeDeclaration(Schema p) {
String prefix = inner.getEnum() != null ? "Enums." : "";
return "(string * " + prefix + getTypeDeclaration(inner) + ") list";
} else if (p.getEnum() != null) {
String h = hashEnum(p);
Set<String> h = hashEnum(p);
return enumUniqNames.get(h);
}

Schema referencedSchema = ModelUtils.getReferencedSchema(openAPI, p);
if (referencedSchema != null && referencedSchema.getEnum() != null) {
String h = hashEnum(referencedSchema);
Set<String> h = hashEnum(referencedSchema);
return "Enums." + enumUniqNames.get(h);
}

Expand Down Expand Up @@ -739,8 +885,8 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List<Mo
}
}

for (Map.Entry<String, String> e : enumUniqNames.entrySet()) {
allModels.add(buildEnumModelWrapper(e.getValue(), e.getKey()));
for (Map.Entry<Set<String>, String> e : enumUniqNames.entrySet()) {
allModels.add(buildEnumModelWrapper(e.getValue(), String.join(",", e.getKey())));
}

enumUniqNames.clear();
Expand Down Expand Up @@ -770,7 +916,7 @@ public String escapeUnsafeCharacters(String input) {

@Override
public String toEnumName(CodegenProperty property) {
String hash = String.join(",", property.get_enum());
Set<String> hash = new TreeSet<>(property.get_enum());

if (enumUniqNames.containsKey(hash)) {
return enumUniqNames.get(hash);
Expand Down
Loading
Loading