Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
fbb46ed
Add OCaml fake-petstore to test corner cases
sir4ur0n Aug 1, 2025
5215f21
Prefix List functions with Stdlib as the fake petstore generates a Li…
sir4ur0n Aug 1, 2025
a5f098a
Handle decimal and any types
sir4ur0n Aug 1, 2025
675ba32
Indent to_json.mustache for easier maintenance
sir4ur0n Aug 1, 2025
3ea004a
Indent api-impl.mustache a bit more for readability before fix
sir4ur0n Aug 1, 2025
38060f9
Fix: do not call `to_json` for free forms and byte arrays
sir4ur0n Aug 1, 2025
2d29839
Fix compilation for binary types
sir4ur0n Aug 1, 2025
2bc64f7
Indent to_string.mustache
sir4ur0n Aug 1, 2025
74c8e30
Add support for exploded form-style object query params
sir4ur0n Aug 18, 2025
847d2c5
Add ocaml-fake-petstore to CI
sir4ur0n Aug 18, 2025
a9e6ca2
Fix free-form body params
sir4ur0n Aug 18, 2025
cad4d18
Cohttp_lwt.Response is deprecated, use Cohttp.Response instead
sir4ur0n Aug 18, 2025
b7414fc
Safe Java code cleanup
sir4ur0n Aug 22, 2025
44964bd
Split into model-record.mustache
sir4ur0n Aug 22, 2025
f42ba61
Add some support for oneOf/anyOf
sir4ur0n Aug 22, 2025
f5eaf8f
Re-generate all OCaml samples
sir4ur0n Aug 22, 2025
ac643b5
Fix: correctly mark non-required maps with default empty list
sir4ur0n Aug 22, 2025
8f5213d
Fix: Correctly encode/decode maps
sir4ur0n Aug 22, 2025
2ae3a63
Refresh documentation
sir4ur0n Aug 22, 2025
748e5f5
Merge remote-tracking branch 'origin/master' into feature/ocaml-oneof
sir4ur0n Aug 22, 2025
ca6bd14
Refresh after merging master
sir4ur0n Aug 22, 2025
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
6 changes: 6 additions & 0 deletions .github/workflows/samples-ocaml.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,13 @@ on:
push:
paths:
- 'samples/client/petstore/ocaml/**'
- 'samples/client/petstore/ocaml-fake-petstore/**'
- 'samples/client/petstore/ocaml-oneOf/**'
pull_request:
paths:
- 'samples/client/petstore/ocaml/**'
- 'samples/client/petstore/ocaml-fake-petstore/**'
- 'samples/client/petstore/ocaml-oneOf/**'

jobs:
build:
Expand All @@ -17,6 +21,8 @@ jobs:
matrix:
sample:
- 'samples/client/petstore/ocaml/'
- 'samples/client/petstore/ocaml-fake-petstore/'
- 'samples/client/petstore/ocaml-oneOf/'
steps:
- uses: actions/checkout@v5
- name: Set-up OCaml
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,8 @@ samples/openapi3/client/petstore/go/privatekey.pem

