Skip to content

Commit dc8fac2

Browse files
authored
[OCaml] Introduce support for oneOf/anyOf, fix default value for non-required maps (#21798)
* Add OCaml fake-petstore to test corner cases * Prefix List functions with Stdlib as the fake petstore generates a List module * Handle decimal and any types * Indent to_json.mustache for easier maintenance * Indent api-impl.mustache a bit more for readability before fix * Fix: do not call `to_json` for free forms and byte arrays Fixes #21312 * Fix compilation for binary types The implementation may not be correct, but at least it compiles. To be checked if someday someone actually uses it/complains. * Indent to_string.mustache * Add support for exploded form-style object query params Fixes #21307 * Add ocaml-fake-petstore to CI * Fix free-form body params * Cohttp_lwt.Response is deprecated, use Cohttp.Response instead * Safe Java code cleanup * Split into model-record.mustache * Add some support for oneOf/anyOf * Re-generate all OCaml samples * Fix: correctly mark non-required maps with default empty list * Fix: Correctly encode/decode maps * Refresh documentation * Refresh after merging master
1 parent cd7fe34 commit dc8fac2

File tree

108 files changed

+5193
-300
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

108 files changed

+5193
-300
lines changed

.github/workflows/samples-ocaml.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,13 @@ on:
44
push:
55
paths:
66
- 'samples/client/petstore/ocaml/**'
7+
- 'samples/client/petstore/ocaml-fake-petstore/**'
8+
- 'samples/client/petstore/ocaml-oneOf/**'
79
pull_request:
810
paths:
911
- 'samples/client/petstore/ocaml/**'
12+
- 'samples/client/petstore/ocaml-fake-petstore/**'
13+
- 'samples/client/petstore/ocaml-oneOf/**'
1014

1115
jobs:
1216
build:
@@ -17,6 +21,8 @@ jobs:
1721
matrix:
1822
sample:
1923
- 'samples/client/petstore/ocaml/'
24+
- 'samples/client/petstore/ocaml-fake-petstore/'
25+
- 'samples/client/petstore/ocaml-oneOf/'
2026
steps:
2127
- uses: actions/checkout@v5
2228
- name: Set-up OCaml

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,8 @@ samples/openapi3/client/petstore/go/privatekey.pem
296296

297297
## OCaml
298298
samples/client/petstore/ocaml/_build/
299+
samples/client/petstore/ocaml-fake-petstore/_build/
300+
samples/client/petstore/ocaml-oneOf/_build/
299301

300302
# jetbrain http client
301303
samples/client/jetbrains/adyen/checkout71/http/client/Apis/http-client.private.env.json

bin/configs/ocaml-fake-petstore.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
generatorName: ocaml
2+
outputDir: samples/client/petstore/ocaml-fake-petstore
3+
inputSpec: modules/openapi-generator/src/test/resources/3_0/petstore-with-fake-endpoints-models-for-testing.yaml
4+
templateDir: modules/openapi-generator/src/main/resources/ocaml
5+
additionalProperties:
6+
packageName: petstore_client

bin/configs/ocaml-oneOf.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
generatorName: ocaml
2+
outputDir: samples/client/petstore/ocaml-oneOf
3+
inputSpec: modules/openapi-generator/src/test/resources/3_0/oneOf_primitive.yaml
4+
templateDir: modules/openapi-generator/src/main/resources/ocaml
5+
additionalProperties:
6+
packageName: petstore_client

docs/generators/ocaml.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -209,8 +209,8 @@ These options may be applied as additional-properties (cli) or configOptions (pl
209209
|Polymorphism|✗|OAS2,OAS3
210210
|Union|✗|OAS3
211211
|allOf|✗|OAS2,OAS3
212-
|anyOf||OAS3
213-
|oneOf||OAS3
212+
|anyOf||OAS3
213+
|oneOf||OAS3
214214
|not|✗|OAS3
215215

216216
### Security Feature

modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/OCamlClientCodegen.java

Lines changed: 20 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,6 @@ public class OCamlClientCodegen extends DefaultCodegen implements CodegenConfig
5151

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

54-
public static final String CO_HTTP = "cohttp";
55-
5654
@Setter protected String packageName = "openapi";
5755
@Setter protected String packageVersion = "1.0.0";
5856
protected String apiDocPath = "docs/";
@@ -97,12 +95,15 @@ public OCamlClientCodegen() {
9795
.excludeSchemaSupportFeatures(
9896
SchemaSupportFeature.Polymorphism
9997
)
98+
.includeSchemaSupportFeatures(
99+
SchemaSupportFeature.oneOf,
100+
SchemaSupportFeature.anyOf
101+
)
100102
.includeClientModificationFeatures(
101103
ClientModificationFeature.BasePath
102104
)
103105
);
104106

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

@@ -171,6 +172,7 @@ public OCamlClientCodegen() {
171172
typeMapping.put("short", "int");
172173
typeMapping.put("char", "char");
173174
typeMapping.put("float", "float");
175+
typeMapping.put("decimal", "string");
174176
typeMapping.put("double", "float");
175177
typeMapping.put("integer", "int32");
176178
typeMapping.put("number", "float");
@@ -179,36 +181,27 @@ public OCamlClientCodegen() {
179181
typeMapping.put("any", "Yojson.Safe.t");
180182
typeMapping.put("file", "string");
181183
typeMapping.put("ByteArray", "string");
184+
typeMapping.put("AnyType", "Yojson.Safe.t");
182185
// lib
183186
typeMapping.put("string", "string");
184187
typeMapping.put("UUID", "string");
185188
typeMapping.put("URI", "string");
186189
typeMapping.put("set", "`Set");
187190
typeMapping.put("password", "string");
188191
typeMapping.put("DateTime", "string");
189-
190-
// supportedLibraries.put(CO_HTTP, "HTTP client: CoHttp.");
191-
//
192-
// CliOption libraryOption = new CliOption(CodegenConstants.LIBRARY, "library template (sub-template) to use.");
193-
// libraryOption.setEnum(supportedLibraries);
194-
// // set hyper as the default
195-
// libraryOption.setDefault(CO_HTTP);
196-
// cliOptions.add(libraryOption);
197-
// setLibrary(CO_HTTP);
198192
}
199193

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

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

210203
// for enum model
211-
if (Boolean.TRUE.equals(cm.isEnum) && cm.allowableValues != null) {
204+
if (cm.isEnum && cm.allowableValues != null) {
212205
toRemove.add(modelEntry.getKey());
213206
} else {
214207
enrichPropertiesWithEnumDefaultValues(cm.getAllVars());
@@ -219,6 +212,15 @@ public Map<String, ModelsMap> postProcessAllModels(Map<String, ModelsMap> supero
219212
enrichPropertiesWithEnumDefaultValues(cm.getVars());
220213
enrichPropertiesWithEnumDefaultValues(cm.getParentVars());
221214
}
215+
216+
if (!cm.oneOf.isEmpty()) {
217+
// Add a boolean if it is a `oneOf`, because Mustache does not let us check if a list is non-empty
218+
cm.getVendorExtensions().put("x-ocaml-isOneOf", true);
219+
}
220+
if (!cm.anyOf.isEmpty()) {
221+
// Add a boolean if it is a `anyOf`, because Mustache does not let us check if a list is non-empty
222+
cm.getVendorExtensions().put("x-ocaml-isAnyOf", true);
223+
}
222224
}
223225
}
224226

@@ -242,8 +244,7 @@ private void enrichPropertiesWithEnumDefaultValues(List<CodegenProperty> propert
242244
@Override
243245
protected void updateDataTypeWithEnumForMap(CodegenProperty property) {
244246
CodegenProperty baseItem = property.items;
245-
while (baseItem != null && (Boolean.TRUE.equals(baseItem.isMap)
246-
|| Boolean.TRUE.equals(baseItem.isArray))) {
247+
while (baseItem != null && (baseItem.isMap || baseItem.isArray)) {
247248
baseItem = baseItem.items;
248249
}
249250

@@ -260,8 +261,7 @@ protected void updateDataTypeWithEnumForMap(CodegenProperty property) {
260261
@Override
261262
protected void updateDataTypeWithEnumForArray(CodegenProperty property) {
262263
CodegenProperty baseItem = property.items;
263-
while (baseItem != null && (Boolean.TRUE.equals(baseItem.isMap)
264-
|| Boolean.TRUE.equals(baseItem.isArray))) {
264+
while (baseItem != null && (baseItem.isMap || baseItem.isArray)) {
265265
baseItem = baseItem.items;
266266
}
267267
if (baseItem != null) {
@@ -312,19 +312,17 @@ private void collectEnumSchemas(String parentName, Map<String, Schema> schemas)
312312

313313
collectEnumSchemas(parentName, sName, schema);
314314

315+
String pName = parentName != null ? parentName + "_" + sName : sName;
315316
if (schema.getProperties() != null) {
316-
String pName = parentName != null ? parentName + "_" + sName : sName;
317317
collectEnumSchemas(pName, schema.getProperties());
318318
}
319319

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

325324
if (ModelUtils.isArraySchema(schema)) {
326325
if (ModelUtils.getSchemaItems(schema) != null) {
327-
String pName = parentName != null ? parentName + "_" + sName : sName;
328326
collectEnumSchemas(pName, ModelUtils.getSchemaItems(schema));
329327
}
330328
}
@@ -677,7 +675,7 @@ private List<Map<String, Object>> buildEnumValues(String valueString) {
677675
public String toEnumValueName(String name) {
678676
if (reservedWords.contains(name)) {
679677
return escapeReservedWord(name);
680-
} else if (((CharSequence) name).chars().anyMatch(character -> specialCharReplacements.keySet().contains(String.valueOf((char) character)))) {
678+
} else if (name.chars().anyMatch(character -> specialCharReplacements.containsKey(String.valueOf((char) character)))) {
681679
return escape(name, specialCharReplacements, Collections.singletonList("_"), null);
682680
} else {
683681
return name;
@@ -723,8 +721,6 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List<Mo
723721
List<CodegenOperation> operations = objectMap.getOperation();
724722

725723
for (CodegenOperation operation : operations) {
726-
// http method verb conversion, depending on client library (e.g. Hyper: PUT => Put, Reqwest: PUT => put)
727-
//if (CO_HTTP.equals(getLibrary())) {
728724
for (CodegenParameter param : operation.bodyParams) {
729725
if (param.isModel && param.dataType.endsWith(".t")) {
730726
param.vendorExtensions.put(X_MODEL_MODULE, param.dataType.substring(0, param.dataType.lastIndexOf('.')));

modules/openapi-generator/src/main/resources/ocaml/api-impl.mustache

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ let {{{operationId}}} {{^hasParams}}(){{/hasParams}}{{#allParams}}{{> to_param}}
3030
let uri = Request.{{> to_optional_prefix}}replace_path_param uri "{{{baseName}}}" {{> to_string}} {{{paramName}}} in
3131
{{/pathParams}}
3232
{{#queryParams}}
33-
let uri = Request.{{> to_optional_prefix}}add_query_param{{#isArray}}_list{{/isArray}} uri "{{{baseName}}}" {{> to_string}} {{{paramName}}} in
33+
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
3434
{{/queryParams}}
3535
{{#hasAuthMethods}}
3636
{{#authMethods}}
@@ -42,7 +42,10 @@ let {{{operationId}}} {{^hasParams}}(){{/hasParams}}{{#allParams}}{{> to_param}}
4242
{{/authMethods}}
4343
{{/hasAuthMethods}}
4444
{{#bodyParams}}
45-
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
45+
let body = Request.
46+
{{#isByteArray}}write_string_body {{{paramName}}}{{/isByteArray}}
47+
{{^isByteArray}}write_as_json_body {{> to_json}} {{{paramName}}}{{/isByteArray}}
48+
in
4649
{{/bodyParams}}
4750
{{^hasBodyParam}}
4851
{{#hasFormParams}}

modules/openapi-generator/src/main/resources/ocaml/json.mustache

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,19 @@ let of_int32 x = `Intlit (Int32.to_string x)
5050

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

53-
let of_list_of of_f l = `List (List.map of_f l)
53+
let of_list_of of_f l = `List (Stdlib.List.map of_f l)
5454

55-
let of_map_of of_f l = `Assoc (List.map (fun (k, v) -> (k, of_f v)) l)
55+
let of_map_of of_f l = `Assoc (Stdlib.List.map (fun (k, v) -> (k, of_f v)) l)
56+
57+
let to_map_of of_f json =
58+
match json with
59+
| `Assoc l ->
60+
Stdlib.List.fold_right
61+
(fun (k, json) acc ->
62+
match (of_f json, acc) with
63+
| Stdlib.Result.Ok parsed_v, Stdlib.Result.Ok tl ->
64+
Stdlib.Result.Ok ((k, parsed_v) :: tl)
65+
| Stdlib.Result.Error e, _ -> Stdlib.Result.Error e
66+
| _, Stdlib.Result.Error e -> Stdlib.Result.Error e)
67+
l (Stdlib.Result.Ok [])
68+
| _ -> Stdlib.Result.Error "Expected"
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
type t =
2+
{{#composedSchemas.anyOf}}
3+
| {{{nameInPascalCase}}} of {{{dataType}}}
4+
{{/composedSchemas.anyOf}}
5+
[@@deriving show, eq];;
6+
7+
let to_yojson = function
8+
{{#composedSchemas.anyOf}}
9+
| {{{nameInPascalCase}}} v -> [%to_yojson: {{{ datatypeWithEnum }}}] v
10+
{{/composedSchemas.anyOf}}
11+
12+
(* Manual implementations because the derived one encodes into a tuple list where the first element is the constructor name. *)
13+
14+
let of_yojson json =
15+
[
16+
{{#composedSchemas.anyOf}}
17+
[%of_yojson: {{{ datatypeWithEnum }}}] json
18+
|> Stdlib.Result.to_option
19+
|> Stdlib.Option.map (fun v -> {{{nameInPascalCase}}} v);
20+
{{/composedSchemas.anyOf}}
21+
]
22+
|> Stdlib.List.filter_map (Fun.id)
23+
|> function
24+
| t :: _ -> Ok t (* Return the first successful parsing. *)
25+
| [] -> Error ("Failed to parse JSON " ^ Yojson.Safe.show json ^ " into a value of type {{{ classname }}}")
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
type t =
2+
{{#composedSchemas.oneOf}}
3+
| {{{nameInPascalCase}}} of {{{dataType}}}
4+
{{/composedSchemas.oneOf}}
5+
[@@deriving show, eq];;
6+
7+
let to_yojson = function
8+
{{#composedSchemas.oneOf}}
9+
| {{{nameInPascalCase}}} v -> [%to_yojson: {{{ datatypeWithEnum }}}] v
10+
{{/composedSchemas.oneOf}}
11+
12+
(* Manual implementations because the derived one encodes into a tuple list where the first element is the constructor name. *)
13+
14+
let of_yojson json =
15+
[
16+
{{#composedSchemas.oneOf}}
17+
[%of_yojson: {{{ datatypeWithEnum }}}] json
18+
|> Stdlib.Result.to_option
19+
|> Stdlib.Option.map (fun v -> {{{nameInPascalCase}}} v);
20+
{{/composedSchemas.oneOf}}
21+
]
22+
|> Stdlib.List.filter_map (Fun.id)
23+
|> function
24+
| [t] -> Ok t
25+
| [] -> Error ("Failed to parse JSON " ^ Yojson.Safe.show json ^ " into a value of type {{{ classname }}}")
26+
| ts -> let parsed_ts = ts
27+
|> Stdlib.List.map show
28+
|> Stdlib.String.concat " | "
29+
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 ^ "]")

0 commit comments

Comments
 (0)