Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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