## OCaml
samples/client/petstore/ocaml/_build/
samples/client/petstore/ocaml-fake-petstore/_build/
samples/client/petstore/ocaml-oneOf/_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-fake-petstore.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
generatorName: ocaml
outputDir: samples/client/petstore/ocaml-fake-petstore
inputSpec: modules/openapi-generator/src/test/resources/3_0/petstore-with-fake-endpoints-models-for-testing.yaml
templateDir: modules/openapi-generator/src/main/resources/ocaml
additionalProperties:
packageName: petstore_client
6 changes: 6 additions & 0 deletions bin/configs/ocaml-oneOf.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
generatorName: ocaml
outputDir: samples/client/petstore/ocaml-oneOf
inputSpec: modules/openapi-generator/src/test/resources/3_0/oneOf_primitive.yaml
templateDir: modules/openapi-generator/src/main/resources/ocaml
additionalProperties:
packageName: petstore_client
4 changes: 2 additions & 2 deletions docs/generators/ocaml.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,8 +209,8 @@ These options may be applied as additional-properties (cli) or configOptions (pl
|Polymorphism|✗|OAS2,OAS3
|Union|✗|OAS3
|allOf|✗|OAS2,OAS3
|anyOf||OAS3
|oneOf||OAS3
|anyOf||OAS3
|oneOf||OAS3
|not|✗|OAS3

### Security Feature
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,6 @@ public class OCamlClientCodegen extends DefaultCodegen implements CodegenConfig

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

public static final String CO_HTTP = "cohttp";

@Setter protected String packageName = "openapi";
@Setter protected String packageVersion = "1.0.0";
protected String apiDocPath = "docs/";
Expand Down Expand Up @@ -97,12 +95,15 @@ public OCamlClientCodegen() {
.excludeSchemaSupportFeatures(
SchemaSupportFeature.Polymorphism
)
.includeSchemaSupportFeatures(
SchemaSupportFeature.oneOf,
SchemaSupportFeature.anyOf
)
.includeClientModificationFeatures(
ClientModificationFeature.BasePath
)
);


outputFolder = "generated-code/ocaml";
modelTemplateFiles.put("model.mustache", ".ml");

Expand Down Expand Up @@ -171,6 +172,7 @@ public OCamlClientCodegen() {
typeMapping.put("short", "int");
typeMapping.put("char", "char");
typeMapping.put("float", "float");
typeMapping.put("decimal", "string");
typeMapping.put("double", "float");
typeMapping.put("integer", "int32");
typeMapping.put("number", "float");
Expand All @@ -179,36 +181,27 @@ public OCamlClientCodegen() {
typeMapping.put("any", "Yojson.Safe.t");
typeMapping.put("file", "string");
typeMapping.put("ByteArray", "string");
typeMapping.put("AnyType", "Yojson.Safe.t");
// lib
typeMapping.put("string", "string");
typeMapping.put("UUID", "string");
typeMapping.put("URI", "string");
typeMapping.put("set", "`Set");
typeMapping.put("password", "string");
typeMapping.put("DateTime", "string");

// supportedLibraries.put(CO_HTTP, "HTTP client: CoHttp.");
//
// CliOption libraryOption = new CliOption(CodegenConstants.LIBRARY, "library template (sub-template) to use.");
// libraryOption.setEnum(supportedLibraries);
// // set hyper as the default
// libraryOption.setDefault(CO_HTTP);
// cliOptions.add(libraryOption);
// setLibrary(CO_HTTP);
}

@Override
public Map<String, ModelsMap> postProcessAllModels(Map<String, ModelsMap> superobjs) {
List<String> toRemove = new ArrayList<>();

for (Map.Entry<String, ModelsMap> modelEntry : superobjs.entrySet()) {
// process enum in models
List<ModelMap> models = modelEntry.getValue().getModels();
for (ModelMap mo : models) {
CodegenModel cm = mo.getModel();

// for enum model
if (Boolean.TRUE.equals(cm.isEnum) && cm.allowableValues != null) {
if (cm.isEnum && cm.allowableValues != null) {
toRemove.add(modelEntry.getKey());
} else {
enrichPropertiesWithEnumDefaultValues(cm.getAllVars());
Expand All @@ -219,6 +212,15 @@ public Map<String, ModelsMap> postProcessAllModels(Map<String, ModelsMap> supero
enrichPropertiesWithEnumDefaultValues(cm.getVars());
enrichPropertiesWithEnumDefaultValues(cm.getParentVars());
}

if (!cm.oneOf.isEmpty()) {
// Add a boolean if it is a `oneOf`, because Mustache does not let us check if a list is non-empty
cm.getVendorExtensions().put("x-ocaml-isOneOf", true);
}
if (!cm.anyOf.isEmpty()) {
// Add a boolean if it is a `anyOf`, because Mustache does not let us check if a list is non-empty
cm.getVendorExtensions().put("x-ocaml-isAnyOf", true);
}
}
}

Expand All @@ -242,8 +244,7 @@ private void enrichPropertiesWithEnumDefaultValues(List<CodegenProperty> propert
@Override
protected void updateDataTypeWithEnumForMap(CodegenProperty property) {
CodegenProperty baseItem = property.items;
while (baseItem != null && (Boolean.TRUE.equals(baseItem.isMap)
|| Boolean.TRUE.equals(baseItem.isArray))) {
while (baseItem != null && (baseItem.isMap || baseItem.isArray)) {
baseItem = baseItem.items;
}

Expand All @@ -260,8 +261,7 @@ protected void updateDataTypeWithEnumForMap(CodegenProperty property) {
@Override
protected void updateDataTypeWithEnumForArray(CodegenProperty property) {
CodegenProperty baseItem = property.items;
while (baseItem != null && (Boolean.TRUE.equals(baseItem.isMap)
|| Boolean.TRUE.equals(baseItem.isArray))) {
while (baseItem != null && (baseItem.isMap || baseItem.isArray)) {
baseItem = baseItem.items;
}
if (baseItem != null) {
Expand Down Expand Up @@ -312,19 +312,17 @@ private void collectEnumSchemas(String parentName, Map<String, Schema> schemas)

collectEnumSchemas(parentName, sName, schema);

String pName = parentName != null ? parentName + "_" + sName : sName;
if (schema.getProperties() != null) {
String pName = parentName != null ? parentName + "_" + sName : sName;
collectEnumSchemas(pName, schema.getProperties());
}

if (schema.getAdditionalProperties() != null && schema.getAdditionalProperties() instanceof Schema) {
String pName = parentName != null ? parentName + "_" + sName : sName;
collectEnumSchemas(pName, (Schema) schema.getAdditionalProperties());
}

if (ModelUtils.isArraySchema(schema)) {
if (ModelUtils.getSchemaItems(schema) != null) {
String pName = parentName != null ? parentName + "_" + sName : sName;
collectEnumSchemas(pName, ModelUtils.getSchemaItems(schema));
}
}
Expand Down Expand Up @@ -677,7 +675,7 @@ private List<Map<String, Object>> buildEnumValues(String valueString) {
public String toEnumValueName(String name) {
if (reservedWords.contains(name)) {
return escapeReservedWord(name);
} else if (((CharSequence) name).chars().anyMatch(character -> specialCharReplacements.keySet().contains(String.valueOf((char) character)))) {
} else if (name.chars().anyMatch(character -> specialCharReplacements.containsKey(String.valueOf((char) character)))) {
return escape(name, specialCharReplacements, Collections.singletonList("_"), null);
} else {
return name;
Expand Down Expand Up @@ -723,8 +721,6 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List<Mo
List<CodegenOperation> operations = objectMap.getOperation();

for (CodegenOperation operation : operations) {
// http method verb conversion, depending on client library (e.g. Hyper: PUT => Put, Reqwest: PUT => put)
//if (CO_HTTP.equals(getLibrary())) {
for (CodegenParameter param : operation.bodyParams) {
if (param.isModel && param.dataType.endsWith(".t")) {
param.vendorExtensions.put(X_MODEL_MODULE, param.dataType.substring(0, param.dataType.lastIndexOf('.')));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ let {{{operationId}}} {{^hasParams}}(){{/hasParams}}{{#allParams}}{{> to_param}}
let uri = Request.{{> to_optional_prefix}}replace_path_param uri "{{{baseName}}}" {{> to_string}} {{{paramName}}} in
{{/pathParams}}
{{#queryParams}}
let uri = Request.{{> to_optional_prefix}}add_query_param{{#isArray}}_list{{/isArray}} uri "{{{baseName}}}" {{> to_string}} {{{paramName}}} in
let uri = Request.{{> to_optional_prefix}}add_query_param{{#isArray}}_list{{/isArray}}{{#isExplode}}{{#isFormStyle}}{{#isMap}}_exploded_form_object{{/isMap}}{{/isFormStyle}}{{/isExplode}} uri "{{{baseName}}}" {{> to_string}} {{{paramName}}} in
{{/queryParams}}
{{#hasAuthMethods}}
{{#authMethods}}
Expand All @@ -42,7 +42,10 @@ let {{{operationId}}} {{^hasParams}}(){{/hasParams}}{{#allParams}}{{> to_param}}
{{/authMethods}}
{{/hasAuthMethods}}
{{#bodyParams}}
let body = Request.{{#isFreeFormObject}}write_json_body{{/isFreeFormObject}}{{#isByteArray}}write_string_body{{/isByteArray}}{{^isFreeFormObject}}{{^isByteArray}}write_as_json_body{{/isByteArray}}{{/isFreeFormObject}} {{> to_json}} {{{paramName}}} in
let body = Request.
{{#isByteArray}}write_string_body {{{paramName}}}{{/isByteArray}}
{{^isByteArray}}write_as_json_body {{> to_json}} {{{paramName}}}{{/isByteArray}}
in
{{/bodyParams}}
{{^hasBodyParam}}
{{#hasFormParams}}
Expand Down
17 changes: 15 additions & 2 deletions modules/openapi-generator/src/main/resources/ocaml/json.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,19 @@ let of_int32 x = `Intlit (Int32.to_string x)

let of_int64 x = `Intlit (Int64.to_string x)

let of_list_of of_f l = `List (List.map of_f l)
let of_list_of of_f l = `List (Stdlib.List.map of_f l)

let of_map_of of_f l = `Assoc (List.map (fun (k, v) -> (k, of_f v)) l)
let of_map_of of_f l = `Assoc (Stdlib.List.map (fun (k, v) -> (k, of_f v)) l)

let to_map_of of_f json =
match json with
| `Assoc l ->
Stdlib.List.fold_right
(fun (k, json) acc ->
match (of_f json, acc) with
| Stdlib.Result.Ok parsed_v, Stdlib.Result.Ok tl ->
Stdlib.Result.Ok ((k, parsed_v) :: tl)
| Stdlib.Result.Error e, _ -> Stdlib.Result.Error e
| _, Stdlib.Result.Error e -> Stdlib.Result.Error e)
l (Stdlib.Result.Ok [])
| _ -> Stdlib.Result.Error "Expected"
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
type t =
{{#composedSchemas.anyOf}}
| {{{nameInPascalCase}}} of {{{dataType}}}
{{/composedSchemas.anyOf}}
[@@deriving show, eq];;

let to_yojson = function
{{#composedSchemas.anyOf}}
| {{{nameInPascalCase}}} v -> [%to_yojson: {{{ datatypeWithEnum }}}] v
{{/composedSchemas.anyOf}}

(* Manual implementations because the derived one encodes into a tuple list where the first element is the constructor name. *)

let of_yojson json =
[
{{#composedSchemas.anyOf}}
[%of_yojson: {{{ datatypeWithEnum }}}] json
|> Stdlib.Result.to_option
|> Stdlib.Option.map (fun v -> {{{nameInPascalCase}}} v);
{{/composedSchemas.anyOf}}
]
|> Stdlib.List.filter_map (Fun.id)
|> function
| t :: _ -> Ok t (* Return the first successful parsing. *)
| [] -> Error ("Failed to parse JSON " ^ Yojson.Safe.show json ^ " into a value of type {{{ classname }}}")
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
type t =
{{#composedSchemas.oneOf}}
| {{{nameInPascalCase}}} of {{{dataType}}}
{{/composedSchemas.oneOf}}
[@@deriving show, eq];;

let to_yojson = function
{{#composedSchemas.oneOf}}
| {{{nameInPascalCase}}} v -> [%to_yojson: {{{ datatypeWithEnum }}}] v
{{/composedSchemas.oneOf}}

(* Manual implementations because the derived one encodes into a tuple list where the first element is the constructor name. *)

let of_yojson json =
[
{{#composedSchemas.oneOf}}
[%of_yojson: {{{ datatypeWithEnum }}}] json
|> Stdlib.Result.to_option
|> Stdlib.Option.map (fun v -> {{{nameInPascalCase}}} v);
{{/composedSchemas.oneOf}}
]
|> Stdlib.List.filter_map (Fun.id)
|> function
| [t] -> Ok t
| [] -> Error ("Failed to parse JSON " ^ Yojson.Safe.show json ^ " into a value of type {{{ classname }}}")
| ts -> let parsed_ts = ts
|> Stdlib.List.map show
|> Stdlib.String.concat " | "
in Error ("Failed to parse JSON " ^ Yojson.Safe.show json ^ " into a value of type {{{ classname }}}: oneOf should only succeed on one parser, but the JSON was parsed into [" ^ parsed_ts ^ "]")
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
type t = {
{{#vars}}
{{#description}}
(* {{{.}}} *)
{{/description}}
{{#isEnum}}
{{{name}}}: {{^isMap}}Enums.{{/isMap}}{{{datatypeWithEnum}}}
{{^isContainer}}
{{#required}}
{{#defaultValue}}[@default {{{.}}}]{{/defaultValue}}
{{#isNullable}} option [@default
{{#defaultValue}}Some({{{.}}}){{/defaultValue}}
{{^defaultValue}}None{{/defaultValue}}
]
{{/isNullable}}
{{/required}}
{{^required}} option [@default
{{#defaultValue}}Some({{{.}}}){{/defaultValue}}
{{^defaultValue}}None{{/defaultValue}}
]
{{/required}}
{{/isContainer}}; [@key "{{{baseName}}}"]
{{/isEnum}}
{{^isEnum}}
{{{name}}}: {{{datatypeWithEnum}}}
{{^isContainer}}
{{#required}}{{#isNullable}} option{{/isNullable}}{{/required}}
{{^required}} option [@default None]{{/required}}
{{/isContainer}}
{{#isArray}}{{^required}} [@default []]{{/required}}{{/isArray}}
{{#isMap}}{{^required}} [@default []] [@to_yojson JsonSupport.of_map_of [%to_yojson: {{{items.datatypeWithEnum}}}]] [@of_yojson JsonSupport.to_map_of [%of_yojson: {{{items.datatypeWithEnum}}}]] {{/required}}{{/isMap}}
; [@key "{{{baseName}}}"]
{{/isEnum}}
{{/vars}}
} [@@deriving yojson { strict = false }, show, eq ];;

{{#description}}
(** {{{.}}} *)
{{/description}}
let create {{#requiredVars}}({{{name}}} : {{#isEnum}}Enums.{{{datatypeWithEnum}}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}{{#isNullable}} option{{/isNullable}}){{^-last}} {{/-last}}{{/requiredVars}}{{^hasRequired}}(){{/hasRequired}} : t = {
{{#vars}}
{{{name}}} = {{#required}}{{{name}}}{{/required}}{{^required}}{{#isContainer}}[]{{/isContainer}}{{^isContainer}}None{{/isContainer}}{{/required}};
{{/vars}}
}
Loading
Loading