Skip to content

Commit b02d00b

Browse files
fix(cpp-httplib-server): generate top-level enum models (#24795)
* fix(cpp-httplib-server): generate top-level enum models * fix(cpp-httplib-server): preserve enum string values * fix(cpp-httplib-server): regenerate samples * fix(cpp-httplib-server): serialize top-level string enums * model-header.mustache fix namespace closure * fix comments * regenerate samples * generate samples in codespace * test(cpp-httplib-server): cover string-valued enums with non-string type The isStringEnum fallback added in f368b5a had no test that distinguished it from the previous ModelUtils.isStringSchema check. ModelUtils.getType returns the first element of a 3.1 type set, so a `type: [null, string]` enum reports its type as "null": isStringSchema is false while isEnum is true, and model-header.mustache then emits `j = available;` instead of `j = "available";`, which does not compile. Reverting CppHttplibServerCodegen to the plain isStringSchema check makes this test fail. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(cpp-httplib-server): assert rendered to_json/from_json for top-level enums The existing enum tests only assert the isStringEnum vendor extension on the fromModel result. That flag is just the input to model-header.mustache, so a break in the template itself -- a renamed flag, an inverted section -- passed the whole suite unnoticed. Add a test that builds a string-backed and an integer-backed top-level enum schema, runs DefaultGenerator over them, and asserts on the rendered enum headers: quoted values for the former, bare values for the latter, plus the inverse via assertFileNotContains. Renaming vendorExtensions.isStringEnum throughout model-header.mustache makes it fail. Also add an integer-backed top-level enum to feature-test.json and regenerate its sample. The template's `{{^vendorExtensions.isStringEnum}}` branch had no checked-in sample output, since the spec carried a top-level string enum but no integer one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * add default branch to switch in to_json * align exceptions also for nested enum definitions * Revert nlohmann exceptions in cpp, as the corresponding methods are not directly related to nlohmann but are more general in nature. --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent e22279b commit b02d00b

12 files changed

Lines changed: 408 additions & 14 deletions

File tree

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1543,6 +1543,17 @@ public String getTypeDeclaration(Schema p) {
15431543
return typeDecl;
15441544
}
15451545

1546+
// untyped enum schemas (no explicit `type: string`) are still string-backed if their values are strings
1547+
@SuppressWarnings("rawtypes")
1548+
private boolean isStringEnumSchema(Schema schema) {
1549+
if (ModelUtils.isStringSchema(schema)) {
1550+
return true;
1551+
}
1552+
List<?> enumValues = schema.getEnum();
1553+
return enumValues != null && !enumValues.isEmpty()
1554+
&& enumValues.stream().allMatch(value -> value instanceof String);
1555+
}
1556+
15461557
@Override
15471558
@SuppressWarnings({"rawtypes", "unchecked"})
15481559
public CodegenModel fromModel(String name, Schema schema) {
@@ -1560,6 +1571,9 @@ public CodegenModel fromModel(String name, Schema schema) {
15601571
if (model != null) {
15611572
model.name = toModelName(modelName);
15621573
model.classname = model.name;
1574+
if (model.isEnum) {
1575+
model.vendorExtensions.put("isStringEnum", isStringEnumSchema(schema));
1576+
}
15631577

15641578
if (ModelUtils.hasAllOf(schema) && this.openAPI != null) {
15651579
int refCount = 0;

‎modules/openapi-generator/src/main/resources/cpp-httplib-server/README.mustache‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,18 @@ The server will start on `http://0.0.0.0:8080` with:
9797
#### {{vendorExtensions.modelNamespace}}::{{vendorExtensions.modelClassName}}
9898

9999
```cpp
100+
{{#isEnum}}
101+
// Select an enum value
102+
auto model = {{vendorExtensions.modelNamespace}}::{{vendorExtensions.modelClassName}}::{{#allowableValues}}{{#enumVars}}{{#-first}}{{name}}{{/-first}}{{/enumVars}}{{/allowableValues}};
103+
104+
// Serialize to JSON via the generated to_json free function
105+
nlohmann::json json = model;
106+
std::string jsonString = json.dump();
107+
108+
// Deserialize from JSON via the generated from_json free function
109+
auto parsedModel = nlohmann::json::parse(jsonString).get<{{vendorExtensions.modelNamespace}}::{{vendorExtensions.modelClassName}}>();
110+
{{/isEnum}}
111+
{{^isEnum}}
100112
// Create a model
101113
auto model = {{vendorExtensions.modelNamespace}}::{{vendorExtensions.modelClassName}}();
102114
{{#vars}}
@@ -109,6 +121,7 @@ std::string jsonString = json.dump();
109121

110122
// Deserialize from JSON
111123
auto parsedModel = {{vendorExtensions.modelNamespace}}::{{vendorExtensions.modelClassName}}::fromJson(nlohmann::json::parse(jsonString));
124+
{{/isEnum}}
112125
```
113126
{{/model}}
114127
{{/models}}

‎modules/openapi-generator/src/main/resources/cpp-httplib-server/model-header.mustache‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,38 @@
1616

1717
namespace {{vendorExtensions.modelNamespace}} {
1818
19+
{{#isEnum}}
20+
enum class {{vendorExtensions.modelClassName}} {
21+
{{#allowableValues}}{{#enumVars}}
22+
{{name}}{{^-last}},{{/-last}}
23+
{{/enumVars}}{{/allowableValues}}
24+
};
25+
26+
inline void to_json(nlohmann::json& j, const {{vendorExtensions.modelClassName}}& value)
27+
{
28+
switch (value)
29+
{
30+
{{#allowableValues}}{{#enumVars}}
31+
case {{vendorExtensions.modelClassName}}::{{name}}: j = {{#vendorExtensions.isStringEnum}}"{{{value}}}"{{/vendorExtensions.isStringEnum}}{{^vendorExtensions.isStringEnum}}{{value}}{{/vendorExtensions.isStringEnum}}; break;
32+
{{/enumVars}}{{/allowableValues}}
33+
default: throw nlohmann::json::type_error::create(302, "Invalid value for {{vendorExtensions.modelClassName}}", &j);
34+
}
35+
}
36+
37+
inline void from_json(const nlohmann::json& j, {{vendorExtensions.modelClassName}}& value)
38+
{
39+
{{#allowableValues}}{{#enumVars}}
40+
if (j == {{#vendorExtensions.isStringEnum}}"{{{value}}}"{{/vendorExtensions.isStringEnum}}{{^vendorExtensions.isStringEnum}}{{value}}{{/vendorExtensions.isStringEnum}})
41+
{
42+
value = {{vendorExtensions.modelClassName}}::{{name}};
43+
return;
44+
}
45+
{{/enumVars}}{{/allowableValues}}
46+
throw nlohmann::json::type_error::create(302, "Invalid value for {{vendorExtensions.modelClassName}}", &j);
47+
}
48+
49+
{{/isEnum}}
50+
{{^isEnum}}
1951
{{#vendorExtensions.isOneOfSchema}}
2052
// oneOf schema - type alias for std::variant
2153
using {{vendorExtensions.modelClassName}} = std::variant<
@@ -250,6 +282,7 @@ private:
250282
{{/vendorExtensions.isAnyOfSchema}}
251283
{{/vendorExtensions.isOneOfSchema}}
252284

285+
{{/isEnum}}
253286
} // namespace {{vendorExtensions.modelNamespace}}
254287

255288
{{/model}}

‎modules/openapi-generator/src/main/resources/cpp-httplib-server/model-source.mustache‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@
33

44
{{#models}}
55
{{#model}}
6+
{{#isEnum}}
7+
#include "{{vendorExtensions.modelClassName}}.h"
8+
9+
{{/isEnum}}
10+
{{^isEnum}}
611
{{^vendorExtensions.isOneOfSchema}}
712
{{^vendorExtensions.isAnyOfSchema}}
813
#include "{{vendorExtensions.modelClassName}}.h"
@@ -79,5 +84,6 @@ std::string {{classname}}::{{enumName}}ToString({{classname}}::{{enumName}} valu
7984
{{^vendorExtensions.isOneOfSchema}}{{^vendorExtensions.isAnyOfSchema}}
8085
} // namespace {{vendorExtensions.modelNamespace}}
8186
{{/vendorExtensions.isAnyOfSchema}}{{/vendorExtensions.isOneOfSchema}}
87+
{{/isEnum}}
8288
{{/model}}
8389
{{/models}}

‎modules/openapi-generator/src/test/java/org/openapitools/codegen/cpphttplibserver/CppHttplibServerCodegenModelTest.java‎

Lines changed: 158 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,15 @@
1919
import org.openapitools.codegen.languages.CppHttplibServerCodegen;
2020
import org.openapitools.codegen.model.ModelMap;
2121
import org.openapitools.codegen.model.ModelsMap;
22+
import io.swagger.v3.oas.models.OpenAPI;
2223
import io.swagger.v3.oas.models.media.*;
2324
import org.testng.Assert;
2425
import org.testng.annotations.Test;
2526

27+
import java.io.File;
28+
import java.io.IOException;
29+
import java.nio.file.Files;
30+
import java.nio.file.Path;
2631
import java.util.Collections;
2732
import java.util.HashMap;
2833
import java.util.List;
@@ -35,13 +40,17 @@ public class CppHttplibServerCodegenModelTest {
3540
* Wraps a single model the way {@link org.openapitools.codegen.DefaultGenerator}
3641
* does before calling {@code postProcessAllModels}, so tests can exercise the full
3742
* enum vendor-extension pipeline (identifier + original-value derivation), not just
38-
* the intermediate state produced by {@code fromModel}.
43+
* the intermediate state produced by {@code fromModel}. This mirrors
44+
* {@code DefaultGenerator}'s {@code processModels}, which runs {@code postProcessModels}
45+
* (and, for C++, {@code postProcessModelsEnum} with it) before {@code postProcessAllModels}
46+
* is ever invoked.
3947
*/
40-
private Map<String, ModelsMap> wrapForPostProcessAllModels(String name, CodegenModel model) {
48+
private Map<String, ModelsMap> wrapForPostProcessAllModels(CppHttplibServerCodegen codegen, String name, CodegenModel model) {
4149
final ModelMap modelMap = new ModelMap();
4250
modelMap.setModel(model);
4351
final ModelsMap modelsMap = new ModelsMap();
4452
modelsMap.setModels(Collections.singletonList(modelMap));
53+
codegen.postProcessModels(modelsMap);
4554
final HashMap<String, ModelsMap> allModels = new HashMap<>();
4655
allModels.put(name, modelsMap);
4756
return allModels;
@@ -173,7 +182,7 @@ public void arrayOfEnumsDeclaresValidUpperCaseIdentifiersTest() {
173182

174183
final CodegenModel model = codegen.fromModel("ModelWithColorArray", schema);
175184
final CodegenModel processedModel = codegen.postProcessAllModels(
176-
wrapForPostProcessAllModels("ModelWithColorArray", model))
185+
wrapForPostProcessAllModels(codegen, "ModelWithColorArray", model))
177186
.get("ModelWithColorArray").getModels().get(0).getModel();
178187

179188
CodegenProperty arrayProp = processedModel.vars.get(0);
@@ -221,7 +230,7 @@ public void numericEnumPropertyTest() {
221230
// postProcessAllModels, since that's the single place both the identifier and the
222231
// original spec value are derived together (see enumSerializationUsesOriginalSpecValueTest).
223232
final CodegenModel processedModel = codegen.postProcessAllModels(
224-
wrapForPostProcessAllModels("UserStatusModel", model))
233+
wrapForPostProcessAllModels(codegen, "UserStatusModel", model))
225234
.get("UserStatusModel").getModels().get(0).getModel();
226235
CodegenProperty statusProp = processedModel.vars.get(0);
227236
Assert.assertTrue((boolean) statusProp.vendorExtensions.getOrDefault("isEnum", false));
@@ -244,7 +253,7 @@ public void enumSerializationUsesOriginalSpecValueTest() {
244253

245254
final CodegenModel model = codegen.fromModel("Pet", schema);
246255
final CodegenModel processedModel = codegen.postProcessAllModels(
247-
wrapForPostProcessAllModels("Pet", model))
256+
wrapForPostProcessAllModels(codegen, "Pet", model))
248257
.get("Pet").getModels().get(0).getModel();
249258

250259
CodegenProperty statusProp = processedModel.vars.get(0);
@@ -274,15 +283,150 @@ public void enumModelTest() {
274283

275284
final CodegenModel model = codegen.fromModel("Status", enumSchema);
276285

277-
// Note: The C++ httplib server generator may not process enum-only models
278-
// in the same way as regular object models. The model might be null or empty.
279-
if (model != null) {
280-
Assert.assertEquals(model.name, "Status");
281-
// Check if it's marked as an enum in vendor extensions
282-
if (model.vendorExtensions.containsKey("x-is-enum")) {
283-
Assert.assertEquals(model.vendorExtensions.get("x-is-enum"), true);
284-
}
285-
}
286+
Assert.assertNotNull(model);
287+
Assert.assertEquals(model.name, "Status");
288+
Assert.assertTrue(model.isEnum, "top-level enum schemas must remain enum models");
289+
Assert.assertEquals(model.vendorExtensions.get("isStringEnum"), true,
290+
"string-backed top-level enums must serialize their values as JSON strings");
291+
Assert.assertNotNull(model.allowableValues);
292+
Assert.assertEquals(model.allowableValues.get("values"),
293+
java.util.Arrays.asList("ACTIVE", "INACTIVE", "PENDING"));
294+
295+
final CodegenModel processedModel = codegen.postProcessAllModels(
296+
wrapForPostProcessAllModels(codegen, "Status", model))
297+
.get("Status").getModels().get(0).getModel();
298+
Assert.assertNotNull(processedModel.vendorExtensions.get("modelClassName"));
299+
Assert.assertEquals(((List<?>) processedModel.allowableValues.get("enumVars")).size(), 3);
300+
}
301+
302+
@Test(description = "convert integer-backed top-level enum model")
303+
public void integerBackedEnumModelTest() {
304+
final CppHttplibServerCodegen codegen = new CppHttplibServerCodegen();
305+
codegen.processOpts();
306+
307+
IntegerSchema enumSchema = new IntegerSchema();
308+
enumSchema.setEnum(java.util.Arrays.asList(0, 1, 2));
309+
310+
final CodegenModel model = codegen.fromModel("NumericStatus", enumSchema);
311+
312+
Assert.assertNotNull(model);
313+
Assert.assertTrue(model.isEnum, "top-level enum schemas must remain enum models");
314+
// model-header.mustache's `{{^vendorExtensions.isStringEnum}}` branch renders these
315+
// values unquoted, so the flag must be false for integer-backed top-level enums.
316+
Assert.assertEquals(model.vendorExtensions.get("isStringEnum"), false,
317+
"integer-backed top-level enums must serialize their values as raw JSON numbers");
318+
}
319+
320+
@Test(description = "convert 3.1 nullable top-level enum model with string values")
321+
public void nullableStringEnumModelTest() {
322+
final CppHttplibServerCodegen codegen = new CppHttplibServerCodegen();
323+
codegen.processOpts();
324+
325+
// OpenAPI 3.1 `type: [null, string]`. ModelUtils.getType() returns the *first* declared
326+
// type, so ModelUtils.isStringSchema() is false here even though the enum is string-backed.
327+
JsonSchema enumSchema = new JsonSchema();
328+
enumSchema.addType("null");
329+
enumSchema.addType("string");
330+
enumSchema.setEnum(java.util.Arrays.asList("available", "pending", "sold"));
331+
332+
final CodegenModel model = codegen.fromModel("NullableStatus", enumSchema);
333+
334+
Assert.assertNotNull(model);
335+
Assert.assertTrue(model.isEnum, "top-level enum schemas must remain enum models");
336+
// Without the value-based fallback the template would emit `j = available;` instead of
337+
// `j = "available";`, which does not compile.
338+
Assert.assertEquals(model.vendorExtensions.get("isStringEnum"), true,
339+
"string-valued top-level enums must serialize their values as JSON strings even when"
340+
+ " the declared type is not plainly `string`");
341+
}
342+
343+
@Test(description = "model-header.mustache must render quoted values for string-backed "
344+
+ "top-level enums and bare values for integer-backed ones; the other enum tests "
345+
+ "only assert the isStringEnum flag that feeds this template, not its output")
346+
public void topLevelEnumHeadersRenderTypedJsonValuesTest() throws IOException {
347+
final File output = Files.createTempDirectory("cpp-httplib-server-enums").toFile();
348+
output.deleteOnExit();
349+
350+
StringSchema stringEnumSchema = new StringSchema();
351+
stringEnumSchema.setEnum(java.util.Arrays.asList("active", "inactive", "pending"));
352+
IntegerSchema integerEnumSchema = new IntegerSchema();
353+
integerEnumSchema.setEnum(java.util.Arrays.asList(0, 1, 2));
354+
355+
final OpenAPI openAPI = TestUtils.createOpenAPI();
356+
openAPI.getComponents().addSchemas("TopLevelStatus", stringEnumSchema);
357+
openAPI.getComponents().addSchemas("TopLevelPriority", integerEnumSchema);
358+
359+
final CppHttplibServerCodegen codegen = new CppHttplibServerCodegen();
360+
codegen.additionalProperties().put("modelNamespace", "models");
361+
codegen.setOutputDir(output.getAbsolutePath());
362+
363+
final List<File> files = new DefaultGenerator()
364+
.opts(new ClientOptInput().openAPI(openAPI).config(codegen))
365+
.generate();
366+
files.forEach(File::deleteOnExit);
367+
368+
// model-header.mustache guards the quoting with `{{#vendorExtensions.isStringEnum}}`, so a
369+
// `type: string` enum must emit quoted values; bare ones would be undeclared identifiers.
370+
final Path stringEnumHeader = output.toPath().resolve("models/TopLevelStatus.h");
371+
TestUtils.assertFileContains(stringEnumHeader,
372+
"case TopLevelStatus::ACTIVE: j = \"active\"; break;",
373+
"case TopLevelStatus::INACTIVE: j = \"inactive\"; break;",
374+
"case TopLevelStatus::PENDING: j = \"pending\"; break;",
375+
"if (j == \"active\")");
376+
TestUtils.assertFileNotContains(stringEnumHeader, "j = active;");
377+
// An enumerator outside the declared set must fail loudly rather than leave `j` untouched,
378+
// mirroring the throw that from_json already performs on an unrecognised JSON value.
379+
TestUtils.assertFileContains(stringEnumHeader,
380+
"default: throw nlohmann::json::type_error::create(302, "
381+
+ "\"Invalid value for TopLevelStatus\", &j);");
382+
383+
// ...while the `{{^vendorExtensions.isStringEnum}}` branch must leave a `type: integer`
384+
// enum's values bare, or they serialize as JSON strings instead of numbers.
385+
final Path integerEnumHeader = output.toPath().resolve("models/TopLevelPriority.h");
386+
TestUtils.assertFileContains(integerEnumHeader,
387+
"case TopLevelPriority::_0: j = 0; break;",
388+
"case TopLevelPriority::_1: j = 1; break;",
389+
"case TopLevelPriority::_2: j = 2; break;",
390+
"if (j == 0)");
391+
TestUtils.assertFileNotContains(integerEnumHeader, "j = \"0\";");
392+
TestUtils.assertFileContains(integerEnumHeader,
393+
"default: throw nlohmann::json::type_error::create(302, "
394+
+ "\"Invalid value for TopLevelPriority\", &j);");
395+
}
396+
397+
@Test(description = "model-source.mustache's ToString and FromString for an enum property "
398+
+ "are plain C++ conversion helpers, not nlohmann (de)serializers, so an unrecognised "
399+
+ "value must not throw nlohmann::json::type_error: ToString falls back to an empty "
400+
+ "string and FromString throws plain std::invalid_argument")
401+
public void propertyEnumConversionRejectsUnknownValuesTest() throws IOException {
402+
final File output = Files.createTempDirectory("cpp-httplib-server-prop-enums").toFile();
403+
output.deleteOnExit();
404+
405+
StringSchema statusSchema = new StringSchema();
406+
statusSchema.setEnum(java.util.Arrays.asList("available", "pending", "sold"));
407+
ObjectSchema petSchema = new ObjectSchema();
408+
petSchema.addProperty("status", statusSchema);
409+
410+
final OpenAPI openAPI = TestUtils.createOpenAPI();
411+
openAPI.getComponents().addSchemas("Pet", petSchema);
412+
413+
final CppHttplibServerCodegen codegen = new CppHttplibServerCodegen();
414+
codegen.additionalProperties().put("modelNamespace", "models");
415+
codegen.setOutputDir(output.getAbsolutePath());
416+
417+
final List<File> files = new DefaultGenerator()
418+
.opts(new ClientOptInput().openAPI(openAPI).config(codegen))
419+
.generate();
420+
files.forEach(File::deleteOnExit);
421+
422+
// Property enums (de)serialize through StatusEnumToString/StatusEnumFromString rather
423+
// than the switch/if-chain in model-header.mustache's top-level to_json/from_json, and
424+
// those helpers don't depend on nlohmann::json at all.
425+
final Path source = output.toPath().resolve("models/Pet.cpp");
426+
TestUtils.assertFileContains(source,
427+
"default: return {};",
428+
"throw std::invalid_argument(\"Invalid enum value\");");
429+
TestUtils.assertFileNotContains(source, "nlohmann::json::type_error::create(302, \"Invalid value for Pet::StatusEnum\")");
286430
}
287431

288432
@Test(description = "convert model with nullable property")

‎modules/openapi-generator/src/test/resources/3_0/cpp-httplib-server/feature-test.json‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1006,6 +1006,14 @@
10061006
}
10071007
}
10081008
},
1009+
"TopLevelStatus": {
1010+
"type": "string",
1011+
"enum": ["active", "inactive", "pending"]
1012+
},
1013+
"TopLevelPriority": {
1014+
"type": "integer",
1015+
"enum": [0, 1, 2]
1016+
},
10091017
"SimpleObject": {
10101018
"type": "object",
10111019
"required": ["id", "name"],

‎samples/server/petstore/cpp-httplib-server/feature-test/.openapi-generator/FILES‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,3 +68,7 @@ models/TestQueryParameters200Response.cpp
6868
models/TestQueryParameters200Response.h
6969
models/TestQueryParametersDeepObjectParameter.cpp
7070
models/TestQueryParametersDeepObjectParameter.h
71+
models/TopLevelPriority.cpp
72+
models/TopLevelPriority.h
73+
models/TopLevelStatus.cpp
74+
models/TopLevelStatus.h

0 commit comments

Comments
 (0)