From 8a6eaa049668ca708a469d7cb2cf8aed8340d57c Mon Sep 17 00:00:00 2001 From: Benjamin Oldenburg Date: Wed, 26 Aug 2026 20:01:28 +0700 Subject: [PATCH 01/41] refactor(cpp-boost-beast): share model pipeline between client and server Move the direction-agnostic document pipeline, model lowering, parameter serialization facts, dialect policy, and schema-IR emission from CppBoostBeastClientCodegen into CppBoostBeastModelCodegen; extract CppBoostBeastOperationFacts from the client template assembler; add additionalEmbeddedTemplateDirs locator support and move shared model/validation templates to cpp-boost-beast-common. Client generated output is byte-identical; cppboostbeast suite (158 tests) green. --- .../openapitools/codegen/CodegenConfig.java | 10 + .../openapitools/codegen/DefaultCodegen.java | 8 + .../languages/CppBoostBeastClientCodegen.java | 1298 +--------------- .../languages/CppBoostBeastModelCodegen.java | 1304 +++++++++++++++++ .../CppBoostBeastOperationFacts.java | 133 ++ .../CppBoostBeastTemplateModelAssembler.java | 94 +- .../GeneratorTemplateContentLocator.java | 12 + .../NullableField.h.mustache | 0 .../anytype-header.mustache | 0 .../licenseInfo.mustache | 11 + .../model-header.mustache | 0 .../model-source.mustache | 0 .../oas31_deep_equal.mustache | 0 .../oas31_exact_json.mustache | 0 .../oas31_exact_number.mustache | 0 .../oas31_exact_number_source.mustache | 0 .../oas31_schema_ir.mustache | 0 .../oas31_schema_ir_header.mustache | 0 .../oas31_schema_ir_source.mustache | 0 .../oas31_validator.mustache | 0 .../validation-types.mustache | 0 .../cppboostbeast/Oas31ExactRuntimeTest.java | 4 +- ...plateContentLocatorAdditionalDirsTest.java | 74 + 23 files changed, 1559 insertions(+), 1389 deletions(-) create mode 100644 modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/CppBoostBeastOperationFacts.java rename modules/openapi-generator/src/main/resources/{cpp-boost-beast-client => cpp-boost-beast-common}/NullableField.h.mustache (100%) rename modules/openapi-generator/src/main/resources/{cpp-boost-beast-client => cpp-boost-beast-common}/anytype-header.mustache (100%) create mode 100644 modules/openapi-generator/src/main/resources/cpp-boost-beast-common/licenseInfo.mustache rename modules/openapi-generator/src/main/resources/{cpp-boost-beast-client => cpp-boost-beast-common}/model-header.mustache (100%) rename modules/openapi-generator/src/main/resources/{cpp-boost-beast-client => cpp-boost-beast-common}/model-source.mustache (100%) rename modules/openapi-generator/src/main/resources/{cpp-boost-beast-client => cpp-boost-beast-common}/oas31_deep_equal.mustache (100%) rename modules/openapi-generator/src/main/resources/{cpp-boost-beast-client => cpp-boost-beast-common}/oas31_exact_json.mustache (100%) rename modules/openapi-generator/src/main/resources/{cpp-boost-beast-client => cpp-boost-beast-common}/oas31_exact_number.mustache (100%) rename modules/openapi-generator/src/main/resources/{cpp-boost-beast-client => cpp-boost-beast-common}/oas31_exact_number_source.mustache (100%) rename modules/openapi-generator/src/main/resources/{cpp-boost-beast-client => cpp-boost-beast-common}/oas31_schema_ir.mustache (100%) rename modules/openapi-generator/src/main/resources/{cpp-boost-beast-client => cpp-boost-beast-common}/oas31_schema_ir_header.mustache (100%) rename modules/openapi-generator/src/main/resources/{cpp-boost-beast-client => cpp-boost-beast-common}/oas31_schema_ir_source.mustache (100%) rename modules/openapi-generator/src/main/resources/{cpp-boost-beast-client => cpp-boost-beast-common}/oas31_validator.mustache (100%) rename modules/openapi-generator/src/main/resources/{cpp-boost-beast-client => cpp-boost-beast-common}/validation-types.mustache (100%) create mode 100644 modules/openapi-generator/src/test/java/org/openapitools/codegen/templating/GeneratorTemplateContentLocatorAdditionalDirsTest.java diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenConfig.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenConfig.java index d9b3d8550e53..13634166b0ec 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenConfig.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenConfig.java @@ -77,6 +77,16 @@ public interface CodegenConfig { String embeddedTemplateDir(); + /** + * Additional embedded (classpath) template directories searched after the + * generator's own embedded template directory. Directories are probed in + * order; the first containing the template wins. Used to share templates + * between related generators. + */ + default java.util.List additionalEmbeddedTemplateDirs() { + return java.util.Collections.emptyList(); + } + String modelFileFolder(); String modelTestFileFolder(); diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java index e8f7eb86e02a..63d8b63d39f4 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java @@ -233,6 +233,9 @@ apiTemplateFiles are for API outputs only (controllers/handlers). @Setter protected String templateDir; protected String embeddedTemplateDir; + /** Additional embedded (classpath) template directories searched after + * {@link #embeddedTemplateDir}; see {@link #additionalEmbeddedTemplateDirs()}. */ + protected List additionalEmbeddedTemplateDirs = new ArrayList<>(); protected Map additionalProperties = new HashMap<>(); protected Map serverVariables = new HashMap<>(); protected Map vendorExtensions = new HashMap<>(); @@ -1713,6 +1716,11 @@ public String embeddedTemplateDir() { } } + @Override + public List additionalEmbeddedTemplateDirs() { + return additionalEmbeddedTemplateDirs; + } + @Override public Map apiDocTemplateFiles() { return apiDocTemplateFiles; diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/CppBoostBeastClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/CppBoostBeastClientCodegen.java index edd97a8b10e4..e9e827830318 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/CppBoostBeastClientCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/CppBoostBeastClientCodegen.java @@ -39,15 +39,8 @@ public class CppBoostBeastClientCodegen extends CppBoostBeastModelCodegen { public static final String DEFAULT_PACKAGE_NAME = "CppBoostBeastOpenAPIClient"; - public static final String EXPORT_MACRO = "exportMacro"; private static final String HAS_EXPORT_MACRO = "hasExportMacro"; - /** Policy for format metadata in composition branch matching. - * Formats remain annotations and never affect branch match counts. */ - private String formatAssertionPolicy = "annotation"; - - /** Value type for the formatAssertion option. */ - private static final String FORMAT_ASSERTION_POLICY_ANNOTATION = "annotation"; /** SSE schema interpretation mode. */ private String sseSchemaMode = "representation"; @@ -58,83 +51,10 @@ public class CppBoostBeastClientCodegen extends CppBoostBeastModelCodegen { private Map sseRequestPropertyMappings = Collections.emptyMap(); private Map sseEventTypeMappings = Collections.emptyMap(); private boolean inferConditionalSseOperations = true; - /** Controls composition-branch validation during model decoding. */ - private boolean validateOnDecode = true; - /** Retains undeclared JSON object members in generated object models. */ - private boolean preserveAdditionalProperties = false; - - private static final String X_CODEGEN_IS_RAW_BODY = "x-codegen-is-raw-body"; - private static final String X_CODEGEN_IS_OPTIONAL_QUERY_PARAMETER = - "x-codegen-is-optional-query-parameter"; - // Authoritative parameter serialization facts stamped by codegenParameterStyled(). - private static final String X_CODEGEN_PARAM_STYLE = "x-codegen-param-style"; - private static final String X_CODEGEN_PARAM_EXPLODE = "x-codegen-param-explode"; - private static final String X_CODEGEN_PARAM_ALLOW_RESERVED = - "x-codegen-param-allow-reserved"; - private static final String X_CODEGEN_PARAM_ALLOW_EMPTY_VALUE = - "x-codegen-param-allow-empty-value"; - private Map componentSchemaIdsByName = Collections.emptyMap(); - - /** Starts an isolated state set for one generator invocation. */ - private void beginGeneration(OpenAPI openApi) { - sourceOpenApi = openApi; - variantModels = new HashSet<>(); - resolvedAliasTypes = new HashMap<>(); - composedKeywordsByModel = new HashMap<>(); - compositionDescriptors = new LinkedHashMap<>(); - compositionDescriptorSets = new LinkedHashMap<>(); - webhookPreservation = new ArrayList<>(); - operationCallbacks = new HashMap<>(); - operationLinks = new HashMap<>(); - allOfIntersections = new LinkedHashMap<>(); - refreshComponentSchemaIds(openApi); - - } - - /** - * swagger-parser materializes the implicit root server as {@code /}, which - * is indistinguishable from a source-level {@code servers: [{url: /}]} in - * the model. Consult the raw document before server-precedence assembly. - */ - private boolean detectExplicitRootServers() { - String inputSpec = getInputSpec(); - if (inputSpec == null || inputSpec.isEmpty()) { - // Programmatic OpenAPI instances have no parser-injected source. - return true; - } - try { - JsonNode document = Oas31RawSpecRecovery.readRawDocument(inputSpec); - return document != null && document.isObject() && document.has("servers"); - } catch (Exception exception) { - throw new IllegalStateException( - "Unable to inspect the source OpenAPI document for root servers", exception); - } - } - - /** - * Returns the composition descriptor for the given schema name, or null - * if the schema is not composed or was not indexed. - */ - public CompositionDescriptor getCompositionDescriptor(String schemaName) { - return compositionDescriptors.get(schemaName); - } - /** - * Returns an unmodifiable view of the full composition descriptor index. - */ - public Map getCompositionDescriptors() { - return Collections.unmodifiableMap(compositionDescriptors); - } - /** - * Returns every composition descriptor present on a schema, in keyword - * order: oneOf, anyOf, then allOf. - */ - public List getCompositionDescriptorsForSchema(String schemaName) { - return compositionDescriptorSets.getOrDefault(schemaName, Collections.emptyList()); - } protected String packageName = DEFAULT_PACKAGE_NAME; private String exportMacro = ""; @@ -151,156 +71,11 @@ public String getHelp() { return "Generates a cpp-boost-beast client."; } - @Override - public void preprocessOpenAPI(OpenAPI openAPI) { - beginGeneration(openAPI); - hasExplicitRootServers = detectExplicitRootServers(); - - List policyDiagnostics = validateDialectPolicy(openAPI); - if (!policyDiagnostics.isEmpty()) { - throw new IllegalArgumentException(String.join("; ", policyDiagnostics)); - } - super.preprocessOpenAPI(openAPI); - // Webhooks are inbound-only metadata for a client generator. Upstream - // folds them into the API map under the same fallback classname as path - // operations, which can replace the path API. Preserve their metadata, - // then remove them so outbound paths still generate; no listener is emitted. - if (openAPI.getWebhooks() != null && !openAPI.getWebhooks().isEmpty()) { - for (Map.Entry e : openAPI.getWebhooks().entrySet()) { - PathItem item = e.getValue(); - List methods = new ArrayList<>(); - if (item.getGet() != null) methods.add("GET " + idOf(item.getGet())); - if (item.getPut() != null) methods.add("PUT " + idOf(item.getPut())); - if (item.getPost() != null) methods.add("POST " + idOf(item.getPost())); - if (item.getDelete() != null) methods.add("DELETE " + idOf(item.getDelete())); - if (item.getPatch() != null) methods.add("PATCH " + idOf(item.getPatch())); - if (item.getHead() != null) methods.add("HEAD " + idOf(item.getHead())); - if (item.getOptions() != null) methods.add("OPTIONS " + idOf(item.getOptions())); - if (item.getTrace() != null) methods.add("TRACE " + idOf(item.getTrace())); - webhookPreservation.add(e.getKey() - + "[" + String.join(", ", methods) + "]"); - } - openAPI.setWebhooks(null); - } - // Capture callback and response-link names for generated API comments. - captureOperationMetadata(openAPI); - // Recover prefixItems dropped when the shared OAS 3.1 normalizer - // converts a type-array JsonSchema to ArraySchema. This must precede - // descriptor scanning so child schemas retain the pristine value. - Oas31RawSpecRecovery.restoreNormalizerDroppedPrefixItems(openAPI, getInputSpec()); - Oas31RawSpecRecovery.recoverPristineLiterals(openAPI, getInputSpec()); - // Populate variantModels and build composition descriptors before - // model processing begins so that getTypeDeclaration can resolve $ref - // to composed models as value types and branch semantics are captured - // before fromModel consumes composed schemas. - Map schemas = openAPI.getComponents() != null - ? openAPI.getComponents().getSchemas() : null; - if (schemas != null) { - // Build descriptor index: must happen after inline model resolver - // flattening so all inline schemas have been extracted to component - // references with stable $ref targets. - for (Map.Entry entry : schemas.entrySet()) { - String schemaName = entry.getKey(); - Schema schema = entry.getValue(); - List descriptors = - Oas31CompositionLowering.buildCompositionDescriptors( - schemaName, schema, openAPI, schemas); - if (!descriptors.isEmpty()) { - String modelName = toModelName(schemaName); - // The primary descriptor drives representation lowering; - // retain and validate every composition keyword separately. - compositionDescriptors.put(modelName, descriptors.get(0)); - compositionDescriptorSets.put(modelName, Collections.unmodifiableList( - new ArrayList<>(descriptors))); - for (CompositionDescriptor descriptor : descriptors) { - Oas31CompositionLowering.validateDescriptorAssertions(descriptor); - } - } - // allOf affects object storage even when oneOf or anyOf selects - // the public representation. - if (schema.getAllOf() != null && !schema.getAllOf().isEmpty()) { - AllOfIntersection intersection = - Oas31CompositionLowering.computeAllOfIntersection( - schemaName, schema, openAPI, schemas, new HashSet<>()); - if (intersection != null) { - allOfIntersections.put(toModelName(schemaName), intersection); - } - } - if ((schema.getOneOf() != null && !schema.getOneOf().isEmpty()) - || (schema.getAnyOf() != null && !schema.getAnyOf().isEmpty())) { - variantModels.add(schemaName); - } - } - } -} // ======================================================================== // OAS 3.1 dialect and schema policy // ======================================================================== - /** Pinned OAS 3.1 Schema dialect (spec.openapis.org/oas/3.1/dialect/2024-11-10). */ - public static final String OAS_31_DIALECT = - "https://spec.openapis.org/oas/3.1/dialect/2024-11-10"; - - /** OAS alias accepted only as the identifier for the same pinned revision. */ - public static final String OAS_31_DIALECT_BASE_ALIAS = - "https://spec.openapis.org/oas/3.1/dialect/base"; - - /** Plain JSON Schema Draft 2020-12 core identifier (non-OAS dialect). */ - public static final String DRAFT_2020_12 = - "https://json-schema.org/draft/2020-12/schema"; - - /** Classified effective schema dialect for an OpenAPI document. */ - public enum OasDialect { - /** OAS 3.1 pinned dialect (or its base alias). */ - OAS_31, - /** Plain JSON Schema Draft 2020-12 (not OAS-wrapped). */ - DRAFT_2020_12_REC, - /** A dialect identifier not recognized by this program. */ - UNRECOGNIZED, - /** No dialect declared (OAS 3.1 default applies for OAS 3.1 documents). */ - UNSPECIFIED - } - - /** - * Dialect resolution, normative-structure checks, and the exhaustive - * keyword-occurrence scanner live in {@link Oas31KeywordScanner}; - * the delegates below keep this generator's public API stable for - * tests and templates. - */ - public static OasDialect resolveEffectiveDialect(String jsonSchemaDialect, String rootSchema) { - return Oas31KeywordScanner.resolveEffectiveDialect(jsonSchemaDialect, rootSchema); - } - - /** Resolve the effective dialect of an OpenAPI document from its declared knobs. */ - public static OasDialect resolveDocumentDialect(OpenAPI openAPI) { - return Oas31KeywordScanner.resolveDocumentDialect(openAPI); - } - - /** OAS 3 structural normative checks (see {@link Oas31KeywordScanner}). */ - public List validateNormativeOas3Structure(OpenAPI openAPI) { - return Oas31KeywordScanner.validateNormativeOas3Structure(openAPI); - } - - /** Dialect/metaschema policy gate (see {@link Oas31KeywordScanner}). */ - public List validateDialectPolicy(OpenAPI openAPI) { - return Oas31KeywordScanner.validateDialectPolicy(openAPI); - } - - - /** - * Exhaustive schema-valued-position scanner (see {@link Oas31KeywordScanner}). - */ - public Oas31KeywordScanner.KeywordOccurrenceLedger scanSchemaKeywordOccurrences( - OpenAPI openAPI) { - return Oas31KeywordScanner.scanSchemaKeywordOccurrences(openAPI); - } - - - /** Set of fail-closed required-vocabulary keywords for this document. */ - public Set failClosedKeywords(OpenAPI openAPI) { - return Oas31KeywordScanner.failClosedKeywords(openAPI); - } public CppBoostBeastClientCodegen() { @@ -515,71 +290,7 @@ public CppBoostBeastClientCodegen() { importMapping.put("AnyType", "#include \"AnyType.h\""); } - @Override - protected ImmutableMap.Builder addMustacheLambdas() { - return super.addMustacheLambdas() - .put("cppStringLiteral", (fragment, writer) -> writer.write( - escapeCppStringContent( - StringEscapeUtils.unescapeHtml4(fragment.execute())))); - } - - @Override - public String escapeText(String input) { - return input == null ? null : escapeCppStringContent(input); - } - - /** - * Generator-specific normalizer that preserves composition structure - * (branch cardinality, null multiplicity, original keyword) for all - * oneOf/anyOf/anyOf-string-enum schemas. Set-equivalent simplification - * happens later in the generator's semantic analyzer (processComposedModel), - * never in the pre-descriptor normalizer. - */ - public static class CppBoostBeastOpenAPINormalizer extends OpenAPINormalizer { - public CppBoostBeastOpenAPINormalizer(OpenAPI openAPI, Map inputRules) { - super(openAPI, inputRules); - } - - @Override - protected Schema processSimplifyAnyOf(Schema schema) { - if (schema.getAnyOf() != null && !schema.getAnyOf().isEmpty()) { - return schema; - } - return super.processSimplifyAnyOf(schema); - } - - @Override - protected Schema processSimplifyOneOf(Schema schema) { - if (schema.getOneOf() != null && !schema.getOneOf().isEmpty()) { - return schema; - } - return super.processSimplifyOneOf(schema); - } - @Override - protected Schema processSimplifyAnyOfStringAndEnumString(Schema schema) { - if (schema.getAnyOf() != null && !schema.getAnyOf().isEmpty()) { - return schema; - } - return super.processSimplifyAnyOfStringAndEnumString(schema); - } - - @Override - protected Schema processSimplifyOneOfEnum(Schema schema) { - if (schema.getOneOf() != null && !schema.getOneOf().isEmpty()) { - return schema; - } - return super.processSimplifyOneOfEnum(schema); - } - - @Override - protected Schema processSimplifyAnyOfEnum(Schema schema) { - if (schema.getAnyOf() != null && !schema.getAnyOf().isEmpty()) { - return schema; - } - return super.processSimplifyAnyOfEnum(schema); - } - } @@ -589,28 +300,7 @@ protected Schema processSimplifyAnyOfEnum(Schema schema) { * @param name string to be camelized * @return Camelized string */ - @Override - public String getterAndSetterCapitalize(String name) { - if (name == null || name.length() == 0) { - return name; - } - - name = toVarName(name); - if (name.startsWith("_")) { - return "_" + camelize(name); - } - - return camelize(name); - } - - private static boolean isSchemaValidationSupportingFile(SupportingFile file) { - String destination = file.getDestinationFilename(); - return "Oas31SchemaRegistry.h".equals(destination) - || "schema_ir.generated.cpp".equals(destination) - || (destination.startsWith("schema_ir.generated.chunk") - && destination.endsWith(".cpp")); - } private static Set parseNameSet(Object rawValue, String optionName) { if (rawValue == null || rawValue.toString().trim().isEmpty()) { @@ -703,29 +393,7 @@ public void processOpts() { additionalProperties.remove("exportDefine"); additionalProperties.remove("exportHeaderGuard"); } - String modelNamespace = modelPackage.replaceAll("\\.", "::"); - additionalProperties.put("modelNamespaceDeclarations", modelPackage.split("\\.")); - additionalProperties.put("modelNamespace", modelNamespace); - additionalProperties.put("schemaValidationNamespace", - modelNamespace + "::detail::schema_validation"); - additionalProperties.put("schemaValidationHeaderGuardPrefix", - modelPackage.replaceAll("[^A-Za-z0-9]", "_").toUpperCase(Locale.ROOT)); - additionalProperties.put("apiHeaderGuardPrefix", - apiPackage.replaceAll("[^A-Za-z0-9]", "_").toUpperCase(Locale.ROOT)); - additionalProperties.put("apiNamespaceDeclarations", apiPackage.split("\\.")); - additionalProperties.put("apiNamespace", apiPackage.replaceAll("\\.", "::")); - - if (additionalProperties.containsKey("formatAssertionPolicy")) { - String policy = additionalProperties.get("formatAssertionPolicy") - .toString().trim().toLowerCase(Locale.ROOT); - if (!FORMAT_ASSERTION_POLICY_ANNOTATION.equals(policy)) { - throw new IllegalArgumentException( - "formatAssertionPolicy supports only 'annotation'; " - + "format assertions are not implemented"); - } - } - formatAssertionPolicy = FORMAT_ASSERTION_POLICY_ANNOTATION; - additionalProperties.put("formatAssertionPolicy", formatAssertionPolicy); + applySharedCppOptions(); // Configure whether SSE schemas describe the wire representation or the // parsed JSON event data. Unknown values use the documented default. @@ -766,402 +434,8 @@ public void processOpts() { } additionalProperties.put("inferConditionalSseOperations", inferConditionalSseOperations); - - // compileWithValidation controls decode-time composition-branch checks. - // Representation safety checks remain active regardless of this option. - if (additionalProperties.containsKey("compileWithValidation")) { - Object raw = additionalProperties.get("compileWithValidation"); - if (raw instanceof Boolean) { - validateOnDecode = (Boolean) raw; - } else { - validateOnDecode = Boolean.parseBoolean(raw.toString().trim()); - } - } - additionalProperties.put("validateOnDecode", validateOnDecode); - additionalProperties.put("compileWithValidation", validateOnDecode); - if (!validateOnDecode) { - supportingFiles.removeIf(CppBoostBeastClientCodegen::isSchemaValidationSupportingFile); - } - preserveAdditionalProperties = false; - if (additionalProperties.containsKey("preserveAdditionalProperties")) { - Object raw = additionalProperties.get("preserveAdditionalProperties"); - if (raw instanceof Boolean) { - preserveAdditionalProperties = (Boolean) raw; - } else { - String value = raw.toString().trim(); - if (!"true".equalsIgnoreCase(value) && !"false".equalsIgnoreCase(value)) { - throw new IllegalArgumentException( - "preserveAdditionalProperties must be true or false: " + value); - } - preserveAdditionalProperties = Boolean.parseBoolean(value); - } - } - additionalProperties.put("preserveAdditionalProperties", preserveAdditionalProperties); - if (additionalProperties.containsKey("tolerateNonNullableNulls")) { - Object raw = additionalProperties.get("tolerateNonNullableNulls"); - if (raw instanceof Boolean) { - tolerateNonNullableNulls = (Boolean) raw; - } else { - tolerateNonNullableNulls = Boolean.parseBoolean(raw.toString().trim()); - } - } - additionalProperties.put("tolerateNonNullableNulls", tolerateNonNullableNulls); - } - - /** - * Location to write model files. You can use the modelPackage() as defined - * when the class is instantiated - */ - @Override - public String modelFileFolder() { - return (outputFolder + "/model").replace("/", File.separator); - } - - /** - * Location to write api files. You can use the apiPackage() as defined when - * the class is instantiated - */ - @Override - public String apiFileFolder() { - return (outputFolder + "/api").replace("/", File.separator); - } - - @Override - public String toModelImport(String name) { - if (importMapping.containsKey(name)) { - return importMapping.get(name); - } else { - return "#include \"" + name + ".h\""; - } - } - - @Override - public CodegenModel fromModel(String name, Schema model) { - // Flatten allOf into a synthetic schema with intersected properties and - // unioned required names. Clearing allOf gives every property direct owned - // storage rather than generated inheritance. - Schema modelArg = model; - if (model != null && model.getAllOf() != null && !model.getAllOf().isEmpty()) { - AllOfIntersection intersection = allOfIntersections.get( - toModelName(name)); - if (intersection != null) { - // Check for unsatisfiable required properties / scalar conflicts - if (!intersection.isSatisfiable()) { - throw new AllOfRequiredUnsatisfiableException( - name, intersection.getUnsatisfiableReason()); - } - - Schema synthetic = Oas31CompositionLowering.buildSyntheticAllOfSchema( - name, intersection); - // Copy top-level attributes from original model - if (model.getDiscriminator() != null) { - synthetic.setDiscriminator(model.getDiscriminator()); - } - if (Boolean.TRUE.equals(model.getNullable())) { - synthetic.setNullable(true); - } - if (model.getDescription() != null) { - synthetic.setDescription(model.getDescription()); - } - if (model.getFormat() != null && intersection.getRootScalarType() != null) { - synthetic.setFormat(model.getFormat()); - } - // Optional impossible properties retain their API surface but - // reject any JSON object in which they are present. - if (!intersection.getOptionalImpossibleProperties().isEmpty()) { - Map ext = synthetic.getExtensions(); - if (ext == null) { - ext = new LinkedHashMap<>(); - synthetic.setExtensions(ext); - } - ext.put("x-cpp-optional-impossible-properties", - new ArrayList<>(intersection.getOptionalImpossibleProperties())); - } - // Flat: allOf = null so super.fromModel sees no parent - synthetic.setAllOf(null); - modelArg = synthetic; - } - } - - // Pre-check: The OpenAPI 3.1 parser converts anyOf [T, null] into - // {type: T, nullable: true} or {$ref: X, nullable: true}, consuming - // the anyOf list. Detect these nullable schemas and produce the - // correct std::optional type. - // - // For $ref schemas (normalised anyOf/oneOf [T, null] where T was a - // $ref), getTypeDeclaration resolves the target and returns the - // correct C++ type. For arrays, getTypeDeclaration returns the - // container type (e.g. std::vector<...>) without optional wrapping, - // so we wrap it here. Inline object schemas (type=object, no $ref) - // are full class models — they stay out of the alias precomputation - // because getTypeDeclaration would return the raw OAS type name - // "object" instead of the model name. They are handled separately - // below via variant model registration. - boolean isNullableSchema = model != null - && Boolean.TRUE.equals(model.getNullable()) - && (model.get$ref() != null - || (model.getType() != null && !"object".equals(model.getType()))); - String preComputedNullUnionType = null; - if (isNullableSchema) { - // Resolve the type to its C++ type and wrap in std::optional - String innerType = getTypeDeclaration(model); - // getTypeDeclaration already returns std::optional for nullable. - // Use it directly if it starts with std::optional<. - if (innerType.startsWith("std::optional<")) { - preComputedNullUnionType = innerType; - } else { - preComputedNullUnionType = "std::optional<" + innerType + ">"; - } - } else if (model != null) { - // Also try the anyOf/oneOf path for cases where the parser - // preserved the composed schema structure. - preComputedNullUnionType = detectNullUnion(model, name); - } - - CodegenModel codegenModel = super.fromModel(name, modelArg); - if (codegenModel == null) { - return null; - } - - codegenModel.vendorExtensions.put( - "x-cpp-component-schema-id", - componentSchemaId(name, componentSchemaIdsByName)); - - // Post-check: Apply the pre-computed null union type if the default - // pipeline consumed the composed schemas. - if (preComputedNullUnionType != null) { - codegenModel.dataType = preComputedNullUnionType; - codegenModel.vendorExtensions.put("x-cpp-type", preComputedNullUnionType); - codegenModel.vendorExtensions.put("x-cpp-composed-keyword", - model.getAnyOf() != null ? "anyOf" : "oneOf"); - codegenModel.vendorExtensions.put("x-cpp-is-alias", true); - codegenModel.vendorExtensions.put("x-cpp-is-optional", true); - // Force a model header/source so Gate A inventory and $ref users get - // `using NullableString = std::optional;`. DefaultCodegen - // marks plain nullable primitives as isAlias and skips file emission. - codegenModel.isAlias = false; - resolvedAliasTypes.put(name, preComputedNullUnionType); - variantModels.add(name); - } - - // Post-check: Inline nullable object schemas (type=object, nullable=true, - // no $ref) are full class models with properties — they cannot use the - // alias path. Register them as variant models so $ref references use value - // semantics (std::shared_ptr → NullableObject) and tag - // the model as optional for correct null-value representation. - if (model != null && model.get$ref() == null - && "object".equals(model.getType()) - && Boolean.TRUE.equals(model.getNullable())) { - variantModels.add(name); - codegenModel.vendorExtensions.put("x-cpp-is-optional", true); - } - - Set oldImports = codegenModel.imports; - codegenModel.imports = new HashSet<>(); - for (String imp : oldImports) { - String newImp = toModelImport(imp); - if (!newImp.isEmpty()) { - codegenModel.imports.add(newImp); - } - } - // Every model header declares vector conversion helpers. - codegenModel.imports.add("#include "); - if (preserveAdditionalProperties) { - codegenModel.imports.add("#include "); - codegenModel.imports.add("#include "); - codegenModel.imports.add("#include "); - reserveExtraJsonPropertyIdentifiers(codegenModel); - } - - // Fixed-const properties: OAS 3.1 `const`, single-value `enum`, or optional - // vendor extension `x-stainless-const`. Portable path is OAS `const` / single enum — - // vendor extensions are never required for correct encode/decode. - if (codegenModel.vars != null) { - Map allProps = new LinkedHashMap<>(); - if (model.getProperties() != null) { - allProps.putAll(model.getProperties()); - } - if (model.getAllOf() != null && openAPI != null) { - for (Object parentObj : model.getAllOf()) { - if (parentObj instanceof Schema) { - Schema parentSchema = ModelUtils.getReferencedSchema( - openAPI, (Schema) parentObj); - if (parentSchema != null && parentSchema.getProperties() != null) { - allProps.putAll(parentSchema.getProperties()); - } - } - } - } - for (CodegenProperty var : codegenModel.vars) { - Object rawProp = allProps.get(var.baseName); - if (!(rawProp instanceof Schema)) { - continue; - } - Schema varSchema = (Schema) rawProp; - boolean hasOasConst = varSchema.getConst() != null; - boolean hasSingleValueEnum = varSchema.getEnum() != null - && varSchema.getEnum().size() == 1; - boolean hasStainlessConst = varSchema.getExtensions() != null - && Boolean.TRUE.equals(varSchema.getExtensions().get("x-stainless-const")); - if (!(hasOasConst || hasSingleValueEnum || hasStainlessConst)) { - continue; - } - String constRawValue = null; - if (varSchema.getConst() != null) { - constRawValue = varSchema.getConst().toString(); - } else if (varSchema.getEnum() != null && !varSchema.getEnum().isEmpty()) { - constRawValue = varSchema.getEnum().get(0).toString(); - } - if (constRawValue == null && var.example != null) { - constRawValue = var.example; - } - if (constRawValue == null) { - constRawValue = "std::string".equals(var.dataType) ? "" : "0"; - } - String inlineValue; - boolean isStringConst = "std::string".equals(var.dataType) - || "std::optional".equals(var.dataType) - || (var.isString && !var.isInteger && !var.isLong && !var.isNumber - && !var.isBoolean); - if ("std::optional".equals(var.dataType)) { - inlineValue = "std::optional{\"" - + escapeCppStringContent(constRawValue) + "\"}"; - } else if (isStringConst || "std::string".equals(var.dataType)) { - inlineValue = "\"" + escapeCppStringContent(constRawValue) + "\""; - } else { - inlineValue = constRawValue; - } - // Neutral OAS-first flag used by templates. - var.vendorExtensions.put("x-cpp-const", true); - var.vendorExtensions.put("x-cpp-const-value", constRawValue); - var.vendorExtensions.put("x-cpp-const-inline-value", inlineValue); - // Mustache is truthy on key presence — only set when string-typed. - if (isStringConst || "std::string".equals(var.dataType) - || "std::optional".equals(var.dataType)) { - var.vendorExtensions.put("x-cpp-const-is-string", true); - } else if (var.isBoolean || "bool".equals(var.dataType) - || "std::optional".equals(var.dataType)) { - var.vendorExtensions.put("x-cpp-const-is-boolean", true); - } - // Keep stainless keys as aliases so older template forks still work. - var.vendorExtensions.put("x-stainless-const", true); - var.vendorExtensions.put("x-stainless-const-value", constRawValue); - var.vendorExtensions.put("x-stainless-const-inline-value", inlineValue); - } - } - - addContainerPropertyNames(codegenModel.vars); - return codegenModel; - } - - @Override - public CodegenParameter fromParameter(Parameter parameter, Set imports) { - CodegenParameter codegenParameter = super.fromParameter(parameter, imports); - // Preserve serialization facts for every parameter location. - codegenParameterStyled(parameter, codegenParameter); - if (!codegenParameter.isQueryParam) { - return codegenParameter; - } - - if (!codegenParameter.required) { - codegenParameter.vendorExtensions.put(X_CODEGEN_IS_OPTIONAL_QUERY_PARAMETER, true); - } - return codegenParameter; - } - - /** - * Records the OAS 3.1 serialization facts consumed by the C++ wire layer. - * Style defaults to form for query/cookie and simple for path/header. Explode - * defaults to true only for form. allowReserved is surfaced consistently; - * allowEmptyValue applies only to form-style query parameters. - */ - private void codegenParameterStyled(Parameter parameter, - CodegenParameter codegenParameter) { - String style = parameter.getStyle() == null - ? null : parameter.getStyle().toString(); - if (style == null) { - if (codegenParameter.isQueryParam || codegenParameter.isCookieParam) { - style = "form"; - } else { - style = "simple"; // path, header - } - } - Boolean explode = Boolean.TRUE.equals(parameter.getExplode()); - if (parameter.getExplode() == null) { - explode = "form".equals(style); // spec default - } - codegenParameter.vendorExtensions.put(X_CODEGEN_PARAM_STYLE, style); - codegenParameter.vendorExtensions.put(X_CODEGEN_PARAM_EXPLODE, explode); - codegenParameter.vendorExtensions.put(X_CODEGEN_PARAM_ALLOW_RESERVED, - Boolean.TRUE.equals(parameter.getAllowReserved())); - codegenParameter.vendorExtensions.put(X_CODEGEN_PARAM_ALLOW_EMPTY_VALUE, - Boolean.TRUE.equals(parameter.getAllowEmptyValue())); - } - - private String queryCollectionDelimiter(Parameter.StyleEnum style) { - if (style == Parameter.StyleEnum.SPACEDELIMITED) { - return "%20"; - } - if (style == Parameter.StyleEnum.PIPEDELIMITED) { - return "%7C"; - } - return ","; - } - - private void addContainerPropertyNames(List properties) { - for (CodegenProperty property : properties) { - CodegenProperty item = property.items; - while (item != null) { - item.vendorExtensions.put("x-container-property-name", property.name); - item = item.items; - } - } } - private void reserveExtraJsonPropertyIdentifiers(CodegenModel codegenModel) { - Set propertyAccessors = new HashSet<>(); - Set propertyMembers = new HashSet<>(); - if (codegenModel.allVars != null) { - for (CodegenProperty property : codegenModel.allVars) { - if (property.getter != null) { - propertyAccessors.add(property.getter); - } - if (property.setter != null) { - propertyAccessors.add(property.setter); - } - if (property.name != null) { - propertyMembers.add("m_" + property.name); - } - } - } - - int maxSuffix = propertyAccessors.size() + propertyMembers.size() + 2; - for (int suffix = 1; suffix <= maxSuffix; suffix++) { - String suffixText = suffix == 1 ? "" : Integer.toString(suffix); - String getter = "getExtraJsonProperties" + suffixText; - String setter = "setExtraJsonProperties" + suffixText; - String member = "m_extraJsonProperties" + suffixText; - if (propertyAccessors.contains(getter) || propertyAccessors.contains(setter) - || propertyMembers.contains(member)) { - continue; - } - codegenModel.vendorExtensions.put("x-cpp-extra-json-properties-getter", getter); - codegenModel.vendorExtensions.put("x-cpp-extra-json-properties-setter", setter); - codegenModel.vendorExtensions.put("x-cpp-extra-json-properties-member", member); - return; - } - throw new IllegalStateException("Unable to reserve C++ extra JSON property identifiers"); - } - - @Override - public String toModelFilename(String name) { - return toModelName(name); - } - - @Override - public String toApiFilename(String name) { - return toApiName(name); - } @Override public OperationsMap postProcessOperationsWithModels( @@ -1189,502 +463,8 @@ public OperationsMap postProcessOperationsWithModels( * @return a string value used as the `dataType` field for model templates, * `returnType` for api templates */ - @Override - public String getTypeDeclaration(Schema p) { - // Handle inline oneOf/anyOf composed schemas (apply lowering rules directly) - if (ModelUtils.isComposedSchema(p) && (p.getOneOf() != null || p.getAnyOf() != null)) { - return lowerInlineComposedSchema(p); - } - - String openAPIType = getSchemaType(p); - - if (ModelUtils.isArraySchema(p)) { - // Use getItems() directly to handle both OpenAPI 3.0 and 3.1 - Schema inner = p.getItems(); - String arrayType; - if (inner != null) { - arrayType = getSchemaType(p) + "<" + getTypeDeclaration(inner) + ">"; - } else { - arrayType = "std::vector"; - } - // Nullable arrays must be wrapped in std::optional so null JSON - // values are representable. The array branch returns before the - // nullable fallback checks at the end of this method. - if (ModelUtils.isNullable(p)) { - return "std::optional<" + arrayType + ">"; - } - return arrayType; - } else if (ModelUtils.isMapSchema(p)) { - Schema inner = ModelUtils.getAdditionalProperties(p); - String innerType = inner == null ? "boost::json::value" : getTypeDeclaration(inner); - String mapType = getSchemaType(p) + ""; - // Nullable maps must be wrapped in std::optional so null JSON - // values are representable. The map branch returns before the - // nullable fallback checks at the end of this method. - if (ModelUtils.isNullable(p)) { - return "std::optional<" + mapType + ">"; - } - return mapType; - } else if (ModelUtils.isByteArraySchema(p)) { - return "std::string"; - } else if (ModelUtils.isStringSchema(p) - || ModelUtils.isDateSchema(p) - || ModelUtils.isDateTimeSchema(p) || ModelUtils.isFileSchema(p) - || languageSpecificPrimitives.contains(openAPIType) - || typeMapping.containsKey(openAPIType) - || typeMapping.values().contains(openAPIType)) { - // Resolve through type mapping for scalar allOf: composed schemas - // return OAS raw types (e.g. "string") or mapped types (e.g. - // "std::string") depending on branch resolution path. - // Re-map if the value is already in the type mapping values. - String resolved = typeMapping.containsKey(openAPIType) - ? typeMapping.get(openAPIType) - : toModelName(openAPIType); - // OAS 3.0 nullable: true → std::optional - if (ModelUtils.isNullable(p)) { - return "std::optional<" + resolved + ">"; - } - return resolved; - } else if (ModelUtils.isNullType(p)) { - // Handle OpenAPI 3.1 null type - return "std::nullptr_t"; - } else if (ModelUtils.isAnyType(p) || ModelUtils.isFreeFormObject(p, openAPI)) { - return "boost::json::value"; - } - - // OAS 3.0 nullable: true → std::optional - if (ModelUtils.isNullable(p)) { - return "std::optional<" + openAPIType + ">"; - } - - // Variant models use value semantics (no shared_ptr wrapping) - if (variantModels.contains(openAPIType)) { - return openAPIType; - } - - // Object references use shared ownership because circular-reference facts - // are unavailable when this declaration is computed. Variant aliases are - // handled above as value types. - return "std::shared_ptr<" + openAPIType + ">"; - } - - /** - * Resolves an inline oneOf/anyOf schema to its lowered C++ type by computing - * branch types and applying the same ordered lowering rules as model-level types. - */ - private String lowerInlineComposedSchema(Schema p) { - String composedKeyword; - List children; - if (p.getOneOf() != null) { - children = p.getOneOf(); - composedKeyword = "oneOf"; - } else { - children = p.getAnyOf(); - composedKeyword = "anyOf"; - } - - List composedBranches = new ArrayList<>(); - for (Schema child : children) { - // Compute the branch type using the full type declaration pipeline - // but strip shared_ptr for variant members (value semantics). - String childType = stripSharedPtr(getTypeDeclaration(child)); - // Resolve $ref targets that are aliased to primitive types at the - // declaration point, before resolvedAliasTypes is available (it is - // populated during postProcessModels, which runs later). This handles - // inline schemas like CreateAssistantRequest_model = oneOf [string, - // $ref AssistantSupportedModels] where the target is anyOf [string, - // string-enum] → std::string, collapsing to just std::string. - Schema resolvedChild = child; - if (!childType.startsWith("std::") && !childType.startsWith("boost::") - && !childType.startsWith("std::shared_ptr<")) { - Schema resolvedTarget = child.get$ref() != null && openAPI != null - ? ModelUtils.getReferencedSchema(openAPI, child) : null; - if (resolvedTarget != null) { - resolvedChild = resolvedTarget; - String resolved = getTypeDeclaration(resolvedTarget); - String stripped = stripSharedPtr(resolved); - if (!stripped.equals(childType)) { - childType = stripped; - } - } - } - boolean isEnum = resolvedChild.getEnum() != null && !resolvedChild.getEnum().isEmpty(); - boolean isStringLike = ModelUtils.isStringSchema(resolvedChild) - || "std::string".equals(childType); - composedBranches.add(new ComposedBranch(childType, isEnum, isStringLike, -1)); - } - - // Deduplicate inside lowerComposedTypes so oneOf branch identity survives - // identical lowered C++ types. - return Oas31CompositionLowering.lowerComposedTypes( - composedBranches, composedKeyword, null, LOGGER::warn); - } - - @Override - public CodegenProperty fromProperty(String name, Schema p, boolean required, - boolean schemaIsFromAdditionalProperties) { - CodegenProperty prop = super.fromProperty(name, p, required, schemaIsFromAdditionalProperties); - if (prop == null || p == null) { - return prop; - } - // Tag inline composed properties so templates can honor oneOf vs anyOf - // decode rules (exactly-one vs first-match) instead of always using - // the generic JsonValueConverter exactly-one path. - if (p.getOneOf() != null && !p.getOneOf().isEmpty()) { - prop.vendorExtensions.put("x-cpp-composed-keyword", "oneOf"); - prop.vendorExtensions.put("x-cpp-is-oneof", true); - } else if (p.getAnyOf() != null && !p.getAnyOf().isEmpty()) { - prop.vendorExtensions.put("x-cpp-composed-keyword", "anyOf"); - prop.vendorExtensions.put("x-cpp-is-anyof", true); - } - if (Oas31RawSpecRecovery.hasExplicitDefault(p)) { - String defaultValue = explicitScalarDefaultValue(prop, p); - if (defaultValue != null) { - prop.defaultValue = defaultValue; - prop.vendorExtensions.put("x-cpp-has-explicit-default", true); - prop.vendorExtensions.put(X_CPP_EXPLICIT_DEFAULT_SCALAR, defaultValue); - prop.vendorExtensions.put("x-cpp-default-is-null", - "null".equals(Oas31RawSpecRecovery.defaultJsonOf(p))); - } - } - return prop; - } - - private String explicitScalarDefaultValue(CodegenProperty property, Schema schema) { - String json = Oas31RawSpecRecovery.defaultJsonOf(schema); - if (json == null) { - return null; - } - - com.fasterxml.jackson.databind.JsonNode value; - try { - value = io.swagger.v3.core.util.Json31.mapper().readTree(json); - } catch (com.fasterxml.jackson.core.JsonProcessingException exception) { - throw new IllegalArgumentException( - "Unable to parse default for property '" + property.baseName + "'", exception); - } - if (value == null || !value.isValueNode()) { - return null; - } - - Object nullableInner = property.vendorExtensions.get( - "x-cpp-nullable-field-inner-type"); - if (value.isNull()) { - if (nullableInner != null) { - return "NullableField<" + nullableInner + ">::makeDefaultNull()"; - } - if (property.dataType != null - && property.dataType.startsWith("std::optional<")) { - return "std::nullopt"; - } - if ("std::nullptr_t".equals(property.dataType)) { - return "nullptr"; - } - if ("boost::json::value".equals(property.dataType)) { - return "boost::json::value(nullptr)"; - } - if (property.dataType != null - && property.dataType.startsWith("std::shared_ptr<")) { - // A branch-local default:null is an annotation, not a model value. - // Ignore it rather than rejecting an otherwise legal schema. - return null; - } - - throw new IllegalArgumentException( - "JSON null default is not representable by C++ property '" - + property.baseName + "' of type " + property.dataType); - } - - String expression; - if (value.isTextual()) { - expression = "\"" + escapeCppStringContent(value.textValue()) + "\""; - } else if (value.isBoolean()) { - expression = value.booleanValue() ? "true" : "false"; - } else if (value.isNumber()) { - expression = explicitNumericDefault(property, value.decimalValue()); - } else { - return null; - } - - if (nullableInner != null) { - return "NullableField<" + nullableInner + ">::makeDefaultValue(" - + expression + ")"; - } - return expression; - } - - private static String explicitNumericDefault( - CodegenProperty property, java.math.BigDecimal value) { - if (property.isInteger || property.isLong) { - java.math.BigInteger integer; - try { - integer = value.toBigIntegerExact(); - } catch (ArithmeticException exception) { - throw new IllegalArgumentException( - "Non-integral default is not representable by integer property '" - + property.baseName + "'", exception); - } - if (property.isLong || "std::int64_t".equals(property.dataType)) { - java.math.BigInteger min = java.math.BigInteger.valueOf(Long.MIN_VALUE); - java.math.BigInteger max = java.math.BigInteger.valueOf(Long.MAX_VALUE); - if (integer.compareTo(min) < 0 || integer.compareTo(max) > 0) { - throw new IllegalArgumentException( - "Default is outside int64 range for property '" - + property.baseName + "'"); - } - if (integer.equals(min)) { - return "std::int64_t{-9223372036854775807LL - 1LL}"; - } - return "std::int64_t{" + integer + "LL}"; - } - try { - return "std::int32_t{" + integer.intValueExact() + "}"; - } catch (ArithmeticException exception) { - throw new IllegalArgumentException( - "Default is outside int32 range for property '" - + property.baseName + "'", exception); - } - } - String literal = value.toString(); - boolean hasFloatingMarker = literal.indexOf('.') >= 0 - || literal.indexOf('e') >= 0 || literal.indexOf('E') >= 0; - if (!hasFloatingMarker) { - literal += ".0"; - } - if (property.isFloat || "float".equals(property.dataType)) { - float narrowed = value.floatValue(); - if (!Float.isFinite(narrowed) - || (value.signum() != 0 && narrowed == 0.0f)) { - throw new IllegalArgumentException( - "Default is outside finite float range for property '" - + property.baseName + "'"); - } - return literal + "F"; - } - double narrowed = value.doubleValue(); - if (!Double.isFinite(narrowed) - || (value.signum() != 0 && narrowed == 0.0)) { - throw new IllegalArgumentException( - "Default is outside finite double range for property '" - + property.baseName + "'"); - } - return literal; - } - @Override - public String toDefaultValue(Schema p) { - if (ModelUtils.isStringSchema(p)) { - if (p.getDefault() != null) { - return "\"" + escapeCppStringContent(p.getDefault().toString()) + "\""; - } else { - return "\"\""; - } - } else if (ModelUtils.isBooleanSchema(p)) { - if (p.getDefault() != null) { - return p.getDefault().toString(); - } else { - return "false"; - } - } else if (ModelUtils.isDateSchema(p)) { - if (p.getDefault() != null) { - return "\"" + escapeCppStringContent(p.getDefault().toString()) + "\""; - } else { - return "\"\""; - } - } else if (ModelUtils.isDateTimeSchema(p)) { - if (p.getDefault() != null) { - return "\"" + escapeCppStringContent(p.getDefault().toString()) + "\""; - } else { - return "\"\""; - } - } else if (ModelUtils.isNumberSchema(p)) { - if (ModelUtils.isFloatSchema(p)) { // float - if (p.getDefault() != null) { - return p.getDefault().toString() + "f"; - } else { - return "0.0f"; - } - } else { // double - if (p.getDefault() != null) { - return p.getDefault().toString(); - } else { - return "0.0"; - } - } - } else if (ModelUtils.isIntegerSchema(p)) { - if (ModelUtils.isLongSchema(p)) { // long - if (p.getDefault() != null) { - return p.getDefault().toString() + "L"; - } else { - return "0L"; - } - } else { // integer - if (p.getDefault() != null) { - return p.getDefault().toString(); - } else { - return "0"; - } - } - } else if (ModelUtils.isByteArraySchema(p)) { - if (p.getDefault() != null) { - return "\"" + escapeCppStringContent(p.getDefault().toString()) + "\""; - } else { - return "\"\""; - } - } else if (ModelUtils.isMapSchema(p)) { - Schema inner = ModelUtils.getAdditionalProperties(p); - String innerType = inner == null ? "boost::json::value" : getTypeDeclaration(inner); - return "std::map()"; - } else if (ModelUtils.isArraySchema(p)) { - // Use getItems() directly to handle OpenAPI 3.1 JsonSchema - Schema inner = p.getItems(); - String innerType = inner != null ? getTypeDeclaration(inner) : "boost::json::value"; - return "std::vector<" + innerType + ">()"; - } else if (!StringUtils.isEmpty(p.get$ref())) { - String refName = toModelName(ModelUtils.getSimpleRef(p.get$ref())); - if (variantModels.contains(refName)) { - return refName + "()"; - } - return "std::make_shared<" + refName + ">()"; - } else if (ModelUtils.isNullType(p)) { - return "nullptr"; - } else if (ModelUtils.isAnyType(p) || ModelUtils.isFreeFormObject(p, openAPI)) { - return "boost::json::value()"; - } - - return "nullptr"; - } - - @Override - public String toDefaultValue(CodegenProperty codegenProperty, Schema schema) { - if (codegenProperty != null) { - if (codegenProperty.dataType != null && codegenProperty.dataType.startsWith("std::shared_ptr<")) { - return "nullptr"; - } - if ("boost::json::value".equals(codegenProperty.dataType)) { - return "boost::json::value()"; - } - Schema referenceSchema = Oas31CompositionLowering.referenceSchemaOf(schema); - if (referenceSchema != null && referenceSchema != schema - && schema.getDefault() == null) { - Schema referencedTarget = ModelUtils.getReferencedSchema(openAPI, referenceSchema); - if (referencedTarget != null && referencedTarget != referenceSchema - && codegenProperty.dataType != null - && codegenProperty.dataType.equals(getTypeDeclaration(referencedTarget))) { - return toDefaultValue(referencedTarget); - } - } - } - return super.toDefaultValue(codegenProperty, schema); - } - - @Override - public void setParameterEncodingValues(CodegenParameter codegenParameter, MediaType mediaType) { - super.setParameterEncodingValues(codegenParameter, mediaType); - // Detect Encoding Object headers that cannot be propagated to - // multipart parts. When an Encoding Object specifies headers, - // emit a diagnostic instead of silently dropping them. - if (codegenParameter.isFormParam && mediaType != null - && mediaType.getEncoding() != null) { - io.swagger.v3.oas.models.media.Encoding encoding = - mediaType.getEncoding().get(codegenParameter.baseName); - if (encoding != null && encoding.getHeaders() != null - && !encoding.getHeaders().isEmpty()) { - LOGGER.warn("Encoding Object on form parameter '{}' specifies {} header(s) " - + "that are not propagated to the multipart part. " - + "Generated code uses only the contentType field. " - + "Header keys: {}", - codegenParameter.baseName, - encoding.getHeaders().size(), - encoding.getHeaders().keySet()); - } - } - } - - @Override - public void postProcessParameter(CodegenParameter parameter) { - super.postProcessParameter(parameter); - - boolean isPrimitiveType = parameter.isPrimitiveType == Boolean.TRUE; - boolean isArray = parameter.isArray == Boolean.TRUE; - boolean isMap = parameter.isMap == Boolean.TRUE; - boolean isString = parameter.isString == Boolean.TRUE; - parameter.vendorExtensions.put(X_CODEGEN_IS_RAW_BODY, - isPrimitiveType || isString || parameter.isByteArray || parameter.isBinary - || "std::string".equals(parameter.dataType)); - - if (!isPrimitiveType && !isArray && !isMap && !isString && !parameter.dataType.startsWith("std::shared_ptr") - && !"boost::json::value".equals(parameter.dataType) - && !"std::nullptr_t".equals(parameter.dataType) - && !parameter.dataType.startsWith("std::variant<") - && !parameter.dataType.startsWith("std::optional<") - && !"std::monostate".equals(parameter.dataType)) { - // Wrap non-primitive types in shared_ptr, unless: - // - The type is a variant/optional model (value semantics) - // - The type is a known variant model name from composed schemas - if (!variantModels.contains(parameter.dataType)) { - parameter.dataType = "std::shared_ptr<" + parameter.dataType + ">"; - parameter.defaultValue = "std::make_shared<" + parameter.dataType + ">()"; - } - } - - // Post-hoc unwrap: if the type ended up as std::shared_ptr, - // strip the shared_ptr wrapper (value semantics for variant types). - if (parameter.dataType != null && parameter.dataType.startsWith("std::shared_ptr<") - && parameter.dataType.endsWith(">")) { - String innerType = parameter.dataType.substring(16, parameter.dataType.length() - 1); - if (variantModels.contains(innerType)) { - parameter.dataType = innerType; - parameter.defaultValue = null; - } - } - - // For form params, validate that encoding style/explode combinations - // are representable in multipart/form-data. Only form-style is supported - // for multipart (space-delimited, pipe-delimited, and deep-object styles - // are not representable). Fail closed with a targeted diagnostic. - if (parameter.isFormParam) { - if (Boolean.TRUE.equals(parameter.isSpaceDelimited)) { - throw new UnsupportedSchemaAssertionException( - parameter.baseName, - "encoding-style"); - } - if (Boolean.TRUE.equals(parameter.isPipeDelimited)) { - throw new UnsupportedSchemaAssertionException( - parameter.baseName, - "encoding-style"); - } - if (Boolean.TRUE.equals(parameter.isDeepObject)) { - throw new UnsupportedSchemaAssertionException( - parameter.baseName, - "encoding-style"); - } - } - - // Tag variant form params for branch-aware multipart serialization. - // When a form parameter's type is a variant, the template uses - // addVariantFormParameter to dispatch binary branches as file parts - // and object branches as JSON parts. - // Only set for actual std::variant types, not for models that alias - // to primitive types (e.g., VideoModel → std::string), which would - // cause instantiation of addVariantFormParameter and - // an invalid std::visit call on a non-variant type. - boolean isVariantParam = false; - if (parameter.isFormParam && parameter.dataType != null) { - if (parameter.dataType.startsWith("std::variant<")) { - isVariantParam = true; - } else if (variantModels.contains(parameter.dataType)) { - String resolved = resolveThroughAliases(parameter.dataType); - if (resolved != null && resolved.startsWith("std::variant<")) { - isVariantParam = true; - } - } - } - if (isVariantParam) { - parameter.vendorExtensions.put("x-codegen-is-variant-form-param", true); - } - } /** * Optional - OpenAPI type conversion. This is used to map OpenAPI types in @@ -1693,86 +473,10 @@ public void postProcessParameter(CodegenParameter parameter) { * * @return a string value of the type or complex model for this property */ - @Override - public String getSchemaType(Schema p) { - // Non-standard format (NOT core OAS vocabulary). Documented generator - // convenience for corpora that use Unix-epoch integer timestamps. - // Disable by not using format: unixtime in the source document. - if (p != null && "unixtime".equals(p.getFormat())) { - return "int64_t"; - } - String openAPIType = super.getSchemaType(p); - String type = null; - String modelName; - if (typeMapping.containsKey(openAPIType)) { - type = typeMapping.get(openAPIType); - } else { - type = openAPIType; - } - modelName = toModelName(type); - return modelName; - } - @Override - public void updateCodegenPropertyEnum(CodegenProperty var) { - // Remove prefix added by DefaultCodegen - String originalDefaultValue = var.defaultValue; - super.updateCodegenPropertyEnum(var); - var.defaultValue = originalDefaultValue; - } - @Override - public Map updateAllModels(Map objs) { - Map updatedModels = super.updateAllModels(objs); - refreshComponentSchemaIds(openAPI); - for (Map.Entry entry : updatedModels.entrySet()) { - for (ModelMap modelMap : entry.getValue().getModels()) { - CodegenModel model = modelMap.getModel(); - String schemaName = model.schemaName != null ? model.schemaName : entry.getKey(); - model.vendorExtensions.put("x-cpp-component-schema-id", - componentSchemaId(schemaName, componentSchemaIdsByName)); - } - } - return updatedModels; - } - - private void refreshComponentSchemaIds(OpenAPI openApi) { - if (openApi == null || openApi.getComponents() == null - || openApi.getComponents().getSchemas() == null) { - componentSchemaIdsByName = Collections.emptyMap(); - return; - } - componentSchemaIdsByName = componentSchemaIds( - openApi.getComponents().getSchemas().keySet()); - } - @Override - public Map postProcessSupportingFileData(Map objs) { - Map processed = super.postProcessSupportingFileData(objs); - if (!validateOnDecode) { - return processed; - } - // Model processing can replace inline branch schema objects after the - // initial recovery pass; refresh the emitted graph from the raw spec. - Oas31RawSpecRecovery.recoverPristineLiterals(openAPI, getInputSpec()); - refreshComponentSchemaIds(openAPI); - Oas31SchemaIrEmitter emitter = new Oas31SchemaIrEmitter( - openAPI, compositionDescriptors, additionalProperties(), componentSchemaIdsByName); - Map produced = emitter.produce(processed); - supportingFiles.removeIf(file -> { - String destination = file.getDestinationFilename(); - return destination.startsWith("schema_ir.generated.chunk") - && destination.endsWith(".cpp"); - }); - int chunkCount = ((Number) produced.get("oas31SchemaIrChunkCount")).intValue(); - for (int chunk = 0; chunk < chunkCount; chunk++) { - supportingFiles.add(new SupportingFile( - Oas31SchemaIrEmitter.schemaIrChunkTemplate(chunk), - "model", Oas31SchemaIrEmitter.schemaIrChunkFilename(chunk))); - } - return produced; - } } diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/CppBoostBeastModelCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/CppBoostBeastModelCodegen.java index 7e24ce105452..65492ce5fadb 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/CppBoostBeastModelCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/CppBoostBeastModelCodegen.java @@ -99,6 +99,1298 @@ protected static String idOf(io.swagger.v3.oas.models.Operation op) { protected Map> operationCallbacks = new HashMap<>(); protected Map> operationLinks = new HashMap<>(); + protected CppBoostBeastModelCodegen() { + // Shared model/validation templates live in cpp-boost-beast-common so + // client and server generators resolve them from a single source. + additionalEmbeddedTemplateDirs = new ArrayList<>(List.of("cpp-boost-beast-common")); + } + public static final String EXPORT_MACRO = "exportMacro"; + /** Policy for format metadata in composition branch matching. + * Formats remain annotations and never affect branch match counts. */ + protected String formatAssertionPolicy = "annotation"; + + /** Value type for the formatAssertion option. */ + protected static final String FORMAT_ASSERTION_POLICY_ANNOTATION = "annotation"; + /** Controls composition-branch validation during model decoding. */ + protected boolean validateOnDecode = true; + /** Retains undeclared JSON object members in generated object models. */ + protected boolean preserveAdditionalProperties = false; + private static final String X_CODEGEN_IS_RAW_BODY = "x-codegen-is-raw-body"; + private static final String X_CODEGEN_IS_OPTIONAL_QUERY_PARAMETER = + "x-codegen-is-optional-query-parameter"; + // Authoritative parameter serialization facts stamped by codegenParameterStyled(). + private static final String X_CODEGEN_PARAM_STYLE = "x-codegen-param-style"; + private static final String X_CODEGEN_PARAM_EXPLODE = "x-codegen-param-explode"; + private static final String X_CODEGEN_PARAM_ALLOW_RESERVED = + "x-codegen-param-allow-reserved"; + private static final String X_CODEGEN_PARAM_ALLOW_EMPTY_VALUE = + "x-codegen-param-allow-empty-value"; + protected Map componentSchemaIdsByName = Collections.emptyMap(); + /** Starts an isolated state set for one generator invocation. */ + protected void beginGeneration(OpenAPI openApi) { + sourceOpenApi = openApi; + variantModels = new HashSet<>(); + resolvedAliasTypes = new HashMap<>(); + composedKeywordsByModel = new HashMap<>(); + compositionDescriptors = new LinkedHashMap<>(); + compositionDescriptorSets = new LinkedHashMap<>(); + webhookPreservation = new ArrayList<>(); + operationCallbacks = new HashMap<>(); + operationLinks = new HashMap<>(); + allOfIntersections = new LinkedHashMap<>(); + refreshComponentSchemaIds(openApi); + + } + + /** + * swagger-parser materializes the implicit root server as {@code /}, which + * is indistinguishable from a source-level {@code servers: [{url: /}]} in + * the model. Consult the raw document before server-precedence assembly. + */ + protected boolean detectExplicitRootServers() { + String inputSpec = getInputSpec(); + if (inputSpec == null || inputSpec.isEmpty()) { + // Programmatic OpenAPI instances have no parser-injected source. + return true; + } + try { + JsonNode document = Oas31RawSpecRecovery.readRawDocument(inputSpec); + return document != null && document.isObject() && document.has("servers"); + } catch (Exception exception) { + throw new IllegalStateException( + "Unable to inspect the source OpenAPI document for root servers", exception); + } + } + + /** + * Returns the composition descriptor for the given schema name, or null + * if the schema is not composed or was not indexed. + */ + public CompositionDescriptor getCompositionDescriptor(String schemaName) { + return compositionDescriptors.get(schemaName); + } + + /** + * Returns an unmodifiable view of the full composition descriptor index. + */ + public Map getCompositionDescriptors() { + return Collections.unmodifiableMap(compositionDescriptors); + } + + /** + * Returns every composition descriptor present on a schema, in keyword + * order: oneOf, anyOf, then allOf. + */ + public List getCompositionDescriptorsForSchema(String schemaName) { + return compositionDescriptorSets.getOrDefault(schemaName, Collections.emptyList()); + } + /** Pinned OAS 3.1 Schema dialect (spec.openapis.org/oas/3.1/dialect/2024-11-10). */ + public static final String OAS_31_DIALECT = + "https://spec.openapis.org/oas/3.1/dialect/2024-11-10"; + + /** OAS alias accepted only as the identifier for the same pinned revision. */ + public static final String OAS_31_DIALECT_BASE_ALIAS = + "https://spec.openapis.org/oas/3.1/dialect/base"; + + /** Plain JSON Schema Draft 2020-12 core identifier (non-OAS dialect). */ + public static final String DRAFT_2020_12 = + "https://json-schema.org/draft/2020-12/schema"; + + /** Classified effective schema dialect for an OpenAPI document. */ + public enum OasDialect { + /** OAS 3.1 pinned dialect (or its base alias). */ + OAS_31, + /** Plain JSON Schema Draft 2020-12 (not OAS-wrapped). */ + DRAFT_2020_12_REC, + /** A dialect identifier not recognized by this program. */ + UNRECOGNIZED, + /** No dialect declared (OAS 3.1 default applies for OAS 3.1 documents). */ + UNSPECIFIED + } + + /** + * Dialect resolution, normative-structure checks, and the exhaustive + * keyword-occurrence scanner live in {@link Oas31KeywordScanner}; + * the delegates below keep this generator's public API stable for + * tests and templates. + */ + public static OasDialect resolveEffectiveDialect(String jsonSchemaDialect, String rootSchema) { + return Oas31KeywordScanner.resolveEffectiveDialect(jsonSchemaDialect, rootSchema); + } + + /** Resolve the effective dialect of an OpenAPI document from its declared knobs. */ + public static OasDialect resolveDocumentDialect(OpenAPI openAPI) { + return Oas31KeywordScanner.resolveDocumentDialect(openAPI); + } + + /** OAS 3 structural normative checks (see {@link Oas31KeywordScanner}). */ + public List validateNormativeOas3Structure(OpenAPI openAPI) { + return Oas31KeywordScanner.validateNormativeOas3Structure(openAPI); + } + + /** Dialect/metaschema policy gate (see {@link Oas31KeywordScanner}). */ + public List validateDialectPolicy(OpenAPI openAPI) { + return Oas31KeywordScanner.validateDialectPolicy(openAPI); + } + + + /** + * Exhaustive schema-valued-position scanner (see {@link Oas31KeywordScanner}). + */ + public Oas31KeywordScanner.KeywordOccurrenceLedger scanSchemaKeywordOccurrences( + OpenAPI openAPI) { + return Oas31KeywordScanner.scanSchemaKeywordOccurrences(openAPI); + } + + + /** Set of fail-closed required-vocabulary keywords for this document. */ + public Set failClosedKeywords(OpenAPI openAPI) { + return Oas31KeywordScanner.failClosedKeywords(openAPI); + } + /** + * Generator-specific normalizer that preserves composition structure + * (branch cardinality, null multiplicity, original keyword) for all + * oneOf/anyOf/anyOf-string-enum schemas. Set-equivalent simplification + * happens later in the generator's semantic analyzer (processComposedModel), + * never in the pre-descriptor normalizer. + */ + public static class CppBoostBeastOpenAPINormalizer extends OpenAPINormalizer { + public CppBoostBeastOpenAPINormalizer(OpenAPI openAPI, Map inputRules) { + super(openAPI, inputRules); + } + + @Override + protected Schema processSimplifyAnyOf(Schema schema) { + if (schema.getAnyOf() != null && !schema.getAnyOf().isEmpty()) { + return schema; + } + return super.processSimplifyAnyOf(schema); + } + + @Override + protected Schema processSimplifyOneOf(Schema schema) { + if (schema.getOneOf() != null && !schema.getOneOf().isEmpty()) { + return schema; + } + return super.processSimplifyOneOf(schema); + } + + @Override + protected Schema processSimplifyAnyOfStringAndEnumString(Schema schema) { + if (schema.getAnyOf() != null && !schema.getAnyOf().isEmpty()) { + return schema; + } + return super.processSimplifyAnyOfStringAndEnumString(schema); + } + + @Override + protected Schema processSimplifyOneOfEnum(Schema schema) { + if (schema.getOneOf() != null && !schema.getOneOf().isEmpty()) { + return schema; + } + return super.processSimplifyOneOfEnum(schema); + } + + @Override + protected Schema processSimplifyAnyOfEnum(Schema schema) { + if (schema.getAnyOf() != null && !schema.getAnyOf().isEmpty()) { + return schema; + } + return super.processSimplifyAnyOfEnum(schema); + } + } + @Override + protected ImmutableMap.Builder addMustacheLambdas() { + return super.addMustacheLambdas() + .put("cppStringLiteral", (fragment, writer) -> writer.write( + escapeCppStringContent( + StringEscapeUtils.unescapeHtml4(fragment.execute())))); + } + + @Override + public String escapeText(String input) { + return input == null ? null : escapeCppStringContent(input); + } + @Override + public String getterAndSetterCapitalize(String name) { + if (name == null || name.length() == 0) { + return name; + } + + name = toVarName(name); + + if (name.startsWith("_")) { + return "_" + camelize(name); + } + + return camelize(name); + } + protected static boolean isSchemaValidationSupportingFile(SupportingFile file) { + String destination = file.getDestinationFilename(); + return "Oas31SchemaRegistry.h".equals(destination) + || "schema_ir.generated.cpp".equals(destination) + || (destination.startsWith("schema_ir.generated.chunk") + && destination.endsWith(".cpp")); + } + /** + * Location to write model files. You can use the modelPackage() as defined + * when the class is instantiated + */ + @Override + public String modelFileFolder() { + return (outputFolder + "/model").replace("/", File.separator); + } + + /** + * Location to write api files. You can use the apiPackage() as defined when + * the class is instantiated + */ + @Override + public String apiFileFolder() { + return (outputFolder + "/api").replace("/", File.separator); + } + + @Override + public String toModelImport(String name) { + if (importMapping.containsKey(name)) { + return importMapping.get(name); + } else { + return "#include \"" + name + ".h\""; + } + } + + @Override + public CodegenModel fromModel(String name, Schema model) { + // Flatten allOf into a synthetic schema with intersected properties and + // unioned required names. Clearing allOf gives every property direct owned + // storage rather than generated inheritance. + Schema modelArg = model; + if (model != null && model.getAllOf() != null && !model.getAllOf().isEmpty()) { + AllOfIntersection intersection = allOfIntersections.get( + toModelName(name)); + if (intersection != null) { + // Check for unsatisfiable required properties / scalar conflicts + if (!intersection.isSatisfiable()) { + throw new AllOfRequiredUnsatisfiableException( + name, intersection.getUnsatisfiableReason()); + } + + Schema synthetic = Oas31CompositionLowering.buildSyntheticAllOfSchema( + name, intersection); + // Copy top-level attributes from original model + if (model.getDiscriminator() != null) { + synthetic.setDiscriminator(model.getDiscriminator()); + } + if (Boolean.TRUE.equals(model.getNullable())) { + synthetic.setNullable(true); + } + if (model.getDescription() != null) { + synthetic.setDescription(model.getDescription()); + } + if (model.getFormat() != null && intersection.getRootScalarType() != null) { + synthetic.setFormat(model.getFormat()); + } + // Optional impossible properties retain their API surface but + // reject any JSON object in which they are present. + if (!intersection.getOptionalImpossibleProperties().isEmpty()) { + Map ext = synthetic.getExtensions(); + if (ext == null) { + ext = new LinkedHashMap<>(); + synthetic.setExtensions(ext); + } + ext.put("x-cpp-optional-impossible-properties", + new ArrayList<>(intersection.getOptionalImpossibleProperties())); + } + // Flat: allOf = null so super.fromModel sees no parent + synthetic.setAllOf(null); + modelArg = synthetic; + } + } + + // Pre-check: The OpenAPI 3.1 parser converts anyOf [T, null] into + // {type: T, nullable: true} or {$ref: X, nullable: true}, consuming + // the anyOf list. Detect these nullable schemas and produce the + // correct std::optional type. + // + // For $ref schemas (normalised anyOf/oneOf [T, null] where T was a + // $ref), getTypeDeclaration resolves the target and returns the + // correct C++ type. For arrays, getTypeDeclaration returns the + // container type (e.g. std::vector<...>) without optional wrapping, + // so we wrap it here. Inline object schemas (type=object, no $ref) + // are full class models — they stay out of the alias precomputation + // because getTypeDeclaration would return the raw OAS type name + // "object" instead of the model name. They are handled separately + // below via variant model registration. + boolean isNullableSchema = model != null + && Boolean.TRUE.equals(model.getNullable()) + && (model.get$ref() != null + || (model.getType() != null && !"object".equals(model.getType()))); + String preComputedNullUnionType = null; + if (isNullableSchema) { + // Resolve the type to its C++ type and wrap in std::optional + String innerType = getTypeDeclaration(model); + // getTypeDeclaration already returns std::optional for nullable. + // Use it directly if it starts with std::optional<. + if (innerType.startsWith("std::optional<")) { + preComputedNullUnionType = innerType; + } else { + preComputedNullUnionType = "std::optional<" + innerType + ">"; + } + } else if (model != null) { + // Also try the anyOf/oneOf path for cases where the parser + // preserved the composed schema structure. + preComputedNullUnionType = detectNullUnion(model, name); + } + + CodegenModel codegenModel = super.fromModel(name, modelArg); + if (codegenModel == null) { + return null; + } + + codegenModel.vendorExtensions.put( + "x-cpp-component-schema-id", + componentSchemaId(name, componentSchemaIdsByName)); + + // Post-check: Apply the pre-computed null union type if the default + // pipeline consumed the composed schemas. + if (preComputedNullUnionType != null) { + codegenModel.dataType = preComputedNullUnionType; + codegenModel.vendorExtensions.put("x-cpp-type", preComputedNullUnionType); + codegenModel.vendorExtensions.put("x-cpp-composed-keyword", + model.getAnyOf() != null ? "anyOf" : "oneOf"); + codegenModel.vendorExtensions.put("x-cpp-is-alias", true); + codegenModel.vendorExtensions.put("x-cpp-is-optional", true); + // Force a model header/source so Gate A inventory and $ref users get + // `using NullableString = std::optional;`. DefaultCodegen + // marks plain nullable primitives as isAlias and skips file emission. + codegenModel.isAlias = false; + resolvedAliasTypes.put(name, preComputedNullUnionType); + variantModels.add(name); + } + + // Post-check: Inline nullable object schemas (type=object, nullable=true, + // no $ref) are full class models with properties — they cannot use the + // alias path. Register them as variant models so $ref references use value + // semantics (std::shared_ptr → NullableObject) and tag + // the model as optional for correct null-value representation. + if (model != null && model.get$ref() == null + && "object".equals(model.getType()) + && Boolean.TRUE.equals(model.getNullable())) { + variantModels.add(name); + codegenModel.vendorExtensions.put("x-cpp-is-optional", true); + } + + Set oldImports = codegenModel.imports; + codegenModel.imports = new HashSet<>(); + for (String imp : oldImports) { + String newImp = toModelImport(imp); + if (!newImp.isEmpty()) { + codegenModel.imports.add(newImp); + } + } + // Every model header declares vector conversion helpers. + codegenModel.imports.add("#include "); + if (preserveAdditionalProperties) { + codegenModel.imports.add("#include "); + codegenModel.imports.add("#include "); + codegenModel.imports.add("#include "); + reserveExtraJsonPropertyIdentifiers(codegenModel); + } + + // Fixed-const properties: OAS 3.1 `const`, single-value `enum`, or optional + // vendor extension `x-stainless-const`. Portable path is OAS `const` / single enum — + // vendor extensions are never required for correct encode/decode. + if (codegenModel.vars != null) { + Map allProps = new LinkedHashMap<>(); + if (model.getProperties() != null) { + allProps.putAll(model.getProperties()); + } + if (model.getAllOf() != null && openAPI != null) { + for (Object parentObj : model.getAllOf()) { + if (parentObj instanceof Schema) { + Schema parentSchema = ModelUtils.getReferencedSchema( + openAPI, (Schema) parentObj); + if (parentSchema != null && parentSchema.getProperties() != null) { + allProps.putAll(parentSchema.getProperties()); + } + } + } + } + for (CodegenProperty var : codegenModel.vars) { + Object rawProp = allProps.get(var.baseName); + if (!(rawProp instanceof Schema)) { + continue; + } + Schema varSchema = (Schema) rawProp; + boolean hasOasConst = varSchema.getConst() != null; + boolean hasSingleValueEnum = varSchema.getEnum() != null + && varSchema.getEnum().size() == 1; + boolean hasStainlessConst = varSchema.getExtensions() != null + && Boolean.TRUE.equals(varSchema.getExtensions().get("x-stainless-const")); + if (!(hasOasConst || hasSingleValueEnum || hasStainlessConst)) { + continue; + } + String constRawValue = null; + if (varSchema.getConst() != null) { + constRawValue = varSchema.getConst().toString(); + } else if (varSchema.getEnum() != null && !varSchema.getEnum().isEmpty()) { + constRawValue = varSchema.getEnum().get(0).toString(); + } + if (constRawValue == null && var.example != null) { + constRawValue = var.example; + } + if (constRawValue == null) { + constRawValue = "std::string".equals(var.dataType) ? "" : "0"; + } + String inlineValue; + boolean isStringConst = "std::string".equals(var.dataType) + || "std::optional".equals(var.dataType) + || (var.isString && !var.isInteger && !var.isLong && !var.isNumber + && !var.isBoolean); + if ("std::optional".equals(var.dataType)) { + inlineValue = "std::optional{\"" + + escapeCppStringContent(constRawValue) + "\"}"; + } else if (isStringConst || "std::string".equals(var.dataType)) { + inlineValue = "\"" + escapeCppStringContent(constRawValue) + "\""; + } else { + inlineValue = constRawValue; + } + // Neutral OAS-first flag used by templates. + var.vendorExtensions.put("x-cpp-const", true); + var.vendorExtensions.put("x-cpp-const-value", constRawValue); + var.vendorExtensions.put("x-cpp-const-inline-value", inlineValue); + // Mustache is truthy on key presence — only set when string-typed. + if (isStringConst || "std::string".equals(var.dataType) + || "std::optional".equals(var.dataType)) { + var.vendorExtensions.put("x-cpp-const-is-string", true); + } else if (var.isBoolean || "bool".equals(var.dataType) + || "std::optional".equals(var.dataType)) { + var.vendorExtensions.put("x-cpp-const-is-boolean", true); + } + // Keep stainless keys as aliases so older template forks still work. + var.vendorExtensions.put("x-stainless-const", true); + var.vendorExtensions.put("x-stainless-const-value", constRawValue); + var.vendorExtensions.put("x-stainless-const-inline-value", inlineValue); + } + } + + addContainerPropertyNames(codegenModel.vars); + return codegenModel; + } + + @Override + public CodegenParameter fromParameter(Parameter parameter, Set imports) { + CodegenParameter codegenParameter = super.fromParameter(parameter, imports); + // Preserve serialization facts for every parameter location. + codegenParameterStyled(parameter, codegenParameter); + if (!codegenParameter.isQueryParam) { + return codegenParameter; + } + + if (!codegenParameter.required) { + codegenParameter.vendorExtensions.put(X_CODEGEN_IS_OPTIONAL_QUERY_PARAMETER, true); + } + return codegenParameter; + } + + /** + * Records the OAS 3.1 serialization facts consumed by the C++ wire layer. + * Style defaults to form for query/cookie and simple for path/header. Explode + * defaults to true only for form. allowReserved is surfaced consistently; + * allowEmptyValue applies only to form-style query parameters. + */ + protected void codegenParameterStyled(Parameter parameter, + CodegenParameter codegenParameter) { + String style = parameter.getStyle() == null + ? null : parameter.getStyle().toString(); + if (style == null) { + if (codegenParameter.isQueryParam || codegenParameter.isCookieParam) { + style = "form"; + } else { + style = "simple"; // path, header + } + } + Boolean explode = Boolean.TRUE.equals(parameter.getExplode()); + if (parameter.getExplode() == null) { + explode = "form".equals(style); // spec default + } + codegenParameter.vendorExtensions.put(X_CODEGEN_PARAM_STYLE, style); + codegenParameter.vendorExtensions.put(X_CODEGEN_PARAM_EXPLODE, explode); + codegenParameter.vendorExtensions.put(X_CODEGEN_PARAM_ALLOW_RESERVED, + Boolean.TRUE.equals(parameter.getAllowReserved())); + codegenParameter.vendorExtensions.put(X_CODEGEN_PARAM_ALLOW_EMPTY_VALUE, + Boolean.TRUE.equals(parameter.getAllowEmptyValue())); + } + + protected String queryCollectionDelimiter(Parameter.StyleEnum style) { + if (style == Parameter.StyleEnum.SPACEDELIMITED) { + return "%20"; + } + if (style == Parameter.StyleEnum.PIPEDELIMITED) { + return "%7C"; + } + return ","; + } + + protected void addContainerPropertyNames(List properties) { + for (CodegenProperty property : properties) { + CodegenProperty item = property.items; + while (item != null) { + item.vendorExtensions.put("x-container-property-name", property.name); + item = item.items; + } + } + } + + protected void reserveExtraJsonPropertyIdentifiers(CodegenModel codegenModel) { + Set propertyAccessors = new HashSet<>(); + Set propertyMembers = new HashSet<>(); + if (codegenModel.allVars != null) { + for (CodegenProperty property : codegenModel.allVars) { + if (property.getter != null) { + propertyAccessors.add(property.getter); + } + if (property.setter != null) { + propertyAccessors.add(property.setter); + } + if (property.name != null) { + propertyMembers.add("m_" + property.name); + } + } + } + + int maxSuffix = propertyAccessors.size() + propertyMembers.size() + 2; + for (int suffix = 1; suffix <= maxSuffix; suffix++) { + String suffixText = suffix == 1 ? "" : Integer.toString(suffix); + String getter = "getExtraJsonProperties" + suffixText; + String setter = "setExtraJsonProperties" + suffixText; + String member = "m_extraJsonProperties" + suffixText; + if (propertyAccessors.contains(getter) || propertyAccessors.contains(setter) + || propertyMembers.contains(member)) { + continue; + } + codegenModel.vendorExtensions.put("x-cpp-extra-json-properties-getter", getter); + codegenModel.vendorExtensions.put("x-cpp-extra-json-properties-setter", setter); + codegenModel.vendorExtensions.put("x-cpp-extra-json-properties-member", member); + return; + } + throw new IllegalStateException("Unable to reserve C++ extra JSON property identifiers"); + } + + @Override + public String toModelFilename(String name) { + return toModelName(name); + } + + @Override + public String toApiFilename(String name) { + return toApiName(name); + } + @Override + public String getTypeDeclaration(Schema p) { + // Handle inline oneOf/anyOf composed schemas (apply lowering rules directly) + if (ModelUtils.isComposedSchema(p) && (p.getOneOf() != null || p.getAnyOf() != null)) { + return lowerInlineComposedSchema(p); + } + + String openAPIType = getSchemaType(p); + + if (ModelUtils.isArraySchema(p)) { + // Use getItems() directly to handle both OpenAPI 3.0 and 3.1 + Schema inner = p.getItems(); + String arrayType; + if (inner != null) { + arrayType = getSchemaType(p) + "<" + getTypeDeclaration(inner) + ">"; + } else { + arrayType = "std::vector"; + } + // Nullable arrays must be wrapped in std::optional so null JSON + // values are representable. The array branch returns before the + // nullable fallback checks at the end of this method. + if (ModelUtils.isNullable(p)) { + return "std::optional<" + arrayType + ">"; + } + return arrayType; + } else if (ModelUtils.isMapSchema(p)) { + Schema inner = ModelUtils.getAdditionalProperties(p); + String innerType = inner == null ? "boost::json::value" : getTypeDeclaration(inner); + String mapType = getSchemaType(p) + ""; + // Nullable maps must be wrapped in std::optional so null JSON + // values are representable. The map branch returns before the + // nullable fallback checks at the end of this method. + if (ModelUtils.isNullable(p)) { + return "std::optional<" + mapType + ">"; + } + return mapType; + } else if (ModelUtils.isByteArraySchema(p)) { + return "std::string"; + } else if (ModelUtils.isStringSchema(p) + || ModelUtils.isDateSchema(p) + || ModelUtils.isDateTimeSchema(p) || ModelUtils.isFileSchema(p) + || languageSpecificPrimitives.contains(openAPIType) + || typeMapping.containsKey(openAPIType) + || typeMapping.values().contains(openAPIType)) { + // Resolve through type mapping for scalar allOf: composed schemas + // return OAS raw types (e.g. "string") or mapped types (e.g. + // "std::string") depending on branch resolution path. + // Re-map if the value is already in the type mapping values. + String resolved = typeMapping.containsKey(openAPIType) + ? typeMapping.get(openAPIType) + : toModelName(openAPIType); + // OAS 3.0 nullable: true → std::optional + if (ModelUtils.isNullable(p)) { + return "std::optional<" + resolved + ">"; + } + return resolved; + } else if (ModelUtils.isNullType(p)) { + // Handle OpenAPI 3.1 null type + return "std::nullptr_t"; + } else if (ModelUtils.isAnyType(p) || ModelUtils.isFreeFormObject(p, openAPI)) { + return "boost::json::value"; + } + + // OAS 3.0 nullable: true → std::optional + if (ModelUtils.isNullable(p)) { + return "std::optional<" + openAPIType + ">"; + } + + // Variant models use value semantics (no shared_ptr wrapping) + if (variantModels.contains(openAPIType)) { + return openAPIType; + } + + // Object references use shared ownership because circular-reference facts + // are unavailable when this declaration is computed. Variant aliases are + // handled above as value types. + return "std::shared_ptr<" + openAPIType + ">"; + } + + /** + * Resolves an inline oneOf/anyOf schema to its lowered C++ type by computing + * branch types and applying the same ordered lowering rules as model-level types. + */ + protected String lowerInlineComposedSchema(Schema p) { + String composedKeyword; + List children; + if (p.getOneOf() != null) { + children = p.getOneOf(); + composedKeyword = "oneOf"; + } else { + children = p.getAnyOf(); + composedKeyword = "anyOf"; + } + + List composedBranches = new ArrayList<>(); + for (Schema child : children) { + // Compute the branch type using the full type declaration pipeline + // but strip shared_ptr for variant members (value semantics). + String childType = stripSharedPtr(getTypeDeclaration(child)); + // Resolve $ref targets that are aliased to primitive types at the + // declaration point, before resolvedAliasTypes is available (it is + // populated during postProcessModels, which runs later). This handles + // inline schemas like CreateAssistantRequest_model = oneOf [string, + // $ref AssistantSupportedModels] where the target is anyOf [string, + // string-enum] → std::string, collapsing to just std::string. + Schema resolvedChild = child; + if (!childType.startsWith("std::") && !childType.startsWith("boost::") + && !childType.startsWith("std::shared_ptr<")) { + Schema resolvedTarget = child.get$ref() != null && openAPI != null + ? ModelUtils.getReferencedSchema(openAPI, child) : null; + if (resolvedTarget != null) { + resolvedChild = resolvedTarget; + String resolved = getTypeDeclaration(resolvedTarget); + String stripped = stripSharedPtr(resolved); + if (!stripped.equals(childType)) { + childType = stripped; + } + } + } + boolean isEnum = resolvedChild.getEnum() != null && !resolvedChild.getEnum().isEmpty(); + boolean isStringLike = ModelUtils.isStringSchema(resolvedChild) + || "std::string".equals(childType); + composedBranches.add(new ComposedBranch(childType, isEnum, isStringLike, -1)); + } + + // Deduplicate inside lowerComposedTypes so oneOf branch identity survives + // identical lowered C++ types. + return Oas31CompositionLowering.lowerComposedTypes( + composedBranches, composedKeyword, null, LOGGER::warn); + } + @Override + public CodegenProperty fromProperty(String name, Schema p, boolean required, + boolean schemaIsFromAdditionalProperties) { + CodegenProperty prop = super.fromProperty(name, p, required, schemaIsFromAdditionalProperties); + if (prop == null || p == null) { + return prop; + } + // Tag inline composed properties so templates can honor oneOf vs anyOf + // decode rules (exactly-one vs first-match) instead of always using + // the generic JsonValueConverter exactly-one path. + if (p.getOneOf() != null && !p.getOneOf().isEmpty()) { + prop.vendorExtensions.put("x-cpp-composed-keyword", "oneOf"); + prop.vendorExtensions.put("x-cpp-is-oneof", true); + } else if (p.getAnyOf() != null && !p.getAnyOf().isEmpty()) { + prop.vendorExtensions.put("x-cpp-composed-keyword", "anyOf"); + prop.vendorExtensions.put("x-cpp-is-anyof", true); + } + if (Oas31RawSpecRecovery.hasExplicitDefault(p)) { + String defaultValue = explicitScalarDefaultValue(prop, p); + if (defaultValue != null) { + prop.defaultValue = defaultValue; + prop.vendorExtensions.put("x-cpp-has-explicit-default", true); + prop.vendorExtensions.put(X_CPP_EXPLICIT_DEFAULT_SCALAR, defaultValue); + prop.vendorExtensions.put("x-cpp-default-is-null", + "null".equals(Oas31RawSpecRecovery.defaultJsonOf(p))); + } + } + return prop; + } + + protected String explicitScalarDefaultValue(CodegenProperty property, Schema schema) { + String json = Oas31RawSpecRecovery.defaultJsonOf(schema); + if (json == null) { + return null; + } + + com.fasterxml.jackson.databind.JsonNode value; + try { + value = io.swagger.v3.core.util.Json31.mapper().readTree(json); + } catch (com.fasterxml.jackson.core.JsonProcessingException exception) { + throw new IllegalArgumentException( + "Unable to parse default for property '" + property.baseName + "'", exception); + } + if (value == null || !value.isValueNode()) { + return null; + } + + Object nullableInner = property.vendorExtensions.get( + "x-cpp-nullable-field-inner-type"); + if (value.isNull()) { + if (nullableInner != null) { + return "NullableField<" + nullableInner + ">::makeDefaultNull()"; + } + if (property.dataType != null + && property.dataType.startsWith("std::optional<")) { + return "std::nullopt"; + } + if ("std::nullptr_t".equals(property.dataType)) { + return "nullptr"; + } + if ("boost::json::value".equals(property.dataType)) { + return "boost::json::value(nullptr)"; + } + if (property.dataType != null + && property.dataType.startsWith("std::shared_ptr<")) { + // A branch-local default:null is an annotation, not a model value. + // Ignore it rather than rejecting an otherwise legal schema. + return null; + } + + throw new IllegalArgumentException( + "JSON null default is not representable by C++ property '" + + property.baseName + "' of type " + property.dataType); + } + + String expression; + if (value.isTextual()) { + expression = "\"" + escapeCppStringContent(value.textValue()) + "\""; + } else if (value.isBoolean()) { + expression = value.booleanValue() ? "true" : "false"; + } else if (value.isNumber()) { + expression = explicitNumericDefault(property, value.decimalValue()); + } else { + return null; + } + + if (nullableInner != null) { + return "NullableField<" + nullableInner + ">::makeDefaultValue(" + + expression + ")"; + } + return expression; + } + + protected static String explicitNumericDefault( + CodegenProperty property, java.math.BigDecimal value) { + if (property.isInteger || property.isLong) { + java.math.BigInteger integer; + try { + integer = value.toBigIntegerExact(); + } catch (ArithmeticException exception) { + throw new IllegalArgumentException( + "Non-integral default is not representable by integer property '" + + property.baseName + "'", exception); + } + if (property.isLong || "std::int64_t".equals(property.dataType)) { + java.math.BigInteger min = java.math.BigInteger.valueOf(Long.MIN_VALUE); + java.math.BigInteger max = java.math.BigInteger.valueOf(Long.MAX_VALUE); + if (integer.compareTo(min) < 0 || integer.compareTo(max) > 0) { + throw new IllegalArgumentException( + "Default is outside int64 range for property '" + + property.baseName + "'"); + } + if (integer.equals(min)) { + return "std::int64_t{-9223372036854775807LL - 1LL}"; + } + return "std::int64_t{" + integer + "LL}"; + } + try { + return "std::int32_t{" + integer.intValueExact() + "}"; + } catch (ArithmeticException exception) { + throw new IllegalArgumentException( + "Default is outside int32 range for property '" + + property.baseName + "'", exception); + } + } + + String literal = value.toString(); + boolean hasFloatingMarker = literal.indexOf('.') >= 0 + || literal.indexOf('e') >= 0 || literal.indexOf('E') >= 0; + if (!hasFloatingMarker) { + literal += ".0"; + } + if (property.isFloat || "float".equals(property.dataType)) { + float narrowed = value.floatValue(); + if (!Float.isFinite(narrowed) + || (value.signum() != 0 && narrowed == 0.0f)) { + throw new IllegalArgumentException( + "Default is outside finite float range for property '" + + property.baseName + "'"); + } + return literal + "F"; + } + double narrowed = value.doubleValue(); + if (!Double.isFinite(narrowed) + || (value.signum() != 0 && narrowed == 0.0)) { + throw new IllegalArgumentException( + "Default is outside finite double range for property '" + + property.baseName + "'"); + } + return literal; + } + + @Override + public String toDefaultValue(Schema p) { + if (ModelUtils.isStringSchema(p)) { + if (p.getDefault() != null) { + return "\"" + escapeCppStringContent(p.getDefault().toString()) + "\""; + } else { + return "\"\""; + } + } else if (ModelUtils.isBooleanSchema(p)) { + if (p.getDefault() != null) { + return p.getDefault().toString(); + } else { + return "false"; + } + } else if (ModelUtils.isDateSchema(p)) { + if (p.getDefault() != null) { + return "\"" + escapeCppStringContent(p.getDefault().toString()) + "\""; + } else { + return "\"\""; + } + } else if (ModelUtils.isDateTimeSchema(p)) { + if (p.getDefault() != null) { + return "\"" + escapeCppStringContent(p.getDefault().toString()) + "\""; + } else { + return "\"\""; + } + } else if (ModelUtils.isNumberSchema(p)) { + if (ModelUtils.isFloatSchema(p)) { // float + if (p.getDefault() != null) { + return p.getDefault().toString() + "f"; + } else { + return "0.0f"; + } + } else { // double + if (p.getDefault() != null) { + return p.getDefault().toString(); + } else { + return "0.0"; + } + } + } else if (ModelUtils.isIntegerSchema(p)) { + if (ModelUtils.isLongSchema(p)) { // long + if (p.getDefault() != null) { + return p.getDefault().toString() + "L"; + } else { + return "0L"; + } + } else { // integer + if (p.getDefault() != null) { + return p.getDefault().toString(); + } else { + return "0"; + } + } + } else if (ModelUtils.isByteArraySchema(p)) { + if (p.getDefault() != null) { + return "\"" + escapeCppStringContent(p.getDefault().toString()) + "\""; + } else { + return "\"\""; + } + } else if (ModelUtils.isMapSchema(p)) { + Schema inner = ModelUtils.getAdditionalProperties(p); + String innerType = inner == null ? "boost::json::value" : getTypeDeclaration(inner); + return "std::map()"; + } else if (ModelUtils.isArraySchema(p)) { + // Use getItems() directly to handle OpenAPI 3.1 JsonSchema + Schema inner = p.getItems(); + String innerType = inner != null ? getTypeDeclaration(inner) : "boost::json::value"; + return "std::vector<" + innerType + ">()"; + } else if (!StringUtils.isEmpty(p.get$ref())) { + String refName = toModelName(ModelUtils.getSimpleRef(p.get$ref())); + if (variantModels.contains(refName)) { + return refName + "()"; + } + return "std::make_shared<" + refName + ">()"; + } else if (ModelUtils.isNullType(p)) { + return "nullptr"; + } else if (ModelUtils.isAnyType(p) || ModelUtils.isFreeFormObject(p, openAPI)) { + return "boost::json::value()"; + } + + return "nullptr"; + } + + @Override + public String toDefaultValue(CodegenProperty codegenProperty, Schema schema) { + if (codegenProperty != null) { + if (codegenProperty.dataType != null && codegenProperty.dataType.startsWith("std::shared_ptr<")) { + return "nullptr"; + } + if ("boost::json::value".equals(codegenProperty.dataType)) { + return "boost::json::value()"; + } + Schema referenceSchema = Oas31CompositionLowering.referenceSchemaOf(schema); + if (referenceSchema != null && referenceSchema != schema + && schema.getDefault() == null) { + Schema referencedTarget = ModelUtils.getReferencedSchema(openAPI, referenceSchema); + if (referencedTarget != null && referencedTarget != referenceSchema + && codegenProperty.dataType != null + && codegenProperty.dataType.equals(getTypeDeclaration(referencedTarget))) { + return toDefaultValue(referencedTarget); + } + } + } + return super.toDefaultValue(codegenProperty, schema); + } + + @Override + public void setParameterEncodingValues(CodegenParameter codegenParameter, MediaType mediaType) { + super.setParameterEncodingValues(codegenParameter, mediaType); + // Detect Encoding Object headers that cannot be propagated to + // multipart parts. When an Encoding Object specifies headers, + // emit a diagnostic instead of silently dropping them. + if (codegenParameter.isFormParam && mediaType != null + && mediaType.getEncoding() != null) { + io.swagger.v3.oas.models.media.Encoding encoding = + mediaType.getEncoding().get(codegenParameter.baseName); + if (encoding != null && encoding.getHeaders() != null + && !encoding.getHeaders().isEmpty()) { + LOGGER.warn("Encoding Object on form parameter '{}' specifies {} header(s) " + + "that are not propagated to the multipart part. " + + "Generated code uses only the contentType field. " + + "Header keys: {}", + codegenParameter.baseName, + encoding.getHeaders().size(), + encoding.getHeaders().keySet()); + } + } + } + @Override + public void postProcessParameter(CodegenParameter parameter) { + super.postProcessParameter(parameter); + + boolean isPrimitiveType = parameter.isPrimitiveType == Boolean.TRUE; + boolean isArray = parameter.isArray == Boolean.TRUE; + boolean isMap = parameter.isMap == Boolean.TRUE; + boolean isString = parameter.isString == Boolean.TRUE; + parameter.vendorExtensions.put(X_CODEGEN_IS_RAW_BODY, + isPrimitiveType || isString || parameter.isByteArray || parameter.isBinary + || "std::string".equals(parameter.dataType)); + + if (!isPrimitiveType && !isArray && !isMap && !isString && !parameter.dataType.startsWith("std::shared_ptr") + && !"boost::json::value".equals(parameter.dataType) + && !"std::nullptr_t".equals(parameter.dataType) + && !parameter.dataType.startsWith("std::variant<") + && !parameter.dataType.startsWith("std::optional<") + && !"std::monostate".equals(parameter.dataType)) { + // Wrap non-primitive types in shared_ptr, unless: + // - The type is a variant/optional model (value semantics) + // - The type is a known variant model name from composed schemas + if (!variantModels.contains(parameter.dataType)) { + parameter.dataType = "std::shared_ptr<" + parameter.dataType + ">"; + parameter.defaultValue = "std::make_shared<" + parameter.dataType + ">()"; + } + } + + // Post-hoc unwrap: if the type ended up as std::shared_ptr, + // strip the shared_ptr wrapper (value semantics for variant types). + if (parameter.dataType != null && parameter.dataType.startsWith("std::shared_ptr<") + && parameter.dataType.endsWith(">")) { + String innerType = parameter.dataType.substring(16, parameter.dataType.length() - 1); + if (variantModels.contains(innerType)) { + parameter.dataType = innerType; + parameter.defaultValue = null; + } + } + + // For form params, validate that encoding style/explode combinations + // are representable in multipart/form-data. Only form-style is supported + // for multipart (space-delimited, pipe-delimited, and deep-object styles + // are not representable). Fail closed with a targeted diagnostic. + if (parameter.isFormParam) { + if (Boolean.TRUE.equals(parameter.isSpaceDelimited)) { + throw new UnsupportedSchemaAssertionException( + parameter.baseName, + "encoding-style"); + } + if (Boolean.TRUE.equals(parameter.isPipeDelimited)) { + throw new UnsupportedSchemaAssertionException( + parameter.baseName, + "encoding-style"); + } + if (Boolean.TRUE.equals(parameter.isDeepObject)) { + throw new UnsupportedSchemaAssertionException( + parameter.baseName, + "encoding-style"); + } + } + + // Tag variant form params for branch-aware multipart serialization. + // When a form parameter's type is a variant, the template uses + // addVariantFormParameter to dispatch binary branches as file parts + // and object branches as JSON parts. + // Only set for actual std::variant types, not for models that alias + // to primitive types (e.g., VideoModel → std::string), which would + // cause instantiation of addVariantFormParameter and + // an invalid std::visit call on a non-variant type. + boolean isVariantParam = false; + if (parameter.isFormParam && parameter.dataType != null) { + if (parameter.dataType.startsWith("std::variant<")) { + isVariantParam = true; + } else if (variantModels.contains(parameter.dataType)) { + String resolved = resolveThroughAliases(parameter.dataType); + if (resolved != null && resolved.startsWith("std::variant<")) { + isVariantParam = true; + } + } + } + if (isVariantParam) { + parameter.vendorExtensions.put("x-codegen-is-variant-form-param", true); + } + } + @Override + public String getSchemaType(Schema p) { + // Non-standard format (NOT core OAS vocabulary). Documented generator + // convenience for corpora that use Unix-epoch integer timestamps. + // Disable by not using format: unixtime in the source document. + if (p != null && "unixtime".equals(p.getFormat())) { + return "int64_t"; + } + String openAPIType = super.getSchemaType(p); + String type = null; + String modelName; + if (typeMapping.containsKey(openAPIType)) { + type = typeMapping.get(openAPIType); + } else { + type = openAPIType; + } + + modelName = toModelName(type); + return modelName; + } + @Override + public void updateCodegenPropertyEnum(CodegenProperty var) { + // Remove prefix added by DefaultCodegen + String originalDefaultValue = var.defaultValue; + super.updateCodegenPropertyEnum(var); + var.defaultValue = originalDefaultValue; + } + protected void refreshComponentSchemaIds(OpenAPI openApi) { + if (openApi == null || openApi.getComponents() == null + || openApi.getComponents().getSchemas() == null) { + componentSchemaIdsByName = Collections.emptyMap(); + return; + } + componentSchemaIdsByName = componentSchemaIds( + openApi.getComponents().getSchemas().keySet()); + } + @Override + public Map postProcessSupportingFileData(Map objs) { + Map processed = super.postProcessSupportingFileData(objs); + if (!validateOnDecode) { + return processed; + } + // Model processing can replace inline branch schema objects after the + // initial recovery pass; refresh the emitted graph from the raw spec. + Oas31RawSpecRecovery.recoverPristineLiterals(openAPI, getInputSpec()); + refreshComponentSchemaIds(openAPI); + Oas31SchemaIrEmitter emitter = new Oas31SchemaIrEmitter( + openAPI, compositionDescriptors, additionalProperties(), componentSchemaIdsByName); + Map produced = emitter.produce(processed); + supportingFiles.removeIf(file -> { + String destination = file.getDestinationFilename(); + return destination.startsWith("schema_ir.generated.chunk") + && destination.endsWith(".cpp"); + }); + int chunkCount = ((Number) produced.get("oas31SchemaIrChunkCount")).intValue(); + for (int chunk = 0; chunk < chunkCount; chunk++) { + supportingFiles.add(new SupportingFile( + Oas31SchemaIrEmitter.schemaIrChunkTemplate(chunk), + "model", Oas31SchemaIrEmitter.schemaIrChunkFilename(chunk))); + } + return produced; + } + @Override + public void preprocessOpenAPI(OpenAPI openAPI) { + beginGeneration(openAPI); + hasExplicitRootServers = detectExplicitRootServers(); + + List policyDiagnostics = validateDialectPolicy(openAPI); + if (!policyDiagnostics.isEmpty()) { + throw new IllegalArgumentException(String.join("; ", policyDiagnostics)); + } + super.preprocessOpenAPI(openAPI); + // Webhooks are inbound-only metadata for a client generator. Upstream + // folds them into the API map under the same fallback classname as path + // operations, which can replace the path API. Preserve their metadata, + // then remove them so outbound paths still generate; no listener is emitted. + if (openAPI.getWebhooks() != null && !openAPI.getWebhooks().isEmpty()) { + for (Map.Entry e : openAPI.getWebhooks().entrySet()) { + PathItem item = e.getValue(); + List methods = new ArrayList<>(); + if (item.getGet() != null) methods.add("GET " + idOf(item.getGet())); + if (item.getPut() != null) methods.add("PUT " + idOf(item.getPut())); + if (item.getPost() != null) methods.add("POST " + idOf(item.getPost())); + if (item.getDelete() != null) methods.add("DELETE " + idOf(item.getDelete())); + if (item.getPatch() != null) methods.add("PATCH " + idOf(item.getPatch())); + if (item.getHead() != null) methods.add("HEAD " + idOf(item.getHead())); + if (item.getOptions() != null) methods.add("OPTIONS " + idOf(item.getOptions())); + if (item.getTrace() != null) methods.add("TRACE " + idOf(item.getTrace())); + webhookPreservation.add(e.getKey() + + "[" + String.join(", ", methods) + "]"); + } + openAPI.setWebhooks(null); + } + // Capture callback and response-link names for generated API comments. + captureOperationMetadata(openAPI); + // Recover prefixItems dropped when the shared OAS 3.1 normalizer + // converts a type-array JsonSchema to ArraySchema. This must precede + // descriptor scanning so child schemas retain the pristine value. + Oas31RawSpecRecovery.restoreNormalizerDroppedPrefixItems(openAPI, getInputSpec()); + Oas31RawSpecRecovery.recoverPristineLiterals(openAPI, getInputSpec()); + // Populate variantModels and build composition descriptors before + // model processing begins so that getTypeDeclaration can resolve $ref + // to composed models as value types and branch semantics are captured + // before fromModel consumes composed schemas. + Map schemas = openAPI.getComponents() != null + ? openAPI.getComponents().getSchemas() : null; + if (schemas != null) { + // Build descriptor index: must happen after inline model resolver + // flattening so all inline schemas have been extracted to component + // references with stable $ref targets. + for (Map.Entry entry : schemas.entrySet()) { + String schemaName = entry.getKey(); + Schema schema = entry.getValue(); + List descriptors = + Oas31CompositionLowering.buildCompositionDescriptors( + schemaName, schema, openAPI, schemas); + if (!descriptors.isEmpty()) { + String modelName = toModelName(schemaName); + // The primary descriptor drives representation lowering; + // retain and validate every composition keyword separately. + compositionDescriptors.put(modelName, descriptors.get(0)); + compositionDescriptorSets.put(modelName, Collections.unmodifiableList( + new ArrayList<>(descriptors))); + for (CompositionDescriptor descriptor : descriptors) { + Oas31CompositionLowering.validateDescriptorAssertions(descriptor); + } + } + // allOf affects object storage even when oneOf or anyOf selects + // the public representation. + if (schema.getAllOf() != null && !schema.getAllOf().isEmpty()) { + AllOfIntersection intersection = + Oas31CompositionLowering.computeAllOfIntersection( + schemaName, schema, openAPI, schemas, new HashSet<>()); + if (intersection != null) { + allOfIntersections.put(toModelName(schemaName), intersection); + } + } + if ((schema.getOneOf() != null && !schema.getOneOf().isEmpty()) + || (schema.getAnyOf() != null && !schema.getAnyOf().isEmpty())) { + variantModels.add(schemaName); + } + } + } +} + + /** Shared namespace/validation options common to client and server generators. */ + protected void applySharedCppOptions() { + String modelNamespace = modelPackage.replaceAll("\\.", "::"); + additionalProperties.put("modelNamespaceDeclarations", modelPackage.split("\\.")); + additionalProperties.put("modelNamespace", modelNamespace); + additionalProperties.put("schemaValidationNamespace", + modelNamespace + "::detail::schema_validation"); + additionalProperties.put("schemaValidationHeaderGuardPrefix", + modelPackage.replaceAll("[^A-Za-z0-9]", "_").toUpperCase(Locale.ROOT)); + additionalProperties.put("apiHeaderGuardPrefix", + apiPackage.replaceAll("[^A-Za-z0-9]", "_").toUpperCase(Locale.ROOT)); + additionalProperties.put("apiNamespaceDeclarations", apiPackage.split("\\.")); + additionalProperties.put("apiNamespace", apiPackage.replaceAll("\\.", "::")); + + if (additionalProperties.containsKey("formatAssertionPolicy")) { + String policy = additionalProperties.get("formatAssertionPolicy") + .toString().trim().toLowerCase(Locale.ROOT); + if (!FORMAT_ASSERTION_POLICY_ANNOTATION.equals(policy)) { + throw new IllegalArgumentException( + "formatAssertionPolicy supports only 'annotation'; " + + "format assertions are not implemented"); + } + } + formatAssertionPolicy = FORMAT_ASSERTION_POLICY_ANNOTATION; + additionalProperties.put("formatAssertionPolicy", formatAssertionPolicy); + + if (additionalProperties.containsKey("compileWithValidation")) { + Object raw = additionalProperties.get("compileWithValidation"); + if (raw instanceof Boolean) { + validateOnDecode = (Boolean) raw; + } else { + validateOnDecode = Boolean.parseBoolean(raw.toString().trim()); + } + } + additionalProperties.put("validateOnDecode", validateOnDecode); + additionalProperties.put("compileWithValidation", validateOnDecode); + if (!validateOnDecode) { + supportingFiles.removeIf(CppBoostBeastModelCodegen::isSchemaValidationSupportingFile); + } + preserveAdditionalProperties = false; + if (additionalProperties.containsKey("preserveAdditionalProperties")) { + Object raw = additionalProperties.get("preserveAdditionalProperties"); + if (raw instanceof Boolean) { + preserveAdditionalProperties = (Boolean) raw; + } else { + String value = raw.toString().trim(); + if (!"true".equalsIgnoreCase(value) && !"false".equalsIgnoreCase(value)) { + throw new IllegalArgumentException( + "preserveAdditionalProperties must be true or false: " + value); + } + preserveAdditionalProperties = Boolean.parseBoolean(value); + } + } + additionalProperties.put("preserveAdditionalProperties", preserveAdditionalProperties); + if (additionalProperties.containsKey("tolerateNonNullableNulls")) { + Object raw = additionalProperties.get("tolerateNonNullableNulls"); + if (raw instanceof Boolean) { + tolerateNonNullableNulls = (Boolean) raw; + } else { + tolerateNonNullableNulls = Boolean.parseBoolean(raw.toString().trim()); + } + } + additionalProperties.put("tolerateNonNullableNulls", tolerateNonNullableNulls); + } + + protected void captureOperationMetadata(OpenAPI openAPI) { operationCallbacks.clear(); operationLinks.clear(); @@ -222,6 +1514,18 @@ public Map updateAllModels(Map objs) { } } + // Stamp every model with its component schema IR id after upstream + // model updates complete (mirrors the pre-refactor client pipeline). + refreshComponentSchemaIds(openAPI); + for (Map.Entry entry : objs.entrySet()) { + for (ModelMap modelMap : entry.getValue().getModels()) { + CodegenModel model = modelMap.getModel(); + String schemaName = model.schemaName != null + ? model.schemaName : entry.getKey(); + model.vendorExtensions.put("x-cpp-component-schema-id", + componentSchemaId(schemaName, componentSchemaIdsByName)); + } + } return objs; } diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/CppBoostBeastOperationFacts.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/CppBoostBeastOperationFacts.java new file mode 100644 index 000000000000..6aa504095a5c --- /dev/null +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/CppBoostBeastOperationFacts.java @@ -0,0 +1,133 @@ +package org.openapitools.codegen.languages; + +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.PathItem; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import io.swagger.v3.oas.models.servers.Server; +import org.openapitools.codegen.CodegenOperation; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Direction-agnostic operation facts shared by the Boost.Beast client and + * server template assemblers: raw-operation lookup, effective security + * groups, and server-list classification. + */ +final class CppBoostBeastOperationFacts { + private CppBoostBeastOperationFacts() { + } + + /** True when the list is exactly swagger-parser's implicit root default + * (a single Server with url "/") and the raw source omitted {@code servers}. */ + static boolean isParserDefaultServerList(List servers) { + return servers != null && servers.size() == 1 + && "/".equals(servers.get(0).getUrl()); + } + + /** The operation's effective security requirements as template-ready + * groups. Each group is an OR alternative containing AND-required scheme + * maps. An empty group is anonymous access; operation {@code security: []} + * clears inherited requirements. */ + static List>> effectiveSecurityGroups( + OpenAPI document, CodegenOperation op) { + List>> groups = new ArrayList<>(); + List requirements = null; + io.swagger.v3.oas.models.Operation raw = operationFor(document, op); + if (raw != null && raw.getSecurity() != null) { + requirements = raw.getSecurity(); // includes `[]` clears + } else if (document != null + && document.getSecurity() != null) { + requirements = document.getSecurity(); + } + if (requirements == null) { + return groups; // no security declared + } + Map schemes = document != null + && document.getComponents() != null + ? document.getComponents().getSecuritySchemes() + : null; + for (SecurityRequirement req : requirements) { + List> ands = new ArrayList<>(); + if (req != null) { + for (Map.Entry> e : req.entrySet()) { + SecurityScheme scheme = schemes == null + ? null : schemes.get(e.getKey()); + Map use = new LinkedHashMap<>(); + use.put("name", cppString(e.getKey())); + use.put("type", cppString(scheme == null || scheme.getType() == null + ? "unknown" : scheme.getType().toString())); + if (scheme != null && scheme.getType() == SecurityScheme.Type.APIKEY) { + use.put("in", cppString(scheme.getIn() == null ? "header" + : scheme.getIn().toString())); + use.put("paramName", cppString(scheme.getName() == null + ? "" : scheme.getName())); + } else { + use.put("in", ""); + use.put("paramName", ""); + } + use.put("httpScheme", cppString(scheme != null + && scheme.getType() == SecurityScheme.Type.HTTP + && scheme.getScheme() != null + ? scheme.getScheme() : "")); + List scopes = e.getValue() == null + ? new ArrayList() : e.getValue(); + use.put("scopes", scopes); + use.put("scopesRendered", scopes.isEmpty() ? null + : scopes.stream() + .map(s -> "\"" + cppString(s) + "\"") + .collect(java.util.stream.Collectors + .joining(", "))); + ands.add(use); + } + } + groups.add(ands); // empty ands = {} + } + return groups; + } + + /** The raw Operation behind a CodegenOperation (PathItem-method lookup). */ + static io.swagger.v3.oas.models.Operation operationFor( + OpenAPI document, CodegenOperation op) { + if (document == null || document.getPaths() == null) { + return null; + } + PathItem item = document.getPaths().get(op.path); + if (item == null) { + return null; + } + if ("GET".equals(op.httpMethod)) { + return item.getGet(); + } + if ("PUT".equals(op.httpMethod)) { + return item.getPut(); + } + if ("POST".equals(op.httpMethod)) { + return item.getPost(); + } + if ("DELETE".equals(op.httpMethod)) { + return item.getDelete(); + } + if ("OPTIONS".equals(op.httpMethod)) { + return item.getOptions(); + } + if ("HEAD".equals(op.httpMethod)) { + return item.getHead(); + } + if ("PATCH".equals(op.httpMethod)) { + return item.getPatch(); + } + if ("TRACE".equals(op.httpMethod)) { + return item.getTrace(); + } + return null; + } + + private static String cppString(String value) { + return CppBoostBeastModelCodegen.escapeCppStringContent( + value == null ? "" : value); + } +} diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/CppBoostBeastTemplateModelAssembler.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/CppBoostBeastTemplateModelAssembler.java index 160891725c24..eb54cd3f8be0 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/CppBoostBeastTemplateModelAssembler.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/CppBoostBeastTemplateModelAssembler.java @@ -170,104 +170,18 @@ private static String commentText(String value) { /** True when the list is exactly swagger-parser's implicit root default * (a single Server with url "/") and the raw source omitted `servers`. */ private static boolean isParserDefaultServerList(List servers) { - return servers != null && servers.size() == 1 - && "/".equals(servers.get(0).getUrl()); + return CppBoostBeastOperationFacts.isParserDefaultServerList(servers); } /** The operation's effective security requirements as template-ready - * groups. Each group is an OR alternative containing AND-required scheme - * maps. An empty group is anonymous access; operation `security: []` - * clears inherited requirements. */ + * groups (see {@link CppBoostBeastOperationFacts#effectiveSecurityGroups}). */ private List>> effectiveSecurityGroups(CodegenOperation op) { - List>> groups = new ArrayList<>(); - List requirements = null; - io.swagger.v3.oas.models.Operation raw = operationFor(op); - if (raw != null && raw.getSecurity() != null) { - requirements = raw.getSecurity(); // includes `[]` clears - } else if (phaseOpenApi != null - && phaseOpenApi.getSecurity() != null) { - requirements = phaseOpenApi.getSecurity(); - } - if (requirements == null) { - return groups; // no security declared - } - Map schemes = phaseOpenApi != null - && phaseOpenApi.getComponents() != null - ? phaseOpenApi.getComponents().getSecuritySchemes() - : null; - for (SecurityRequirement req : requirements) { - List> ands = new ArrayList<>(); - if (req != null) { - for (Map.Entry> e : req.entrySet()) { - SecurityScheme scheme = schemes == null - ? null : schemes.get(e.getKey()); - Map use = new LinkedHashMap<>(); - use.put("name", cppString(e.getKey())); - use.put("type", cppString(scheme == null || scheme.getType() == null - ? "unknown" : scheme.getType().toString())); - if (scheme != null && scheme.getType() == SecurityScheme.Type.APIKEY) { - use.put("in", cppString(scheme.getIn() == null ? "header" - : scheme.getIn().toString())); - use.put("paramName", cppString(scheme.getName() == null - ? "" : scheme.getName())); - } else { - use.put("in", ""); - use.put("paramName", ""); - } - use.put("httpScheme", cppString(scheme != null - && scheme.getType() == SecurityScheme.Type.HTTP - && scheme.getScheme() != null - ? scheme.getScheme() : "")); - List scopes = e.getValue() == null - ? new ArrayList() : e.getValue(); - use.put("scopes", scopes); - use.put("scopesRendered", scopes.isEmpty() ? null - : scopes.stream() - .map(s -> "\"" + cppString(s) + "\"") - .collect(java.util.stream.Collectors - .joining(", "))); - ands.add(use); - } - } - groups.add(ands); // empty ands = {} - } - return groups; + return CppBoostBeastOperationFacts.effectiveSecurityGroups(phaseOpenApi, op); } /** The raw Operation behind a CodegenOperation (PathItem-method lookup). */ private io.swagger.v3.oas.models.Operation operationFor(CodegenOperation op) { - if (phaseOpenApi == null || phaseOpenApi.getPaths() == null) { - return null; - } - PathItem item = phaseOpenApi.getPaths().get(op.path); - if (item == null) { - return null; - } - if ("GET".equals(op.httpMethod)) { - return item.getGet(); - } - if ("PUT".equals(op.httpMethod)) { - return item.getPut(); - } - if ("POST".equals(op.httpMethod)) { - return item.getPost(); - } - if ("DELETE".equals(op.httpMethod)) { - return item.getDelete(); - } - if ("OPTIONS".equals(op.httpMethod)) { - return item.getOptions(); - } - if ("HEAD".equals(op.httpMethod)) { - return item.getHead(); - } - if ("PATCH".equals(op.httpMethod)) { - return item.getPatch(); - } - if ("TRACE".equals(op.httpMethod)) { - return item.getTrace(); - } - return null; + return CppBoostBeastOperationFacts.operationFor(phaseOpenApi, op); } /** Returns the effective operation server URL with first-level variables diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/templating/GeneratorTemplateContentLocator.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/templating/GeneratorTemplateContentLocator.java index 62b9ca91887a..5296cad89cc9 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/templating/GeneratorTemplateContentLocator.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/templating/GeneratorTemplateContentLocator.java @@ -119,6 +119,18 @@ private String resolveFullTemplatePath(String relativeTemplateFile) { return loc; } + // Finally, probe additional shared embedded template directories (in + // order) so related generators can reuse a common template root. + for (String additionalDir : config.additionalEmbeddedTemplateDirs()) { + if (additionalDir == null || additionalDir.isEmpty()) { + continue; + } + final String additional = additionalDir + File.separator + relativeTemplateFile; + if (embeddedTemplateExists(additional)) { + return additional; + } + } + return null; } } diff --git a/modules/openapi-generator/src/main/resources/cpp-boost-beast-client/NullableField.h.mustache b/modules/openapi-generator/src/main/resources/cpp-boost-beast-common/NullableField.h.mustache similarity index 100% rename from modules/openapi-generator/src/main/resources/cpp-boost-beast-client/NullableField.h.mustache rename to modules/openapi-generator/src/main/resources/cpp-boost-beast-common/NullableField.h.mustache diff --git a/modules/openapi-generator/src/main/resources/cpp-boost-beast-client/anytype-header.mustache b/modules/openapi-generator/src/main/resources/cpp-boost-beast-common/anytype-header.mustache similarity index 100% rename from modules/openapi-generator/src/main/resources/cpp-boost-beast-client/anytype-header.mustache rename to modules/openapi-generator/src/main/resources/cpp-boost-beast-common/anytype-header.mustache diff --git a/modules/openapi-generator/src/main/resources/cpp-boost-beast-common/licenseInfo.mustache b/modules/openapi-generator/src/main/resources/cpp-boost-beast-common/licenseInfo.mustache new file mode 100644 index 000000000000..e737bfd822a0 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/cpp-boost-beast-common/licenseInfo.mustache @@ -0,0 +1,11 @@ +/** + * {{{appName}}} + * {{{appDescription}}} + * + * {{#version}}The version of the OpenAPI document: {{{.}}}{{/version}} +{{#infoEmail}} * Contact: {{{.}}} +{{/infoEmail}} * + * NOTE: This class is auto generated by OpenAPI-Generator {{{generatorVersion}}}. + * https://openapi-generator.tech + * Do not edit the class manually. + */ diff --git a/modules/openapi-generator/src/main/resources/cpp-boost-beast-client/model-header.mustache b/modules/openapi-generator/src/main/resources/cpp-boost-beast-common/model-header.mustache similarity index 100% rename from modules/openapi-generator/src/main/resources/cpp-boost-beast-client/model-header.mustache rename to modules/openapi-generator/src/main/resources/cpp-boost-beast-common/model-header.mustache diff --git a/modules/openapi-generator/src/main/resources/cpp-boost-beast-client/model-source.mustache b/modules/openapi-generator/src/main/resources/cpp-boost-beast-common/model-source.mustache similarity index 100% rename from modules/openapi-generator/src/main/resources/cpp-boost-beast-client/model-source.mustache rename to modules/openapi-generator/src/main/resources/cpp-boost-beast-common/model-source.mustache diff --git a/modules/openapi-generator/src/main/resources/cpp-boost-beast-client/oas31_deep_equal.mustache b/modules/openapi-generator/src/main/resources/cpp-boost-beast-common/oas31_deep_equal.mustache similarity index 100% rename from modules/openapi-generator/src/main/resources/cpp-boost-beast-client/oas31_deep_equal.mustache rename to modules/openapi-generator/src/main/resources/cpp-boost-beast-common/oas31_deep_equal.mustache diff --git a/modules/openapi-generator/src/main/resources/cpp-boost-beast-client/oas31_exact_json.mustache b/modules/openapi-generator/src/main/resources/cpp-boost-beast-common/oas31_exact_json.mustache similarity index 100% rename from modules/openapi-generator/src/main/resources/cpp-boost-beast-client/oas31_exact_json.mustache rename to modules/openapi-generator/src/main/resources/cpp-boost-beast-common/oas31_exact_json.mustache diff --git a/modules/openapi-generator/src/main/resources/cpp-boost-beast-client/oas31_exact_number.mustache b/modules/openapi-generator/src/main/resources/cpp-boost-beast-common/oas31_exact_number.mustache similarity index 100% rename from modules/openapi-generator/src/main/resources/cpp-boost-beast-client/oas31_exact_number.mustache rename to modules/openapi-generator/src/main/resources/cpp-boost-beast-common/oas31_exact_number.mustache diff --git a/modules/openapi-generator/src/main/resources/cpp-boost-beast-client/oas31_exact_number_source.mustache b/modules/openapi-generator/src/main/resources/cpp-boost-beast-common/oas31_exact_number_source.mustache similarity index 100% rename from modules/openapi-generator/src/main/resources/cpp-boost-beast-client/oas31_exact_number_source.mustache rename to modules/openapi-generator/src/main/resources/cpp-boost-beast-common/oas31_exact_number_source.mustache diff --git a/modules/openapi-generator/src/main/resources/cpp-boost-beast-client/oas31_schema_ir.mustache b/modules/openapi-generator/src/main/resources/cpp-boost-beast-common/oas31_schema_ir.mustache similarity index 100% rename from modules/openapi-generator/src/main/resources/cpp-boost-beast-client/oas31_schema_ir.mustache rename to modules/openapi-generator/src/main/resources/cpp-boost-beast-common/oas31_schema_ir.mustache diff --git a/modules/openapi-generator/src/main/resources/cpp-boost-beast-client/oas31_schema_ir_header.mustache b/modules/openapi-generator/src/main/resources/cpp-boost-beast-common/oas31_schema_ir_header.mustache similarity index 100% rename from modules/openapi-generator/src/main/resources/cpp-boost-beast-client/oas31_schema_ir_header.mustache rename to modules/openapi-generator/src/main/resources/cpp-boost-beast-common/oas31_schema_ir_header.mustache diff --git a/modules/openapi-generator/src/main/resources/cpp-boost-beast-client/oas31_schema_ir_source.mustache b/modules/openapi-generator/src/main/resources/cpp-boost-beast-common/oas31_schema_ir_source.mustache similarity index 100% rename from modules/openapi-generator/src/main/resources/cpp-boost-beast-client/oas31_schema_ir_source.mustache rename to modules/openapi-generator/src/main/resources/cpp-boost-beast-common/oas31_schema_ir_source.mustache diff --git a/modules/openapi-generator/src/main/resources/cpp-boost-beast-client/oas31_validator.mustache b/modules/openapi-generator/src/main/resources/cpp-boost-beast-common/oas31_validator.mustache similarity index 100% rename from modules/openapi-generator/src/main/resources/cpp-boost-beast-client/oas31_validator.mustache rename to modules/openapi-generator/src/main/resources/cpp-boost-beast-common/oas31_validator.mustache diff --git a/modules/openapi-generator/src/main/resources/cpp-boost-beast-client/validation-types.mustache b/modules/openapi-generator/src/main/resources/cpp-boost-beast-common/validation-types.mustache similarity index 100% rename from modules/openapi-generator/src/main/resources/cpp-boost-beast-client/validation-types.mustache rename to modules/openapi-generator/src/main/resources/cpp-boost-beast-common/validation-types.mustache diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/cppboostbeast/Oas31ExactRuntimeTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/cppboostbeast/Oas31ExactRuntimeTest.java index 8308c9c64a88..246c431c51e4 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/cppboostbeast/Oas31ExactRuntimeTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/cppboostbeast/Oas31ExactRuntimeTest.java @@ -44,7 +44,7 @@ public void exactRuntimePreservesArbitraryJsonNumbersEndToEnd() throws Exception Path includeDirectory = output; Path executable = output.resolve("oas31-exact-runtime-test"); Path validationTemplate = Path.of( - "src/main/resources/cpp-boost-beast-client/validation-types.mustache"); + "src/main/resources/cpp-boost-beast-common/validation-types.mustache"); String modelNamespace = "org::openapitools::client::model"; String validationNamespace = modelNamespace + "::detail::schema_validation"; String validationGuard = @@ -807,7 +807,7 @@ private static void writeValidationSupportHeaders( Path output, String namespaceName, String guardPrefix) throws IOException { - Path templateDirectory = Path.of("src/main/resources/cpp-boost-beast-client"); + Path templateDirectory = Path.of("src/main/resources/cpp-boost-beast-common"); String[][] headers = { {"oas31_exact_number.mustache", "Oas31ExactNumber.h"}, {"oas31_exact_json.mustache", "Oas31ExactJson.h"}, diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/templating/GeneratorTemplateContentLocatorAdditionalDirsTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/templating/GeneratorTemplateContentLocatorAdditionalDirsTest.java new file mode 100644 index 000000000000..6d11f002648c --- /dev/null +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/templating/GeneratorTemplateContentLocatorAdditionalDirsTest.java @@ -0,0 +1,74 @@ +/* + * Copyright 2026 OpenAPI-Generator Contributors (https://openapi-generator.tech) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.openapitools.codegen.templating; + +import org.openapitools.codegen.languages.CppBoostBeastClientCodegen; +import org.testng.Assert; +import org.testng.annotations.Test; +import java.io.File; +import java.util.List; + +public class GeneratorTemplateContentLocatorAdditionalDirsTest { + + private static String normalized(String path) { + return path.replace(File.separatorChar, '/'); + } + + @Test + public void resolvesTemplatesFromAdditionalEmbeddedDirs() { + CppBoostBeastClientCodegen codegen = new CppBoostBeastClientCodegen(); + GeneratorTemplateContentLocator locator = + new GeneratorTemplateContentLocator(codegen); + + String resolved = locator.getFullTemplatePath("oas31_validator.mustache"); + + Assert.assertNotNull(resolved, "shared template must resolve via additional dirs"); + Assert.assertEquals(normalized(resolved), "cpp-boost-beast-common/oas31_validator.mustache"); + } + + @Test + public void primaryEmbeddedDirWinsOverAdditionalDirs() { + CppBoostBeastClientCodegen codegen = new CppBoostBeastClientCodegen(); + // api-header.mustache exists in both cpp-boost-beast-client and (would) + // shadow-check: it only exists in the client dir, so assert the client + // dir is probed first by resolving a client-only template. + GeneratorTemplateContentLocator locator = + new GeneratorTemplateContentLocator(codegen); + + String resolved = locator.getFullTemplatePath("api-header.mustache"); + + Assert.assertNotNull(resolved); + Assert.assertEquals(normalized(resolved), "cpp-boost-beast-client/api-header.mustache"); + } + + @Test + public void unknownTemplateReturnsNull() { + CppBoostBeastClientCodegen codegen = new CppBoostBeastClientCodegen(); + GeneratorTemplateContentLocator locator = + new GeneratorTemplateContentLocator(codegen); + + Assert.assertNull(locator.getFullTemplatePath("no-such-template.mustache")); + } + + @Test + public void additionalDirsListIsConfiguredOnGenerator() { + CppBoostBeastClientCodegen codegen = new CppBoostBeastClientCodegen(); + + Assert.assertEquals(codegen.additionalEmbeddedTemplateDirs(), + List.of("cpp-boost-beast-common")); + } +} From f5375d36d77e1623718f4903543356a68a2dbf16 Mon Sep 17 00:00:00 2001 From: Benjamin Oldenburg Date: Wed, 26 Aug 2026 20:44:35 +0700 Subject: [PATCH 02/41] feat(cpp-boost-beast-server): generator, runtime, and API templates Add the cpp-boost-beast-server generator (BETA) emitting a C++17 Boost.Beast HTTP/1.1 server: strand-per-connection sessions with message_generator responses, encoded-segment routing with 404/405+Allow, OAS parameter deserialization (path simple/label/matrix, query form/space/pipe/deepObject, header simple, cookie form) with enum, pattern, and bound validation into RFC 9457 problem responses, JSON body codec over generated model to/fromJsonValue APIs, OR-of-AND security extraction with a deny-by-default Authorizer seam, single-shot strand posting responders, and an addApiImplStubs quick-start main. CMake links Boost 1.81+ json+url compiled libraries; -Wall/-W4 clean. --- .../languages/CppBoostBeastServerCodegen.java | 435 ++++++++++++++++++ ...oostBeastServerTemplateModelAssembler.java | 362 +++++++++++++++ .../org.openapitools.codegen.CodegenConfig | 1 + .../CMakeLists.txt.mustache | 130 ++++++ .../cpp-boost-beast-server/README.mustache | 80 ++++ .../api-header.mustache | 138 ++++++ .../api-source.mustache | 373 +++++++++++++++ .../authorizer-header.mustache | 37 ++ .../body-json-header.mustache | 245 ++++++++++ .../http-server-header.mustache | 59 +++ .../http-server-source.mustache | 356 ++++++++++++++ .../licenseInfo.mustache | 11 + .../cpp-boost-beast-server/main.mustache | 64 +++ .../param-codecs-header.mustache | 191 ++++++++ .../problem-header.mustache | 163 +++++++ .../responder-header.mustache | 93 ++++ .../router-header.mustache | 205 +++++++++ 17 files changed, 2943 insertions(+) create mode 100644 modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/CppBoostBeastServerCodegen.java create mode 100644 modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/CppBoostBeastServerTemplateModelAssembler.java create mode 100644 modules/openapi-generator/src/main/resources/cpp-boost-beast-server/CMakeLists.txt.mustache create mode 100644 modules/openapi-generator/src/main/resources/cpp-boost-beast-server/README.mustache create mode 100644 modules/openapi-generator/src/main/resources/cpp-boost-beast-server/api-header.mustache create mode 100644 modules/openapi-generator/src/main/resources/cpp-boost-beast-server/api-source.mustache create mode 100644 modules/openapi-generator/src/main/resources/cpp-boost-beast-server/authorizer-header.mustache create mode 100644 modules/openapi-generator/src/main/resources/cpp-boost-beast-server/body-json-header.mustache create mode 100644 modules/openapi-generator/src/main/resources/cpp-boost-beast-server/http-server-header.mustache create mode 100644 modules/openapi-generator/src/main/resources/cpp-boost-beast-server/http-server-source.mustache create mode 100644 modules/openapi-generator/src/main/resources/cpp-boost-beast-server/licenseInfo.mustache create mode 100644 modules/openapi-generator/src/main/resources/cpp-boost-beast-server/main.mustache create mode 100644 modules/openapi-generator/src/main/resources/cpp-boost-beast-server/param-codecs-header.mustache create mode 100644 modules/openapi-generator/src/main/resources/cpp-boost-beast-server/problem-header.mustache create mode 100644 modules/openapi-generator/src/main/resources/cpp-boost-beast-server/responder-header.mustache create mode 100644 modules/openapi-generator/src/main/resources/cpp-boost-beast-server/router-header.mustache diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/CppBoostBeastServerCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/CppBoostBeastServerCodegen.java new file mode 100644 index 000000000000..bb69dd472ba9 --- /dev/null +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/CppBoostBeastServerCodegen.java @@ -0,0 +1,435 @@ +/* + * Copyright 2026 OpenAPI-Generator Contributors (https://openapi-generator.tech) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.openapitools.codegen.languages; + +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.Operation; +import io.swagger.v3.oas.models.PathItem; +import io.swagger.v3.oas.models.media.Content; +import io.swagger.v3.oas.models.media.MediaType; +import io.swagger.v3.oas.models.parameters.Parameter; +import io.swagger.v3.oas.models.responses.ApiResponse; +import org.openapitools.codegen.CodegenType; +import org.openapitools.codegen.SupportingFile; +import org.openapitools.codegen.meta.GeneratorMetadata; +import org.openapitools.codegen.meta.Stability; +import org.openapitools.codegen.meta.features.DataTypeFeature; +import org.openapitools.codegen.meta.features.DocumentationFeature; +import org.openapitools.codegen.meta.features.GlobalFeature; +import org.openapitools.codegen.meta.features.ParameterFeature; +import org.openapitools.codegen.meta.features.SchemaSupportFeature; +import org.openapitools.codegen.meta.features.SecurityFeature; +import org.openapitools.codegen.meta.features.WireFormatFeature; +import org.openapitools.codegen.model.ModelMap; +import org.openapitools.codegen.model.OperationsMap; +import org.apache.commons.lang3.StringUtils; + +import java.io.File; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.EnumSet; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +/** + * C++ Boost.Beast HTTP server code generator. Emits an asynchronous + * HTTP/1.1 server (Boost.Beast + Boost.Asio + Boost.URL) with typed + * per-operation request/response contracts, OAS parameter deserialization, + * a pluggable security authorizer seam, RFC 9457 problem responses, and + * decode-time OAS 3.1 schema validation shared with the client generator. + * + *

Mustache templates are located in + * {@code src/main/resources/cpp-boost-beast-server/} with shared + * model/validation templates resolved from {@code cpp-boost-beast-common}. + */ +public class CppBoostBeastServerCodegen extends CppBoostBeastModelCodegen { + + public static final String DEFAULT_PACKAGE_NAME = "CppBoostBeastServer"; + public static final String ADD_API_IMPL_STUBS = "addApiImplStubs"; + + protected String packageName = DEFAULT_PACKAGE_NAME; + + @Override + public CodegenType getTag() { + return CodegenType.SERVER; + } + + @Override + public String getName() { + return "cpp-boost-beast-server"; + } + + @Override + public String getHelp() { + return "Generates a C++ Boost.Beast HTTP server."; + } + + public CppBoostBeastServerCodegen() { + super(); + openapiNormalizer.put("NORMALIZER_CLASS", + CppBoostBeastClientCodegen.CppBoostBeastOpenAPINormalizer.class.getName()); + generatorMetadata = GeneratorMetadata.newBuilder(generatorMetadata) + .stability(Stability.BETA) + .build(); + modifyFeatureSet(features -> features + .includeDocumentationFeatures(DocumentationFeature.Readme) + .securityFeatures(EnumSet.of( + SecurityFeature.ApiKey, + SecurityFeature.BasicAuth, + SecurityFeature.BearerToken)) + .wireFormatFeatures(EnumSet.of(WireFormatFeature.JSON)) + .includeGlobalFeatures( + GlobalFeature.ParameterStyling, + GlobalFeature.MultiServer + ) + .excludeGlobalFeatures( + GlobalFeature.XMLStructureDefinitions, + GlobalFeature.Callbacks, + GlobalFeature.LinkObjects + ) + .includeSchemaSupportFeatures( + SchemaSupportFeature.Polymorphism, + SchemaSupportFeature.Composite, + SchemaSupportFeature.oneOf, + SchemaSupportFeature.anyOf, + SchemaSupportFeature.allOf, + SchemaSupportFeature.not, + SchemaSupportFeature.Union + ) + .includeDataTypeFeatures( + DataTypeFeature.Int32, + DataTypeFeature.Int64, + DataTypeFeature.Float, + DataTypeFeature.Double, + DataTypeFeature.String, + DataTypeFeature.Boolean, + DataTypeFeature.Enum, + DataTypeFeature.Array, + DataTypeFeature.Maps, + DataTypeFeature.Object, + DataTypeFeature.Null, + DataTypeFeature.AnyType + ) + .excludeDataTypeFeatures( + DataTypeFeature.Decimal, + DataTypeFeature.Date, + DataTypeFeature.DateTime, + DataTypeFeature.Uuid, + DataTypeFeature.Byte, + DataTypeFeature.Binary, + DataTypeFeature.Password + ) + .includeParameterFeatures( + ParameterFeature.Cookie + ) + ); + + outputFolder = "generated-code" + File.separator + "cpp-boost-beast-server"; + modelTemplateFiles.put("model-header.mustache", ".h"); + modelTemplateFiles.put("model-source.mustache", ".cpp"); + apiTemplateFiles.put("api-header.mustache", ".h"); + apiTemplateFiles.put("api-source.mustache", ".cpp"); + + embeddedTemplateDir = templateDir = "cpp-boost-beast-server"; + + modelPackage = "org.openapitools.server.model"; + apiPackage = "org.openapitools.server.api"; + + cliOptions.clear(); + + addOption(org.openapitools.codegen.CodegenConstants.PACKAGE_NAME, + "C++ package and library name.", DEFAULT_PACKAGE_NAME); + addOption(org.openapitools.codegen.CodegenConstants.MODEL_PACKAGE, + "C++ namespace for models (convention: name.space.model).", this.modelPackage); + addOption(org.openapitools.codegen.CodegenConstants.API_PACKAGE, + "C++ namespace for apis (convention: name.space.api).", this.apiPackage); + org.openapitools.codegen.CliOption compileWithValidationOption = + new org.openapitools.codegen.CliOption("compileWithValidation", + "Emit schema-validation IR and kValidateOnDecode=true in generated" + + " ValidationTypes.h (default). Set to false to omit the IR."); + compileWithValidationOption.defaultValue(Boolean.TRUE.toString()); + cliOptions.add(compileWithValidationOption); + org.openapitools.codegen.CliOption tolerateOption = + new org.openapitools.codegen.CliOption( + "tolerateNonNullableNulls", + "Treat explicit JSON null values as absent for generated model" + + " properties whose schemas do not allow null. Enabled by" + + " default; set to false for strict schema decoding."); + tolerateOption.defaultValue(Boolean.TRUE.toString()); + cliOptions.add(tolerateOption); + org.openapitools.codegen.CliOption preserveOption = + new org.openapitools.codegen.CliOption( + "preserveAdditionalProperties", + "Retain undeclared JSON object members in generated object models" + + " and re-emit them; set to false for strict handling."); + preserveOption.defaultValue(Boolean.FALSE.toString()); + cliOptions.add(preserveOption); + org.openapitools.codegen.CliOption stubsOption = + org.openapitools.codegen.CliOption.newBoolean( + ADD_API_IMPL_STUBS, + "Generate API implementation stubs that answer 501 problem+json" + + " and a sample main.cpp for quick start"); + stubsOption.defaultValue(Boolean.FALSE.toString()); + cliOptions.add(stubsOption); + + supportingFiles.add(new SupportingFile("validation-types.mustache", "model", "ValidationTypes.h")); + supportingFiles.add(new SupportingFile("NullableField.h.mustache", "model", "NullableField.h")); + supportingFiles.add(new SupportingFile("anytype-header.mustache", "model", "AnyType.h")); + supportingFiles.add(new SupportingFile( + "oas31_exact_number.mustache", "model", "Oas31ExactNumber.h")); + supportingFiles.add(new SupportingFile( + "oas31_exact_number_source.mustache", "model", "Oas31ExactNumber.cpp")); + supportingFiles.add(new SupportingFile("oas31_schema_ir.mustache", "model", "Oas31SchemaIr.h")); + supportingFiles.add(new SupportingFile("oas31_deep_equal.mustache", "model", "Oas31DeepEqual.h")); + supportingFiles.add(new SupportingFile("oas31_exact_json.mustache", "model", "Oas31ExactJson.h")); + supportingFiles.add(new SupportingFile("oas31_validator.mustache", "model", "Oas31Validator.h")); + supportingFiles.add(new SupportingFile( + "oas31_schema_ir_header.mustache", "model", "Oas31SchemaRegistry.h")); + supportingFiles.add(new SupportingFile( + "oas31_schema_ir_source.mustache", "model", "schema_ir.generated.cpp")); + + supportingFiles.add(new SupportingFile("http-server-header.mustache", "server", "HttpServer.h")); + supportingFiles.add(new SupportingFile("http-server-source.mustache", "server", "HttpServer.cpp")); + supportingFiles.add(new SupportingFile("router-header.mustache", "server", "Router.h")); + supportingFiles.add(new SupportingFile("responder-header.mustache", "server", "Responder.h")); + supportingFiles.add(new SupportingFile("problem-header.mustache", "server", "Problem.h")); + supportingFiles.add(new SupportingFile("authorizer-header.mustache", "server", "Authorizer.h")); + supportingFiles.add(new SupportingFile("param-codecs-header.mustache", "server", "ParamCodecs.h")); + supportingFiles.add(new SupportingFile("body-json-header.mustache", "server", "BodyJson.h")); + supportingFiles.add(new SupportingFile("README.mustache", "", "README.md")); + supportingFiles.add(new SupportingFile("CMakeLists.txt.mustache", "", "CMakeLists.txt")); + + languageSpecificPrimitives = new HashSet( + Arrays.asList("int", "char", "bool", "long", "float", "double", + "std::int32_t", "std::int64_t")); + + typeMapping.put("date", "std::string"); + typeMapping.put("DateTime", "std::string"); + typeMapping.put("string", "std::string"); + typeMapping.put("integer", "std::int32_t"); + typeMapping.put("long", "std::int64_t"); + typeMapping.put("boolean", "bool"); + typeMapping.put("array", "std::vector"); + typeMapping.put("set", "std::vector"); + typeMapping.put("map", "std::map"); + typeMapping.put("file", "std::string"); + typeMapping.put("object", "boost::json::value"); + typeMapping.put("number", "double"); + typeMapping.put("UUID", "std::string"); + typeMapping.put("URI", "std::string"); + typeMapping.put("ByteArray", "std::string"); + + importMapping.put("std::vector", "#include "); + importMapping.put("std::map", "#include "); + importMapping.put("std::string", "#include "); + importMapping.put("int32_t", "#include "); + importMapping.put("int64_t", "#include "); + importMapping.put("boost::json::value", "#include "); + importMapping.put("std::nullptr_t", "#include "); + importMapping.put("Null", "#include "); + importMapping.put("std::optional", "#include "); + importMapping.put("std::variant", "#include "); + importMapping.put("std::monostate", "#include "); + importMapping.put("std::shared_ptr", "#include "); + importMapping.put("AnyType", "#include \"AnyType.h\""); + } + + @Override + public void processOpts() { + super.processOpts(); + packageName = additionalProperties.getOrDefault( + org.openapitools.codegen.CodegenConstants.PACKAGE_NAME, + DEFAULT_PACKAGE_NAME).toString(); + if (StringUtils.isBlank(packageName)) { + throw new IllegalArgumentException("packageName must not be blank"); + } + additionalProperties.put( + org.openapitools.codegen.CodegenConstants.PACKAGE_NAME, packageName); + applySharedCppOptions(); + + boolean addStubs = Boolean.parseBoolean( + additionalProperties.getOrDefault(ADD_API_IMPL_STUBS, Boolean.FALSE) + .toString()); + additionalProperties.put(ADD_API_IMPL_STUBS, addStubs); + if (addStubs) { + supportingFiles.add(new SupportingFile("main.mustache", "", "main.cpp")); + } + } + + @Override + public void preprocessOpenAPI(OpenAPI openAPI) { + super.preprocessOpenAPI(openAPI); + List rejections = validateServerSupportSurface(openAPI); + if (!rejections.isEmpty()) { + throw new IllegalArgumentException( + "cpp-boost-beast-server: " + String.join("; ", rejections)); + } + } + + @Override + public OperationsMap postProcessOperationsWithModels( + OperationsMap objs, List allModels) { + return new CppBoostBeastServerTemplateModelAssembler(sourceOpenApi) + .assemble(objs, allModels); + } + + /** + * Generation-time rejection gate for surfaces the initial server runtime + * does not implement. Fails closed with precise diagnostics instead of + * emitting silent stubs. + */ + List validateServerSupportSurface(OpenAPI openAPI) { + List diagnostics = new ArrayList<>(); + if (openAPI == null || openAPI.getPaths() == null) { + return diagnostics; + } + Map shapeOwners = new LinkedHashMap<>(); + for (Map.Entry pathEntry : openAPI.getPaths().entrySet()) { + String pathTemplate = pathEntry.getKey(); + String previous = shapeOwners.putIfAbsent( + routeShapeKey(pathTemplate), pathTemplate); + if (previous != null && !previous.equals(pathTemplate)) { + diagnostics.add("path templates '" + previous + "' and '" + pathTemplate + + "' have the same shape; server routing requires distinct shapes"); + } + PathItem item = pathEntry.getValue(); + if (item == null || item.readOperationsMap() == null) { + continue; + } + for (Map.Entry opEntry + : item.readOperationsMap().entrySet()) { + Operation operation = opEntry.getValue(); + if (operation == null) { + continue; + } + String operationId = operation.getOperationId() != null + ? operation.getOperationId() + : opEntry.getKey() + " " + pathTemplate; + if (operation.getRequestBody() != null + && operation.getRequestBody().getContent() != null) { + collectMediaTypeRejections( + operation.getRequestBody().getContent(), + operationId, diagnostics); + } + if (operation.getResponses() != null) { + for (ApiResponse response : operation.getResponses().values()) { + if (response != null && response.getContent() != null) { + collectMediaTypeRejections( + response.getContent(), operationId, diagnostics); + } + } + } + appendParameterRejections(operation.getParameters(), + pathEntry.getKey() + ":" + opEntry.getKey(), diagnostics); + appendParameterRejections(item.getParameters(), + pathEntry.getKey() + ":pathItem", diagnostics); + } + } + return diagnostics; + } + + private static void collectMediaTypeRejections( + Content content, String operationId, List diagnostics) { + for (String mediaType : content.keySet()) { + if (!isSupportedMediaType(mediaType)) { + diagnostics.add("operation '" + operationId + + "' uses unsupported media type '" + mediaType + + "'; only JSON bodies are supported"); + } + } + } + + private static boolean isSupportedMediaType(String mediaType) { + String normalized = mediaType == null ? "" : mediaType.trim().toLowerCase(Locale.ROOT); + int semicolon = normalized.indexOf(';'); + if (semicolon >= 0) { + normalized = normalized.substring(0, semicolon).trim(); + } + return "application/json".equals(normalized) + || normalized.endsWith("+json") + || "*/*".equals(normalized); + } + + private static void appendParameterRejections( + List parameters, String location, List diagnostics) { + if (parameters == null) { + return; + } + for (Parameter parameter : parameters) { + if (parameter == null) { + continue; + } + String label = parameter.getName() != null + ? parameter.getName() : "(unnamed)"; + if (parameter.getContent() != null) { + diagnostics.add("parameter '" + label + "' at " + location + + " uses content-style serialization; only schema" + + " parameters are supported"); + continue; + } + String style = parameter.getStyle() == null + ? null : parameter.getStyle().toString(); + if (style == null) { + continue; // location default is allowed + } + String in = parameter.getIn() == null ? "" : parameter.getIn(); + boolean allowed; + switch (in) { + case "header": + allowed = "simple".equals(style); + break; + case "cookie": + allowed = "form".equals(style); + break; + case "query": + allowed = "form".equals(style) || "spaceDelimited".equals(style) + || "pipeDelimited".equals(style) || "deepObject".equals(style); + break; + case "path": + allowed = "simple".equals(style) || "label".equals(style) + || "matrix".equals(style); + break; + default: + allowed = true; + break; + } + if (!allowed) { + diagnostics.add("parameter '" + label + "' at " + location + + " uses unsupported style '" + style + "' for in='" + in + "'"); + } + } + } + + /** Canonical routing shape: literal vs placeholder per path segment. */ + private static String routeShapeKey(String pathTemplate) { + StringBuilder key = new StringBuilder(); + for (String segment : pathTemplate.split("/")) { + if (segment.isEmpty()) { + continue; + } + if (segment.startsWith("{") && segment.endsWith("}")) { + key.append("/{"); + } else { + key.append('/').append(segment); + } + } + return key.length() == 0 ? "/" : key.toString(); + } +} diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/CppBoostBeastServerTemplateModelAssembler.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/CppBoostBeastServerTemplateModelAssembler.java new file mode 100644 index 000000000000..92af55da8dca --- /dev/null +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/CppBoostBeastServerTemplateModelAssembler.java @@ -0,0 +1,362 @@ +/* + * Copyright 2026 OpenAPI-Generator Contributors (https://openapi-generator.tech) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.openapitools.codegen.languages; + +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.Operation; +import io.swagger.v3.oas.models.parameters.Parameter; +import org.openapitools.codegen.CodegenOperation; +import org.openapitools.codegen.CodegenParameter; +import org.openapitools.codegen.CodegenProperty; +import org.openapitools.codegen.CodegenResponse; +import org.openapitools.codegen.model.ModelMap; +import org.openapitools.codegen.model.OperationsMap; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Template-model assembly for the Boost.Beast server generator: converts each + * {@link CodegenOperation} into the vendor-extension facts consumed by the + * server api-header/api-source templates (route table, typed request structs, + * responder methods, security groups, parameter validation constraints). + */ +final class CppBoostBeastServerTemplateModelAssembler { + + private final OpenAPI sourceOpenApi; + + CppBoostBeastServerTemplateModelAssembler(OpenAPI sourceOpenApi) { + this.sourceOpenApi = sourceOpenApi; + } + + OperationsMap assemble(OperationsMap objs, List allModels) { + if (objs == null || objs.getOperations() == null) { + return objs; + } + for (CodegenOperation op : objs.getOperations().getOperation()) { + if (op == null) { + continue; + } + Operation raw = CppBoostBeastOperationFacts.operationFor(sourceOpenApi, op); + + Map route = new LinkedHashMap<>(); + route.put("method", op.httpMethod); + route.put("pathTemplate", op.path); + route.put("hasBody", !op.bodyParams.isEmpty()); + op.vendorExtensions.put("x-server-route", route); + + op.vendorExtensions.put("x-server-operation-pascal", + pascalCase(op.operationId != null ? op.operationId : op.operationIdLowerCase)); + + op.vendorExtensions.put("x-server-params", + serverParams(op, raw)); + + Map body = requestBodyFacts(op, raw); + op.vendorExtensions.put("x-server-has-request-body", body.get("hasBody")); + op.vendorExtensions.put("x-server-request-model", body.get("model")); + op.vendorExtensions.put("x-server-request-kind", body.get("kind")); + op.vendorExtensions.put("x-server-request-inner", body.get("inner")); + op.vendorExtensions.put("x-server-request-media-types", + body.get("mediaTypes")); + + op.vendorExtensions.put("x-server-responses", serverResponses(op)); + + op.vendorExtensions.put("x-server-security-groups", + CppBoostBeastOperationFacts.effectiveSecurityGroups(sourceOpenApi, op)); + } + return objs; + } + + // ------------------------------------------------------------------ + // Parameters + // ------------------------------------------------------------------ + + private List> serverParams(CodegenOperation op, Operation raw) { + List> params = new ArrayList<>(); + for (CodegenParameter param : op.allParams) { + if (param == null) { + continue; + } + Map facts = new LinkedHashMap<>(); + facts.put("cppName", param.paramName != null ? param.paramName : param.baseName); + facts.put("baseName", param.baseName); + String in = param.isPathParam ? "path" + : param.isQueryParam ? "query" + : param.isHeaderParam ? "header" + : param.isCookieParam ? "cookie" : "body"; + facts.put("in", in); + facts.put("isPath", "path".equals(in)); + facts.put("isQuery", "query".equals(in)); + facts.put("isHeader", "header".equals(in)); + facts.put("isCookie", "cookie".equals(in)); + Object style = param.vendorExtensions.get("x-codegen-param-style"); + String styleText = style == null ? "" : style.toString(); + if (styleText.isEmpty()) { + styleText = "query".equals(in) || "cookie".equals(in) + ? "form" : "simple"; + } + Object explode = param.vendorExtensions.get("x-codegen-param-explode"); + boolean explodeFlag = Boolean.TRUE.equals(explode); + if (explode == null) { + explodeFlag = "form".equals(styleText); + } + facts.put("style", styleText); + facts.put("styleSimple", "simple".equals(styleText)); + facts.put("styleLabel", "label".equals(styleText)); + facts.put("styleMatrix", "matrix".equals(styleText)); + facts.put("styleForm", "form".equals(styleText)); + facts.put("styleSpaceDelimited", "spaceDelimited".equals(styleText)); + facts.put("stylePipeDelimited", "pipeDelimited".equals(styleText)); + facts.put("styleDeepObject", "deepObject".equals(styleText)); + facts.put("explode", explodeFlag); + facts.put("required", param.required); + facts.put("isContainer", Boolean.TRUE.equals(param.isContainer) + || Boolean.TRUE.equals(param.isArray)); + String dataType = param.dataType == null ? "" : param.dataType; + if (dataType.startsWith("std::shared_ptr<") && dataType.endsWith(">")) { + dataType = dataType.substring( + "std::shared_ptr<".length(), dataType.length() - 1).trim(); + } + facts.put("dataType", dataType); + facts.put("innerType", innerTemplateArg(dataType)); + facts.put("defaultValue", param.defaultValue == null ? "" : param.defaultValue); + facts.put("stringKind", "std::string".equals(dataType)); + facts.put("integerKind", "std::int32_t".equals(dataType) + || "std::int64_t".equals(dataType)); + facts.put("numberKind", "float".equals(dataType) + || "double".equals(dataType)); + facts.put("boolKind", "bool".equals(dataType)); + + Parameter rawParam = findRawParameter(op, raw, param.baseName, in); + applySchemaConstraints(facts, rawParam == null ? null : rawParam.getSchema()); + + params.add(facts); + } + return params; + } + + private Parameter findRawParameter( + CodegenOperation op, Operation raw, String baseName, String in) { + if (raw != null && raw.getParameters() != null) { + for (Parameter candidate : raw.getParameters()) { + if (candidate != null && baseName.equals(candidate.getName()) + && (candidate.getIn() == null || candidate.getIn().equals(in))) { + return candidate; + } + } + } + return null; + } + + private void applySchemaConstraints( + Map facts, io.swagger.v3.oas.models.media.Schema schema) { + List enumValues = new ArrayList<>(); + String enumKind = ""; + if (schema != null && schema.getEnum() != null && !schema.getEnum().isEmpty()) { + for (Object value : schema.getEnum()) { + if (value instanceof Boolean) { + enumValues.add(value.toString()); + if (enumKind.isEmpty()) { + enumKind = "bool"; + } + } else if (value instanceof Integer || value instanceof Long + || value instanceof Short || value instanceof Byte) { + enumValues.add(value.toString()); + if (enumKind.isEmpty() || "bool".equals(enumKind)) { + enumKind = "integer"; + } + } else if (value instanceof Double || value instanceof Float + || value instanceof java.math.BigDecimal) { + enumValues.add(value.toString()); + if (enumKind.isEmpty() || "bool".equals(enumKind)) { + enumKind = "number"; + } + } else { + enumValues.add("\"" + + CppBoostBeastModelCodegen.escapeCppStringContent( + value == null ? "" : value.toString()) + + "\""); + if (enumKind.isEmpty()) { + enumKind = "string"; + } + } + } + } + facts.put("enumValues", enumValues); + facts.put("enumKind", enumKind); + facts.put("hasEnum", !enumValues.isEmpty()); + String pattern = schema != null && schema.getPattern() != null + ? CppBoostBeastModelCodegen.escapeCppStringContent(schema.getPattern()) : ""; + facts.put("pattern", pattern); + facts.put("hasPattern", !pattern.isEmpty()); + String minimum = schema != null && schema.getMinimum() != null + ? schema.getMinimum().toString() : ""; + facts.put("minimum", minimum); + facts.put("hasMinimum", !minimum.isEmpty()); + String maximum = schema != null && schema.getMaximum() != null + ? schema.getMaximum().toString() : ""; + facts.put("maximum", maximum); + facts.put("hasMaximum", !maximum.isEmpty()); + String minLength = schema != null && schema.getMinLength() != null + ? schema.getMinLength().toString() : ""; + facts.put("minLength", minLength); + facts.put("hasMinLength", !minLength.isEmpty()); + String maxLength = schema != null && schema.getMaxLength() != null + ? schema.getMaxLength().toString() : ""; + facts.put("maxLength", maxLength); + facts.put("hasMaxLength", !maxLength.isEmpty()); + } + + // ------------------------------------------------------------------ + // Request body + // ------------------------------------------------------------------ + + private Map requestBodyFacts(CodegenOperation op, Operation raw) { + Map facts = new LinkedHashMap<>(); + List mediaTypes = new ArrayList<>(); + if (raw != null && raw.getRequestBody() != null + && raw.getRequestBody().getContent() != null) { + mediaTypes.addAll(raw.getRequestBody().getContent().keySet()); + } + String rendered = ""; + for (String mediaType : mediaTypes) { + if (!rendered.isEmpty()) { + rendered += ", "; + } + rendered += "\"" + CppBoostBeastModelCodegen.escapeCppStringContent(mediaType) + + "\""; + } + facts.put("hasBody", !mediaTypes.isEmpty()); + facts.put("mediaTypes", rendered); + + String dataType = op.bodyParam != null && op.bodyParam.dataType != null + ? op.bodyParam.dataType : ""; + if (dataType.startsWith("std::shared_ptr<") && dataType.endsWith(">")) { + dataType = dataType.substring( + "std::shared_ptr<".length(), dataType.length() - 1).trim(); + } + facts.put("model", dataType); + facts.put("kind", bodyKind(dataType)); + facts.put("inner", innerTemplateArg(dataType)); + return facts; + } + + // ------------------------------------------------------------------ + // Responses + // ------------------------------------------------------------------ + + private List> serverResponses(CodegenOperation op) { + List> responses = new ArrayList<>(); + if (op.responses == null) { + return responses; + } + for (CodegenResponse response : op.responses) { + if (response == null) { + continue; + } + Map facts = new LinkedHashMap<>(); + boolean isDefault = Boolean.TRUE.equals(response.isDefault) + || (response.code != null && "default".equals(response.code)); + String code = response.code == null ? "default" : response.code; + facts.put("code", code); + facts.put("isDefault", isDefault); + facts.put("sendMethod", isDefault + ? "sendDefault" : "send" + sanitizeCode(code)); + String dataType = response.dataType == null ? "" : response.dataType; + if (dataType.startsWith("std::shared_ptr<") && dataType.endsWith(">")) { + dataType = dataType.substring( + "std::shared_ptr<".length(), dataType.length() - 1).trim(); + } + facts.put("hasModel", !dataType.isEmpty()); + facts.put("cppType", dataType); + facts.put("kind", bodyKind(dataType)); + facts.put("inner", innerTemplateArg(dataType)); + responses.add(facts); + } + return responses; + } + + /** Response/body serialization kind derived from the C++ type. */ + static String bodyKind(String dataType) { + if (dataType == null || dataType.isEmpty()) { + return "none"; + } + if (dataType.startsWith("std::vector<")) { + return "vector"; + } + if (dataType.startsWith("std::map<")) { + return "map"; + } + if ("boost::json::value".equals(dataType)) { + return "any"; + } + return "model"; + } + + /** Inner template argument for containers ("" for scalars). */ + static String innerTemplateArg(String dataType) { + if (dataType == null) { + return ""; + } + if (dataType.startsWith("std::vector<") && dataType.endsWith(">")) { + return dataType.substring("std::vector<".length(), dataType.length() - 1).trim(); + } + if (dataType.startsWith("std::map<") && dataType.endsWith(">")) { + String args = dataType.substring("std::map<".length(), dataType.length() - 1); + int depth = 0; + for (int i = 0; i < args.length(); i++) { + char c = args.charAt(i); + if (c == '<') { + depth++; + } else if (c == '>') { + depth--; + } else if (c == ',' && depth == 0) { + return args.substring(i + 1).trim(); + } + } + } + return ""; + } + + private static String sanitizeCode(String code) { + StringBuilder out = new StringBuilder(); + for (char c : code.toCharArray()) { + out.append(Character.isDigit(c) ? c : '_'); + } + return out.toString(); + } + + private static String pascalCase(String operationId) { + if (operationId == null || operationId.isEmpty()) { + return "Operation"; + } + StringBuilder out = new StringBuilder(); + boolean upperNext = true; + for (char c : operationId.toCharArray()) { + if (c == '_' || c == '-' || c == ' ' || c == '.') { + upperNext = true; + } else if (upperNext) { + out.append(Character.toUpperCase(c)); + upperNext = false; + } else { + out.append(c); + } + } + return out.toString(); + } +} diff --git a/modules/openapi-generator/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig b/modules/openapi-generator/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig index 24b98443bca6..4a6fd4c244ee 100644 --- a/modules/openapi-generator/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig +++ b/modules/openapi-generator/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig @@ -14,6 +14,7 @@ org.openapitools.codegen.languages.ClojureClientCodegen org.openapitools.codegen.languages.ConfluenceWikiCodegen org.openapitools.codegen.languages.CppHttplibServerCodegen org.openapitools.codegen.languages.CppBoostBeastClientCodegen +org.openapitools.codegen.languages.CppBoostBeastServerCodegen org.openapitools.codegen.languages.CppOatppClientCodegen org.openapitools.codegen.languages.CppQtClientCodegen org.openapitools.codegen.languages.CppQtQHttpEngineServerCodegen diff --git a/modules/openapi-generator/src/main/resources/cpp-boost-beast-server/CMakeLists.txt.mustache b/modules/openapi-generator/src/main/resources/cpp-boost-beast-server/CMakeLists.txt.mustache new file mode 100644 index 000000000000..85dc39407dbc --- /dev/null +++ b/modules/openapi-generator/src/main/resources/cpp-boost-beast-server/CMakeLists.txt.mustache @@ -0,0 +1,130 @@ +cmake_minimum_required(VERSION 3.14) +project({{{packageName}}} VERSION 1.0.0 LANGUAGES CXX) + +include(GNUInstallDirs) + +if (POLICY CMP0167) + cmake_policy(SET CMP0167 OLD) +endif () + +set(BOOST_BOOST_TARGET_PREDEFINED FALSE) +set(BOOST_JSON_TARGET_PREDEFINED FALSE) +set(BOOST_URL_TARGET_PREDEFINED FALSE) +if (TARGET Boost::boost) + set(BOOST_BOOST_TARGET_PREDEFINED TRUE) +endif () +if (TARGET Boost::json) + set(BOOST_JSON_TARGET_PREDEFINED TRUE) +endif () +if (TARGET Boost::url) + set(BOOST_URL_TARGET_PREDEFINED TRUE) +endif () + +find_package(Boost 1.81 REQUIRED COMPONENTS json url) +# Imported targets created in this subdirectory are otherwise invisible to +# sibling consumers when this project is included with add_subdirectory(). +if (NOT BOOST_BOOST_TARGET_PREDEFINED) + set_property(TARGET Boost::boost PROPERTY IMPORTED_GLOBAL TRUE) +endif () +if (NOT BOOST_JSON_TARGET_PREDEFINED) + set_property(TARGET Boost::json PROPERTY IMPORTED_GLOBAL TRUE) +endif () +if (NOT BOOST_URL_TARGET_PREDEFINED) + set_property(TARGET Boost::url PROPERTY IMPORTED_GLOBAL TRUE) +endif () +set(THREADS_TARGET_PREDEFINED FALSE) +if (TARGET Threads::Threads) + set(THREADS_TARGET_PREDEFINED TRUE) +endif () +set(THREADS_PREFER_PTHREAD_FLAG TRUE) +find_package(Threads REQUIRED) +if (NOT THREADS_TARGET_PREDEFINED) + set_property(TARGET Threads::Threads PROPERTY IMPORTED_GLOBAL TRUE) +endif () + +# Boost.URL is consumed in header-only mode from HttpServer.cpp. +if (MSVC) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /W4") +else () + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fPIC -Wall") +endif () + + +add_library({{{packageName}}} STATIC) + +set_property(TARGET {{{packageName}}} PROPERTY CXX_STANDARD 17) +set_property(TARGET {{{packageName}}} PROPERTY CXX_STANDARD_REQUIRED ON) +set_property(TARGET {{{packageName}}} PROPERTY CXX_EXTENSIONS OFF) + +target_sources({{{packageName}}} PRIVATE +# models +{{#models}} +{{#model}} + model/{{classname}}.cpp + model/{{classname}}.h +{{/model}} +{{/models}} +# apis +{{#apiInfo}} +{{#apis}} +{{#operations}} + api/{{classname}}.cpp + api/{{classname}}.h +{{/operations}} +{{/apis}} +{{/apiInfo}} +# server runtime + server/Authorizer.h + server/BodyJson.h + server/HttpServer.cpp + server/HttpServer.h + server/ParamCodecs.h + server/Problem.h + server/Responder.h + server/Router.h +# shared model/validation support + model/AnyType.h + model/NullableField.h + model/Oas31DeepEqual.h + model/Oas31ExactNumber.cpp + model/Oas31ExactNumber.h + model/Oas31SchemaIr.h + model/Oas31ExactJson.h + model/Oas31Validator.h + model/ValidationTypes.h +{{#validateOnDecode}} + model/schema_ir.generated.cpp +{{#oas31SchemaIrChunkFiles}} + model/{{filename}} +{{/oas31SchemaIrChunkFiles}} + model/Oas31SchemaRegistry.h +{{/validateOnDecode}} +) + +target_link_libraries({{{packageName}}} + PUBLIC Boost::boost Boost::json Boost::url Threads::Threads) + +target_include_directories({{{packageName}}} PUBLIC + $ + $ + $ + $ + $ + $ + $ + $) + +install(TARGETS {{{packageName}}} + ARCHIVE DESTINATION "${CMAKE_INSTALL_LIBDIR}") + +install(DIRECTORY api model server + DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME}" + FILES_MATCHING PATTERN "*.h") + +{{#addApiImplStubs}} +add_executable(${PROJECT_NAME}_main main.cpp) +set_property(TARGET ${PROJECT_NAME}_main PROPERTY CXX_STANDARD 17) +set_property(TARGET ${PROJECT_NAME}_main PROPERTY CXX_STANDARD_REQUIRED ON) +set_property(TARGET ${PROJECT_NAME}_main PROPERTY CXX_EXTENSIONS OFF) +target_link_libraries(${PROJECT_NAME}_main PRIVATE {{{packageName}}}) +{{/addApiImplStubs}} diff --git a/modules/openapi-generator/src/main/resources/cpp-boost-beast-server/README.mustache b/modules/openapi-generator/src/main/resources/cpp-boost-beast-server/README.mustache new file mode 100644 index 000000000000..b8b37d5c175e --- /dev/null +++ b/modules/openapi-generator/src/main/resources/cpp-boost-beast-server/README.mustache @@ -0,0 +1,80 @@ +{{>licenseInfo}} +# {{packageName}} — Boost.Beast server + +{{#appDescription}} +{{{appDescription}}} +{{/appDescription}} + +Generated from an OpenAPI document by **openapi-generator** (`cpp-boost-beast-server`). + +## Requirements + +- C++17 compiler +- CMake ≥ 3.14 +- Boost ≥ 1.81 (headers, `json`, and URL — Beast/Asio are header-only) + +## Layout + +- `api/` — generated service interfaces, typed request structs, per-operation + responders, and route registration (`Api::attach`) +- `model/` — generated model types plus the shared OAS 3.1 exact-validation + runtime (`Oas31*`) and schema registry +- `server/` — HTTP/1.1 runtime: `HttpServer`, `Router`, `Responder`, + `Problem` (RFC 9457), `Authorizer`, parameter codecs, JSON body conversion + +## Building + +```sh +cmake -S . -B build +cmake --build build +``` + +## Using + +Implement the generated `Api` service interfaces and attach them: + +```cpp +namespace api = {{apiNamespace}}; +namespace model = {{modelNamespace}}; + +class MyDefaultApi : public api::DefaultApi { + void getPetById(api::GetPetByIdRequest request, + api::RequestContext& context, + api::GetPetByIdResponder responder) override { + model::Pet pet; + pet.setId(request.petId); + responder.send200(std::move(pet)); + } +}; + +int main() { + boost::asio::io_context ioc; + auto router = std::make_shared(); + api::ServerOptions options; + options.authorizer = std::make_shared(); + auto server = std::make_shared( + ioc, router, options); + api::DefaultApi::attach(*server, + std::make_shared()); + server->listen(boost::asio::ip::tcp::endpoint{ + boost::asio::ip::make_address("0.0.0.0"), 8080}); + ioc.run(); +} +``` + +Requests are fully decoded and schema-validated before the service method +runs; serialization failures, malformed input, unknown routes (404), wrong +methods (405 + `Allow`), unsupported media types (415), oversized bodies +(413), and security denials (401) produce RFC 9457 `application/problem+json` +responses without application code. + +With `addApiImplStubs=true` a `main.cpp` and stub services (501 responses) +are generated for quick start. + +## Security + +Declared OpenAPI security requirements are enforced before dispatch: +credentials are extracted per scheme (API keys by location, raw +`Authorization` for HTTP schemes) and handed to your `Authorizer`. Without +an authorizer, secured operations deny by default. Credential values are +never logged. diff --git a/modules/openapi-generator/src/main/resources/cpp-boost-beast-server/api-header.mustache b/modules/openapi-generator/src/main/resources/cpp-boost-beast-server/api-header.mustache new file mode 100644 index 000000000000..f5a49c89b1e5 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/cpp-boost-beast-server/api-header.mustache @@ -0,0 +1,138 @@ +{{>licenseInfo}} +{{#operations}}/* + * {{classname}}.h + * + * {{description}} + */ + +#ifndef {{apiHeaderGuardPrefix}}_{{classname}}_H_ +#define {{apiHeaderGuardPrefix}}_{{classname}}_H_ + +#include +#include +#include +#include +#include + +#include "HttpServer.h" +#include "Problem.h" +#include "Responder.h" +#include "Router.h" + +{{#imports}}{{{import}}} +{{/imports}} + +{{#apiNamespaceDeclarations}} +namespace {{this}} { +{{/apiNamespaceDeclarations}} + +{{#imports}} +using namespace {{modelNamespace}}; +{{/imports}} + +{{#operation}} +// --------------------------------------------------------------------------- + +/// Fully decoded request data for {{operationId}}. +struct {{vendorExtensions.x-server-operation-pascal}}Request { +{{#vendorExtensions.x-server-params}} + {{{dataType}}} {{cppName}}{}; +{{/vendorExtensions.x-server-params}} +{{#vendorExtensions.x-server-has-request-body}} + {{{vendorExtensions.x-server-request-model}}} body{}; +{{/vendorExtensions.x-server-has-request-body}} +}; + +/// Single-shot responder for {{operationId}}. Movable, thread-safe value +/// type; the second and later completions are ignored. +class {{vendorExtensions.x-server-operation-pascal}}Responder { +public: + explicit {{vendorExtensions.x-server-operation-pascal}}Responder( + std::shared_ptr core) + : core_(std::move(core)) {} + +{{#vendorExtensions.x-server-responses}} +{{#isDefault}} +{{#hasModel}} + void sendDefault({{{cppType}}} value, unsigned status) const { + core_->sendJson(status, value, "application/json"); + } +{{/hasModel}} +{{^hasModel}} + void sendDefault(unsigned status) const { + core_->sendEmpty(status); + } +{{/hasModel}} +{{/isDefault}} +{{^isDefault}} +{{#hasModel}} + void {{sendMethod}}({{{cppType}}} value) const { + core_->sendJson({{code}}, value, "application/json"); + } +{{/hasModel}} +{{^hasModel}} + void {{sendMethod}}() const { + core_->sendEmpty({{code}}); + } +{{/hasModel}} +{{/isDefault}} +{{/vendorExtensions.x-server-responses}} + + void sendProblem(Problem problem) const { + core_->sendProblem(std::move(problem)); + } + + void sendNotImplemented(std::string const& operationId) const { + core_->sendNotImplemented(operationId); + } + +private: + std::shared_ptr core_; +}; + +{{/operation}} +/** + * Service interface for {{description}}. Implementations receive fully + * decoded, validated requests and own their response completion. + */ +class {{classname}} { +public: + virtual ~{{classname}}() = default; + +{{#operation}} + virtual void {{nickname}}( + {{vendorExtensions.x-server-operation-pascal}}Request request, + RequestContext& context, + {{vendorExtensions.x-server-operation-pascal}}Responder responder) = 0; +{{/operation}} + + /// Registers every {{classname}} route on the server. + static void attach(HttpServer& server, std::shared_ptr<{{classname}}> impl); +}; + +{{#addApiImplStubs}} +/** + * Quick-start stub service: every operation answers 501 problem+json. + */ +class {{classname}}Stub : public {{classname}} { +public: +{{#operation}} + void {{nickname}}( + {{vendorExtensions.x-server-operation-pascal}}Request request, + RequestContext& context, + {{vendorExtensions.x-server-operation-pascal}}Responder responder) override { + (void)request; + (void)context; + responder.sendNotImplemented("{{operationId}}"); + } +{{/operation}} +}; +{{/addApiImplStubs}} + + +{{#apiNamespaceDeclarations}} +} +{{/apiNamespaceDeclarations}} + +#endif // {{apiHeaderGuardPrefix}}_{{classname}}_H_ +{{/operations}} diff --git a/modules/openapi-generator/src/main/resources/cpp-boost-beast-server/api-source.mustache b/modules/openapi-generator/src/main/resources/cpp-boost-beast-server/api-source.mustache new file mode 100644 index 000000000000..e55350edd07f --- /dev/null +++ b/modules/openapi-generator/src/main/resources/cpp-boost-beast-server/api-source.mustache @@ -0,0 +1,373 @@ +{{>licenseInfo}} +{{#operations}}/* + * {{classname}}.cpp + */ + +#include "{{classname}}.h" + +#include "BodyJson.h" +#include "ParamCodecs.h" +#include "Problem.h" +#include "Responder.h" +#include "Router.h" + +#include +#include +#include +#include +#include +#include + +{{#apiNamespaceDeclarations}} +namespace {{this}} { +{{/apiNamespaceDeclarations}} + +void {{classname}}::attach(HttpServer& server, std::shared_ptr<{{classname}}> impl) { + auto router = server.routerPtr(); + +{{#operation}} + // ------------------------------------------------------------------ + // {{httpMethod}} {{path}} ({{operationId}}) + // ------------------------------------------------------------------ + { + SecurityGroups security; +{{#vendorExtensions.x-server-security-groups}} + { + std::vector group; +{{#.}} + group.push_back(SchemeRequirement{ + "{{name}}", "{{type}}", "{{in}}", "{{paramName}}", "{{httpScheme}}"}); +{{/.}} + security.push_back(std::move(group)); + } +{{/vendorExtensions.x-server-security-groups}} + + router->add( + "{{httpMethod}}", + "{{path}}", + [impl](RequestContext& ctx, std::shared_ptr responderCore) { + {{vendorExtensions.x-server-operation-pascal}}Request request; + Problem problem; + bool invalid = false; + +{{#vendorExtensions.x-server-params}} + // ---- parameter {{baseName}} ({{in}}, {{style}}) ---- + { +{{#isPath}} +{{^isContainer}} + auto rawSegment = ctx.pathParams.find("{{baseName}}"); + std::string encoded = + rawSegment != ctx.pathParams.end() ? rawSegment->second : std::string(); + std::string text; +{{#styleSimple}} text = percentDecode(encoded); +{{/styleSimple}} +{{#styleLabel}} text = percentDecode(stripLabelPrefix(encoded)); +{{/styleLabel}} +{{#styleMatrix}} text = percentDecode(stripMatrixPrefix(encoded, "{{baseName}}")); +{{/styleMatrix}} + if (text.empty()) { + problem.withError("{{baseName}}", "path parameter is missing or empty"); + invalid = true; + } else if (!parseScalar(text, request.{{cppName}})) { + problem.withError("{{baseName}}", "path parameter is not a valid {{{dataType}}}"); + invalid = true; + } +{{#stringKind}}{{#hasEnum}} + if (!invalid) { + static std::vector const kAllowed = { {{#enumValues}}{{.}}, {{/enumValues}} }; + if (std::find(kAllowed.begin(), kAllowed.end(), request.{{cppName}}) == kAllowed.end()) { + problem.withError("{{baseName}}", "value is not one of the allowed enum members"); + invalid = true; + } + } +{{/hasEnum}}{{#hasPattern}} + if (!invalid) { + static std::regex const kPattern("{{{pattern}}}"); + if (!std::regex_match(request.{{cppName}}, kPattern)) { + problem.withError("{{baseName}}", "value does not match the required pattern"); + invalid = true; + } + } +{{/hasPattern}}{{#hasMinLength}} + if (!invalid && request.{{cppName}}.size() < {{minLength}}) { + problem.withError("{{baseName}}", "value is shorter than minLength"); + invalid = true; + } +{{/hasMinLength}}{{#hasMaxLength}} + if (!invalid && request.{{cppName}}.size() > {{maxLength}}) { + problem.withError("{{baseName}}", "value is longer than maxLength"); + invalid = true; + } +{{/hasMaxLength}}{{/stringKind}} +{{#integerKind}}{{#hasMinimum}} + if (!invalid && static_cast(request.{{cppName}}) < {{minimum}}L) { + problem.withError("{{baseName}}", "value is below the minimum"); + invalid = true; + } +{{/hasMinimum}}{{#hasMaximum}} + if (!invalid && static_cast(request.{{cppName}}) > {{maximum}}L) { + problem.withError("{{baseName}}", "value is above the maximum"); + invalid = true; + } +{{/hasMaximum}}{{/integerKind}} +{{#numberKind}}{{#hasMinimum}} + if (!invalid && static_cast(request.{{cppName}}) < {{minimum}}L) { + problem.withError("{{baseName}}", "value is below the minimum"); + invalid = true; + } +{{/hasMinimum}}{{#hasMaximum}} + if (!invalid && static_cast(request.{{cppName}}) > {{maximum}}L) { + problem.withError("{{baseName}}", "value is above the maximum"); + invalid = true; + } +{{/hasMaximum}}{{/numberKind}} +{{/isContainer}} +{{#isContainer}} + auto rawSegment = ctx.pathParams.find("{{baseName}}"); + std::string encoded = + rawSegment != ctx.pathParams.end() ? rawSegment->second : std::string(); + std::vector elements; +{{#styleSimple}} elements = splitSimple(encoded); +{{/styleSimple}} +{{#styleLabel}}{{#explode}} elements = splitOn(stripLabelPrefix(encoded), '.'); +{{/explode}}{{^explode}} elements = splitSimple(stripLabelPrefix(encoded)); +{{/explode}}{{/styleLabel}} +{{#styleMatrix}}{{#explode}} + elements = splitOn(stripMatrixPrefix(encoded, "{{baseName}}"), ';'); +{{/explode}}{{^explode}} + elements = splitSimple(stripMatrixPrefix(encoded, "{{baseName}}")); +{{/explode}}{{/styleMatrix}} + for (std::string& element : elements) { + {{{innerType}}} value; + if (!parseScalar(percentDecode(element), value)) { + problem.withError("{{baseName}}", "path parameter element is not a valid {{{innerType}}}"); + invalid = true; + break; + } + request.{{cppName}}.push_back(std::move(value)); + } +{{/isContainer}} +{{/isPath}} +{{#isQuery}} +{{^isContainer}} + auto values = ctx.query.equal_range("{{baseName}}"); + bool present = values.first != values.second; + if (!present) { +{{#required}} + problem.withError("{{baseName}}", "required query parameter is missing"); + invalid = true; +{{/required}} +{{^required}} + // absent optional query parameter keeps its default value +{{/required}} + } else if (!parseScalar(values.first->second, request.{{cppName}})) { + problem.withError("{{baseName}}", "query parameter is not a valid {{{dataType}}}"); + invalid = true; + } +{{#stringKind}}{{#hasEnum}} + if (!invalid && present) { + static std::vector const kAllowed = { {{#enumValues}}{{.}}, {{/enumValues}} }; + if (std::find(kAllowed.begin(), kAllowed.end(), request.{{cppName}}) == kAllowed.end()) { + problem.withError("{{baseName}}", "value is not one of the allowed enum members"); + invalid = true; + } + } +{{/hasEnum}}{{/stringKind}} +{{#stringKind}}{{#hasPattern}} + if (!invalid && present) { + static std::regex const kPattern("{{{pattern}}}"); + if (!std::regex_match(request.{{cppName}}, kPattern)) { + problem.withError("{{baseName}}", "value does not match the required pattern"); + invalid = true; + } + } +{{/hasPattern}}{{/stringKind}} +{{#integerKind}}{{#hasMinimum}} + if (!invalid && present && static_cast(request.{{cppName}}) < {{minimum}}L) { + problem.withError("{{baseName}}", "value is below the minimum"); + invalid = true; + } +{{/hasMinimum}}{{#hasMaximum}} + if (!invalid && present && static_cast(request.{{cppName}}) > {{maximum}}L) { + problem.withError("{{baseName}}", "value is above the maximum"); + invalid = true; + } +{{/hasMaximum}}{{/integerKind}} +{{#numberKind}}{{#hasMinimum}} + if (!invalid && present && static_cast(request.{{cppName}}) < {{minimum}}L) { + problem.withError("{{baseName}}", "value is below the minimum"); + invalid = true; + } +{{/hasMinimum}}{{#hasMaximum}} + if (!invalid && present && static_cast(request.{{cppName}}) > {{maximum}}L) { + problem.withError("{{baseName}}", "value is above the maximum"); + invalid = true; + } +{{/hasMaximum}}{{/numberKind}} +{{/isContainer}} +{{#isContainer}} +{{^styleDeepObject}} + auto values = ctx.query.equal_range("{{baseName}}"); + bool present = values.first != values.second; + std::vector elements; + if (present) { +{{#styleForm}}{{#explode}} + for (auto it = values.first; it != values.second; ++it) { + elements.push_back(it->second); + } +{{/explode}}{{^explode}} + elements = splitSimple(values.first->second); +{{/explode}}{{/styleForm}} +{{#stylePipeDelimited}} + elements = splitOn(values.first->second, '|'); +{{/stylePipeDelimited}} +{{#styleSpaceDelimited}} + elements = splitOn(values.first->second, ' '); +{{/styleSpaceDelimited}} + } + if (!present) { +{{#required}} + problem.withError("{{baseName}}", "required query parameter is missing"); + invalid = true; +{{/required}} +{{^required}} + // absent optional query parameter keeps its default value +{{/required}} + } else { + for (std::string& element : elements) { + {{{innerType}}} value; + if (!parseScalar(element, value)) { + problem.withError("{{baseName}}", "query parameter element is not a valid {{{innerType}}}"); + invalid = true; + break; + } + request.{{cppName}}.push_back(std::move(value)); + } + } +{{/styleDeepObject}} +{{#styleDeepObject}} + for (auto const& entry : ctx.query) { + std::string const prefix = "{{baseName}}["; + if (entry.first.size() > prefix.size() + && entry.first.compare(0, prefix.size(), prefix) == 0 + && entry.first.back() == ']') { + std::string key = entry.first.substr( + prefix.size(), entry.first.size() - prefix.size() - 1); + request.{{cppName}}[percentDecode(key)] = percentDecode(entry.second); + } + } +{{/styleDeepObject}} +{{/isContainer}} +{{/isQuery}} +{{#isHeader}} +{{^isContainer}} + auto values = ctx.headers.equal_range("{{baseName}}"); + bool present = values.first != values.second; + if (!present) { +{{#required}} + problem.withError("{{baseName}}", "required header parameter is missing"); + invalid = true; +{{/required}} +{{^required}} + // absent optional header parameter keeps its default value +{{/required}} + } else if (!parseScalar(values.first->second, request.{{cppName}})) { + problem.withError("{{baseName}}", "header parameter is not a valid {{{dataType}}}"); + invalid = true; + } +{{/isContainer}} +{{#isContainer}} + auto values = ctx.headers.equal_range("{{baseName}}"); + bool present = values.first != values.second; + if (!present) { +{{#required}} + problem.withError("{{baseName}}", "required header parameter is missing"); + invalid = true; +{{/required}} + } else { + for (std::string& element : splitSimple(values.first->second)) { + {{{innerType}}} value; + if (!parseScalar(element, value)) { + problem.withError("{{baseName}}", "header parameter element is not a valid {{{innerType}}}"); + invalid = true; + break; + } + request.{{cppName}}.push_back(std::move(value)); + } + } +{{/isContainer}} +{{/isHeader}} +{{#isCookie}} + auto values = ctx.cookies.equal_range("{{baseName}}"); + bool present = values.first != values.second; + if (!present) { +{{#required}} + problem.withError("{{baseName}}", "required cookie parameter is missing"); + invalid = true; +{{/required}} +{{^required}} + // absent optional cookie parameter keeps its default value +{{/required}} + } else if (!parseScalar(values.first->second, request.{{cppName}})) { + problem.withError("{{baseName}}", "cookie parameter is not a valid {{{dataType}}}"); + invalid = true; + } +{{/isCookie}} + } +{{/vendorExtensions.x-server-params}} + +{{#vendorExtensions.x-server-has-request-body}} + // ---- request body ({{vendorExtensions.x-server-request-model}}) ---- + { + auto contentTypeEntry = ctx.headers.find("content-type"); + std::string contentType = + contentTypeEntry != ctx.headers.end() + ? contentTypeEntry->second : std::string(); + std::size_t semicolon = contentType.find(';'); + if (semicolon != std::string::npos) { + contentType.resize(semicolon); + } + while (!contentType.empty() && contentType.back() == ' ') { + contentType.pop_back(); + } + static std::vector const kMediaTypes = { + {{{vendorExtensions.x-server-request-media-types}}} }; + bool supported = !contentType.empty() + ? std::find(kMediaTypes.begin(), kMediaTypes.end(), contentType) != kMediaTypes.end() + || std::find(kMediaTypes.begin(), kMediaTypes.end(), "*/*") != kMediaTypes.end() + : kMediaTypes.size() == 1; + if (!supported) { + responderCore->sendProblem(Problem::unsupportedMediaType(contentType)); + return; + } + try { + fromJsonBody(ctx.body, request.body); + } catch (std::invalid_argument const& error) { + Problem parseProblem = Problem::badRequest(error.what()); + parseProblem.withError("body", error.what()); + responderCore->sendProblem(std::move(parseProblem)); + return; + } + } +{{/vendorExtensions.x-server-has-request-body}} + + if (invalid) { + problem.status = 400; + problem.title = "Bad Request"; + responderCore->sendProblem(std::move(problem)); + return; + } + + {{vendorExtensions.x-server-operation-pascal}}Responder responder(responderCore); + impl->{{nickname}}(std::move(request), ctx, std::move(responder)); + }, + std::move(security), + "{{operationId}}"); + } +{{/operation}} +} + +{{#apiNamespaceDeclarations}} +} +{{/apiNamespaceDeclarations}} +{{/operations}} diff --git a/modules/openapi-generator/src/main/resources/cpp-boost-beast-server/authorizer-header.mustache b/modules/openapi-generator/src/main/resources/cpp-boost-beast-server/authorizer-header.mustache new file mode 100644 index 000000000000..eeaf8474b34c --- /dev/null +++ b/modules/openapi-generator/src/main/resources/cpp-boost-beast-server/authorizer-header.mustache @@ -0,0 +1,37 @@ +{{>licenseInfo}} +// ============================================================================ +// Authorizer.h - security enforcement seam. The runtime extracts credentials +// from the request per the operation's declared security schemes; verifying +// them is the application's job. Deny by default when no authorizer is set. +// ============================================================================ +#ifndef {{apiHeaderGuardPrefix}}_SERVER_AUTHORIZER_H_ +#define {{apiHeaderGuardPrefix}}_SERVER_AUTHORIZER_H_ + +#include +#include + +namespace {{apiNamespace}} { + +/// Credentials extracted from an incoming request, keyed by scheme name. +struct AuthCredentials { + /// API-key values by declared parameter location and name + /// ("header:X-API-KEY", "query:token", "cookie:session"). + std::map apiKeyValues; + /// Raw Authorization header value (empty when absent). Never logged. + std::string httpAuthorization; +}; + +/// Application-provided authorization decision point. +class Authorizer { +public: + virtual ~Authorizer() = default; + + /// Return true to allow the operation to proceed. Called only after the + /// request structurally satisfied at least one declared security group. + virtual bool authorize(std::string const& operationId, + AuthCredentials const& credentials) = 0; +}; + +} // namespace {{apiNamespace}} + +#endif // {{apiHeaderGuardPrefix}}_SERVER_AUTHORIZER_H_ diff --git a/modules/openapi-generator/src/main/resources/cpp-boost-beast-server/body-json-header.mustache b/modules/openapi-generator/src/main/resources/cpp-boost-beast-server/body-json-header.mustache new file mode 100644 index 000000000000..0d3ebf6f192a --- /dev/null +++ b/modules/openapi-generator/src/main/resources/cpp-boost-beast-server/body-json-header.mustache @@ -0,0 +1,245 @@ +{{>licenseInfo}} +// ============================================================================ +// BodyJson.h - generic typed body conversion between generated model types +// and JSON bodies. Uses the generated models' toJsonValue/fromJsonValue +// member API, so models, containers, maps, shared_ptrs, and primitives all +// convert without per-operation code. Header-only. +// ============================================================================ +#ifndef {{apiHeaderGuardPrefix}}_SERVER_BODY_JSON_H_ +#define {{apiHeaderGuardPrefix}}_SERVER_BODY_JSON_H_ + +#include + +#include +#include +#include +#include +#include +#include + +namespace {{apiNamespace}} { + +template +struct HasToJsonValue : std::false_type {}; +template +struct HasToJsonValue().toJsonValue())>> : std::true_type {}; + +template +struct HasFromJsonValue : std::false_type {}; +template +struct HasFromJsonValue().fromJsonValue(std::declval()))>> + : std::true_type {}; + +// --------------------------------------------------------------------------- +// Serialization: typed value -> JSON body string. +// --------------------------------------------------------------------------- + +inline boost::json::value bodyLeaf(std::string const& value) { + return boost::json::value(value); +} + +inline boost::json::value bodyLeaf(const char* value) { + return boost::json::value(std::string(value == nullptr ? "" : value)); +} + +inline boost::json::value bodyLeaf(bool value) { + return boost::json::value(value); +} + +inline boost::json::value bodyLeaf(std::int32_t value) { + return boost::json::value(value); +} + +inline boost::json::value bodyLeaf(std::int64_t value) { + return boost::json::value(value); +} + +inline boost::json::value bodyLeaf(float value) { + return boost::json::value(static_cast(value)); +} + +inline boost::json::value bodyLeaf(double value) { + return boost::json::value(value); +} + +template +std::enable_if_t::value, boost::json::value> +bodyLeaf(T const& value) { + return value.toJsonValue(); +} + +template +boost::json::value bodyLeaf(std::shared_ptr const& value) { + if (!value) return boost::json::value(nullptr); + return bodyLeaf(*value); +} + +template +boost::json::value bodyLeaf(std::vector const& values) { + boost::json::array array; + array.reserve(values.size()); + for (T const& value : values) { + array.push_back(bodyLeaf(value)); + } + return array; +} + +template +boost::json::value bodyLeaf(std::map const& values) { + boost::json::object object; + for (auto const& entry : values) { + object[entry.first] = bodyLeaf(entry.second); + } + return object; +} + +template +std::string toJsonBody(T const& value) { + return boost::json::serialize(bodyLeaf(value)); +} + +// --------------------------------------------------------------------------- +// Deserialization: parsed JSON document -> typed value. Throws +// std::invalid_argument on shape mismatches. +// --------------------------------------------------------------------------- + +inline void fromJsonLeaf(boost::json::value const& json, std::string& out) { + if (!json.is_string()) { + throw std::invalid_argument("expected a JSON string"); + } + out.assign(json.as_string().data(), json.as_string().size()); +} + +inline void fromJsonLeaf(boost::json::value const& json, bool& out) { + if (!json.is_bool()) { + throw std::invalid_argument("expected a JSON boolean"); + } + out = json.as_bool(); +} + +inline void fromJsonLeaf(boost::json::value const& json, std::int32_t& out) { + if (json.is_int64()) { + std::int64_t value = json.as_int64(); + if (value < std::numeric_limits::min() + || value > std::numeric_limits::max()) { + throw std::invalid_argument("integer out of int32 range"); + } + out = static_cast(value); + return; + } + if (json.is_uint64()) { + std::uint64_t value = json.as_uint64(); + if (value > static_cast( + std::numeric_limits::max())) { + throw std::invalid_argument("integer out of int32 range"); + } + out = static_cast(value); + return; + } + throw std::invalid_argument("expected a JSON integer"); +} + +inline void fromJsonLeaf(boost::json::value const& json, std::int64_t& out) { + if (json.is_int64()) { + out = json.as_int64(); + return; + } + if (json.is_uint64()) { + std::uint64_t value = json.as_uint64(); + if (value > static_cast( + std::numeric_limits::max())) { + throw std::invalid_argument("integer out of int64 range"); + } + out = static_cast(value); + return; + } + throw std::invalid_argument("expected a JSON integer"); +} + +inline void fromJsonLeaf(boost::json::value const& json, float& out) { + if (json.is_double()) { + out = static_cast(json.as_double()); + return; + } + if (json.is_int64() || json.is_uint64()) { + out = static_cast(json.to_number()); + return; + } + throw std::invalid_argument("expected a JSON number"); +} + +inline void fromJsonLeaf(boost::json::value const& json, double& out) { + if (json.is_double()) { + out = json.as_double(); + return; + } + if (json.is_int64() || json.is_uint64()) { + out = static_cast(json.to_number()); + return; + } + throw std::invalid_argument("expected a JSON number"); +} + +template +std::enable_if_t::value, void> +fromJsonLeaf(boost::json::value const& json, T& out) { + out.fromJsonValue(json); +} + +template +void fromJsonLeaf(boost::json::value const& json, std::shared_ptr& out) { + if (json.is_null()) { + out.reset(); + return; + } + if (!out) { + out = std::make_shared(); + } + fromJsonLeaf(json, *out); +} + +template +void fromJsonLeaf(boost::json::value const& json, std::vector& out) { + if (!json.is_array()) { + throw std::invalid_argument("expected a JSON array"); + } + out.clear(); + out.reserve(json.as_array().size()); + for (boost::json::value const& element : json.as_array()) { + T value; + fromJsonLeaf(element, value); + out.push_back(std::move(value)); + } +} + +template +void fromJsonLeaf(boost::json::value const& json, std::map& out) { + if (!json.is_object()) { + throw std::invalid_argument("expected a JSON object"); + } + out.clear(); + for (auto const& member : json.as_object()) { + T value; + fromJsonLeaf(member.value(), value); + out.emplace(std::string(member.key().data(), member.key().size()), + std::move(value)); + } +} + +/// Parses a JSON request body and decodes it into a typed value. +/// Throws std::invalid_argument on malformed JSON or shape mismatch. +template +void fromJsonBody(std::string const& body, T& out) { + boost::system::error_code error; + boost::json::value parsed = boost::json::parse(body, error); + if (error) { + throw std::invalid_argument("malformed JSON body: " + error.message()); + } + fromJsonLeaf(parsed, out); +} + +} // namespace {{apiNamespace}} + +#endif // {{apiHeaderGuardPrefix}}_SERVER_BODY_JSON_H_ diff --git a/modules/openapi-generator/src/main/resources/cpp-boost-beast-server/http-server-header.mustache b/modules/openapi-generator/src/main/resources/cpp-boost-beast-server/http-server-header.mustache new file mode 100644 index 000000000000..a6201487ba65 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/cpp-boost-beast-server/http-server-header.mustache @@ -0,0 +1,59 @@ +{{>licenseInfo}} +// ============================================================================ +// HttpServer.h - asynchronous HTTP/1.1 listener and connection sessions on +// Boost.Beast + Boost.Asio. The caller owns the io_context; the server owns +// its acceptor and stops gracefully on demand. +// ============================================================================ +#ifndef {{apiHeaderGuardPrefix}}_SERVER_HTTP_SERVER_H_ +#define {{apiHeaderGuardPrefix}}_SERVER_HTTP_SERVER_H_ + +#include "Authorizer.h" +#include "Router.h" + +#include +#include + +#include +#include +#include + +namespace {{apiNamespace}} { + +struct ServerOptions { + unsigned readTimeoutSeconds = 30; + std::size_t bodyLimitBytes = 8u * 1024 * 1024; + std::shared_ptr authorizer; +}; + +class HttpServer : public std::enable_shared_from_this { +public: + HttpServer(boost::asio::io_context& ioc, + std::shared_ptr router, + ServerOptions options = {}); + + /// Opens, binds (SO_REUSEADDR), and listens. Throws on failure. + void listen(boost::asio::ip::tcp::endpoint endpoint); + + /// The bound endpoint (useful after listening on port 0). + boost::asio::ip::tcp::endpoint localEndpoint() const; + + /// Graceful stop: closes the acceptor. Open sessions finish their + /// current exchange or terminate with the io_context. + void stop(); + + Router& router() { return *router_; } + std::shared_ptr routerPtr() const { return router_; } + +private: + struct ListenerState; + void acceptNext(); + + boost::asio::io_context& ioc_; + std::shared_ptr router_; + ServerOptions options_; + std::shared_ptr listener_; +}; + +} // namespace {{apiNamespace}} + +#endif // {{apiHeaderGuardPrefix}}_SERVER_HTTP_SERVER_H_ diff --git a/modules/openapi-generator/src/main/resources/cpp-boost-beast-server/http-server-source.mustache b/modules/openapi-generator/src/main/resources/cpp-boost-beast-server/http-server-source.mustache new file mode 100644 index 000000000000..532e72345791 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/cpp-boost-beast-server/http-server-source.mustache @@ -0,0 +1,356 @@ +{{>licenseInfo}} +// ============================================================================ +// HttpServer.cpp - listener, session lifecycle, request decoding, security +// enforcement, and response writing. This is the single translation unit +// that compiles Boost.URL (header-only mode). +// ============================================================================ +#include "HttpServer.h" +#include "ParamCodecs.h" +#include "Problem.h" +#include "Responder.h" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace {{apiNamespace}} { + +namespace { + +namespace http = boost::beast::http; +namespace net = boost::asio; +using tcp = boost::asio::ip::tcp; + +void failLog(boost::beast::error_code ec, char const* what) { + std::cerr << "cpp-boost-beast-server " << what << ": " << ec.message() << "\n"; +} + +std::string lowercased(std::string text) { + std::transform(text.begin(), text.end(), text.begin(), + [](unsigned char c) { + return static_cast(std::tolower(c)); + }); + return text; +} + +/// Extracts credentials per the route's declared schemes. +AuthCredentials collectCredentials( + RequestContext const& ctx, SecurityGroups const& groups) { + AuthCredentials credentials; + for (std::vector const& group : groups) { + for (SchemeRequirement const& scheme : group) { + if (scheme.type == "apiKey") { + std::string key = scheme.in + ":" + scheme.paramName; + if (credentials.apiKeyValues.count(key) != 0) { + continue; + } + if (scheme.in == "header") { + auto range = ctx.headers.equal_range( + lowercased(scheme.paramName)); + for (auto it = range.first; it != range.second; ++it) { + if (!it->second.empty()) { + credentials.apiKeyValues.emplace(key, it->second); + break; + } + } + } else if (scheme.in == "query") { + auto range = ctx.query.equal_range(scheme.paramName); + for (auto it = range.first; it != range.second; ++it) { + if (!it->second.empty()) { + credentials.apiKeyValues.emplace(key, it->second); + break; + } + } + } else if (scheme.in == "cookie") { + auto range = ctx.cookies.equal_range(scheme.paramName); + for (auto it = range.first; it != range.second; ++it) { + if (!it->second.empty()) { + credentials.apiKeyValues.emplace(key, it->second); + break; + } + } + } + } else if (scheme.type == "http") { + auto range = ctx.headers.equal_range("authorization"); + for (auto it = range.first; it != range.second; ++it) { + if (!it->second.empty()) { + credentials.httpAuthorization = it->second; + break; + } + } + } + } + } + return credentials; +} + +/// True when at least one OR-alternative is structurally satisfied. +bool structurallySatisfied( + SecurityGroups const& groups, AuthCredentials const& credentials) { + for (std::vector const& group : groups) { + if (group.empty()) { + return true; // anonymous alternative + } + bool allPresent = true; + for (SchemeRequirement const& scheme : group) { + if (scheme.type == "apiKey") { + if (credentials.apiKeyValues.count( + scheme.in + ":" + scheme.paramName) == 0) { + allPresent = false; + break; + } + } else if (scheme.type == "http") { + if (credentials.httpAuthorization.empty()) { + allPresent = false; + break; + } + } else { + allPresent = false; + break; + } + } + if (allPresent) { + return true; + } + } + return false; +} + +// --------------------------------------------------------------------------- +// session - one HTTP/1.1 connection. +// --------------------------------------------------------------------------- +class session : public std::enable_shared_from_this { +public: + session(tcp::socket&& socket, + std::shared_ptr router, + ServerOptions options) + : stream_(std::move(socket)) + , router_(std::move(router)) + , options_(std::move(options)) {} + + void run() { + net::dispatch(stream_.get_executor(), + boost::beast::bind_front_handler( + &session::do_read, shared_from_this())); + } + +private: + void do_read() { + stream_.expires_after( + std::chrono::seconds(options_.readTimeoutSeconds)); + parser_.emplace(); + parser_->body_limit(options_.bodyLimitBytes); + http::async_read( + stream_, buffer_, *parser_, + boost::beast::bind_front_handler( + &session::on_read, shared_from_this())); + } + + void on_read(boost::beast::error_code ec, std::size_t) { + if (ec == http::error::end_of_stream) { + return do_close(); + } + if (ec == http::error::body_limit) { + return send_response( + toProblemResponse(Problem::payloadTooLarge())); + } + if (ec) { + return failLog(ec, "read"); + } + handle_request(parser_->release()); + } + + void handle_request(http::request&& request) { + RequestContext ctx; + ctx.method = std::string(request.method_string()); + ctx.target = std::string(request.target()); + ctx.body = request.body(); + + boost::system::result parsedUrl = + boost::urls::parse_origin_form(request.target()); + if (!parsedUrl.has_value()) { + return send_response(toProblemResponse( + Problem::badRequest("malformed request target"))); + } + for (auto const& param : parsedUrl->params()) { + std::string key(param.key.data(), param.key.size()); + std::string value(param.value.data(), param.value.size()); + ctx.query.emplace(std::move(key), std::move(value)); + } + + for (auto const& field : request) { + std::string name = lowercased(std::string(field.name_string())); + std::string value(field.value().data(), field.value().size()); + if (name == "cookie") { + parseCookieHeader(value, ctx.cookies); + } + ctx.headers.emplace(std::move(name), std::move(value)); + } + + RouteMatch match = router_->match(ctx.method, ctx.target); + if (!match.handler) { + std::string allowed = router_->allowedMethods(ctx.target); + if (!allowed.empty()) { + http::response res = + toProblemResponse(Problem::methodNotAllowed(allowed)); + res.set(http::field::allow, allowed); + return send_response(std::move(res)); + } + return send_response(toProblemResponse( + Problem::notFound(std::string(request.target())))); + } + + ctx.pathParams = match.pathParams; + ctx.operationId = match.operationId; + + if (!match.security.empty()) { + AuthCredentials credentials = + collectCredentials(ctx, match.security); + bool allowed = structurallySatisfied(match.security, credentials) + && options_.authorizer + && options_.authorizer->authorize(ctx.operationId, credentials); + if (!allowed) { + return send_response( + toProblemResponse(Problem::unauthorized())); + } + } + + auto responder = std::make_shared( + [self = shared_from_this()]( + http::response&& response) { + net::post(self->stream_.get_executor(), + [self, response = std::move(response)]() mutable { + self->send_response(std::move(response)); + }); + }); + responder->setOperationId(ctx.operationId); + try { + match.handler(ctx, responder); + } catch (std::exception const& error) { + std::cerr << "cpp-boost-beast-server: handler exception for " + << ctx.operationId << ": " << error.what() << "\n"; + if (!responder->completed()) { + send_response(toProblemResponse(Problem::internal())); + } + } + } + + + void send_response(http::response&& response) { + bool keepAlive = response.keep_alive(); + auto message = std::make_shared( + std::move(response)); + boost::beast::async_write( + stream_, std::move(*message), + [self = shared_from_this(), message, keepAlive]( + boost::beast::error_code ec, std::size_t) { + self->on_write(keepAlive, message, ec, 0); + }); + } + + void on_write(bool keepAlive, + std::shared_ptr message, + boost::beast::error_code ec, + std::size_t) { + boost::ignore_unused(message); + if (ec) { + return failLog(ec, "write"); + } + if (!keepAlive) { + return do_close(); + } + do_read(); + } + + void do_close() { + boost::beast::error_code ec; + stream_.socket().shutdown(tcp::socket::shutdown_send, ec); + } + + boost::beast::tcp_stream stream_; + boost::beast::flat_buffer buffer_; + std::shared_ptr router_; + ServerOptions options_; + std::optional> parser_; +}; + +} // namespace + +// --------------------------------------------------------------------------- +// HttpServer +// --------------------------------------------------------------------------- + +struct HttpServer::ListenerState { + tcp::acceptor acceptor; + + explicit ListenerState(net::io_context& ioc) + : acceptor(net::make_strand(ioc)) {} +}; + +HttpServer::HttpServer(net::io_context& ioc, + std::shared_ptr router, + ServerOptions options) + : ioc_(ioc) + , router_(std::move(router)) + , options_(std::move(options)) + , listener_(std::make_shared(ioc)) {} + +void HttpServer::listen(tcp::endpoint endpoint) { + boost::beast::error_code ec; + auto& acceptor = listener_->acceptor; + acceptor.open(endpoint.protocol(), ec); + if (ec) { + throw std::runtime_error("open: " + ec.message()); + } + acceptor.set_option(net::socket_base::reuse_address(true), ec); + if (ec) { + throw std::runtime_error("set_option: " + ec.message()); + } + acceptor.bind(endpoint, ec); + if (ec) { + throw std::runtime_error("bind: " + ec.message()); + } + acceptor.listen(net::socket_base::max_listen_connections, ec); + if (ec) { + throw std::runtime_error("listen: " + ec.message()); + } + acceptNext(); +} + +tcp::endpoint HttpServer::localEndpoint() const { + return listener_->acceptor.local_endpoint(); +} + +void HttpServer::stop() { + boost::beast::error_code ec; + listener_->acceptor.close(ec); +} + +void HttpServer::acceptNext() { + auto self = shared_from_this(); + listener_->acceptor.async_accept( + net::make_strand(ioc_), + [self](boost::beast::error_code ec, tcp::socket socket) { + if (ec == net::error::operation_aborted) { + return; + } + if (ec) { + return failLog(ec, "accept"); + } + std::make_shared( + std::move(socket), self->router_, self->options_)->run(); + self->acceptNext(); + }); +} + +} // namespace {{apiNamespace}} diff --git a/modules/openapi-generator/src/main/resources/cpp-boost-beast-server/licenseInfo.mustache b/modules/openapi-generator/src/main/resources/cpp-boost-beast-server/licenseInfo.mustache new file mode 100644 index 000000000000..e737bfd822a0 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/cpp-boost-beast-server/licenseInfo.mustache @@ -0,0 +1,11 @@ +/** + * {{{appName}}} + * {{{appDescription}}} + * + * {{#version}}The version of the OpenAPI document: {{{.}}}{{/version}} +{{#infoEmail}} * Contact: {{{.}}} +{{/infoEmail}} * + * NOTE: This class is auto generated by OpenAPI-Generator {{{generatorVersion}}}. + * https://openapi-generator.tech + * Do not edit the class manually. + */ diff --git a/modules/openapi-generator/src/main/resources/cpp-boost-beast-server/main.mustache b/modules/openapi-generator/src/main/resources/cpp-boost-beast-server/main.mustache new file mode 100644 index 000000000000..d5f77a74744c --- /dev/null +++ b/modules/openapi-generator/src/main/resources/cpp-boost-beast-server/main.mustache @@ -0,0 +1,64 @@ +{{>licenseInfo}} +// ============================================================================ +// main.cpp - quick-start server entry point (generated with +// addApiImplStubs=true). Every operation answers 501 problem+json until you +// provide a real service implementation. +// ============================================================================ +#include + +#include +#include +#include + +#include "HttpServer.h" +#include "Router.h" + +{{#apiInfo}} +{{#apis}} +{{#operations}} +#include "{{classname}}.h" +{{/operations}} +{{/apis}} +{{/apiInfo}} +{{#apiNamespaceDeclarations}} +using namespace {{this}}; +{{/apiNamespaceDeclarations}} + +{{#apiInfo}} +{{#apis}} +{{#operations}} +static void attach{{classname}}(HttpServer& server) { + {{classname}}::attach(server, std::make_shared<{{classname}}Stub>()); +} +{{/operations}} +{{/apis}} +{{/apiInfo}} + +int main() { + unsigned port = 8080; + if (char const* portText = std::getenv("PORT")) { + port = static_cast(std::strtoul(portText, nullptr, 10)); + } + + try { + boost::asio::io_context ioc; + auto router = std::make_shared(); + auto server = std::make_shared(ioc, router); +{{#apiInfo}} +{{#apis}} +{{#operations}} + attach{{classname}}(*server); +{{/operations}} +{{/apis}} +{{/apiInfo}} + server->listen(boost::asio::ip::tcp::endpoint{ + boost::asio::ip::make_address("0.0.0.0"), + static_cast(port)}); + std::cout << "{{packageName}} listening on 0.0.0.0:" << port << "\n"; + ioc.run(); + } catch (std::exception const& error) { + std::cerr << "fatal: " << error.what() << "\n"; + return EXIT_FAILURE; + } + return EXIT_SUCCESS; +} diff --git a/modules/openapi-generator/src/main/resources/cpp-boost-beast-server/param-codecs-header.mustache b/modules/openapi-generator/src/main/resources/cpp-boost-beast-server/param-codecs-header.mustache new file mode 100644 index 000000000000..8010c1220a8c --- /dev/null +++ b/modules/openapi-generator/src/main/resources/cpp-boost-beast-server/param-codecs-header.mustache @@ -0,0 +1,191 @@ +{{>licenseInfo}} +// ============================================================================ +// ParamCodecs.h - OAS parameter deserialization primitives: percent +// decoding, strict scalar parsing, simple/label/matrix and form-style +// splitting, and cookie parsing. Header-only. +// ============================================================================ +#ifndef {{apiHeaderGuardPrefix}}_SERVER_PARAM_CODECS_H_ +#define {{apiHeaderGuardPrefix}}_SERVER_PARAM_CODECS_H_ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace {{apiNamespace}} { + +/// Percent-decodes a URI component (%XX sequences). Invalid escapes pass +/// through unchanged; '+' is NOT translated to space (that is form encoding). +inline std::string percentDecode(std::string_view encoded) { + std::string out; + out.reserve(encoded.size()); + for (std::size_t i = 0; i < encoded.size(); ++i) { + if (encoded[i] == '%' && i + 2 < encoded.size()) { + auto hexValue = [](char c) -> int { + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'a' && c <= 'f') return c - 'a' + 10; + if (c >= 'A' && c <= 'F') return c - 'A' + 10; + return -1; + }; + int high = hexValue(encoded[i + 1]); + int low = hexValue(encoded[i + 2]); + if (high >= 0 && low >= 0) { + out.push_back(static_cast((high << 4) | low)); + i += 2; + continue; + } + } + out.push_back(encoded[i]); + } + return out; +} + +/// Splits a simple-style (comma-delimited) list into raw elements. +inline std::vector splitSimple(std::string_view value) { + std::vector parts; + std::size_t start = 0; + while (true) { + std::size_t comma = value.find(',', start); + if (comma == std::string_view::npos) { + parts.emplace_back(value.substr(start)); + break; + } + parts.emplace_back(value.substr(start, comma - start)); + start = comma + 1; + } + return parts; +} + +/// Splits on an arbitrary single-character delimiter (pipe/space styles). +inline std::vector splitOn(std::string_view value, char delimiter) { + std::vector parts; + std::size_t start = 0; + while (true) { + std::size_t hit = value.find(delimiter, start); + if (hit == std::string_view::npos) { + parts.emplace_back(value.substr(start)); + break; + } + parts.emplace_back(value.substr(start, hit - start)); + start = hit + 1; + } + return parts; +} + +/// Strips a label-style "." prefix from one raw path-segment value. +inline std::string stripLabelPrefix(std::string_view segment) { + if (!segment.empty() && segment.front() == '.') { + segment.remove_prefix(1); + } + return std::string(segment); +} + +/// Strips a matrix-style ";name=" prefix from one raw path-segment value. +inline std::string stripMatrixPrefix(std::string_view segment, std::string const& name) { + if (!segment.empty() && segment.front() == ';') { + segment.remove_prefix(1); + std::string prefix = name + "="; + if (segment.substr(0, prefix.size()) == prefix) { + segment.remove_prefix(prefix.size()); + } + } + return std::string(segment); +} + +/// Parses a Cookie header value ("k=v; k2=v2") into decoded pairs. +inline void parseCookieHeader( + std::string_view header, + std::multimap& out) { + std::size_t start = 0; + while (start < header.size()) { + std::size_t semi = header.find(';', start); + std::string_view pair = semi == std::string_view::npos + ? header.substr(start) : header.substr(start, semi - start); + std::size_t eq = pair.find('='); + if (eq != std::string_view::npos) { + std::string key = percentDecode(pair.substr(0, eq)); + while (!key.empty() && (key.back() == ' ' || key.back() == '\t')) { + key.pop_back(); + } + std::string value = percentDecode(pair.substr(eq + 1)); + while (!value.empty() && value.front() == ' ') { + value.erase(value.begin()); + } + if (!key.empty()) { + out.emplace(std::move(key), std::move(value)); + } + } + if (semi == std::string_view::npos) { + break; + } + start = semi + 1; + } +} + +// --------------------------------------------------------------------------- +// Strict scalar parsing: whole-input match, range-checked, no trailing junk. +// --------------------------------------------------------------------------- + +inline bool parseScalar(std::string_view text, std::string& out) { + out.assign(text); + return true; +} + +inline bool parseScalar(std::string_view text, bool& out) { + if (text == "true") { out = true; return true; } + if (text == "false") { out = false; return true; } + return false; +} + +template +inline bool parseIntegerScalar(std::string_view text, T& out) { + if (text.empty()) return false; + std::string storage(text); + char* end = nullptr; + errno = 0; + long long parsed = std::strtoll(storage.c_str(), &end, 10); + if (end == nullptr || *end != '\0' || errno == ERANGE) return false; + if (parsed < static_cast(std::numeric_limits::min()) + || parsed > static_cast(std::numeric_limits::max())) { + return false; + } + out = static_cast(parsed); + return true; +} + +inline bool parseScalar(std::string_view text, std::int32_t& out) { + return parseIntegerScalar(text, out); +} + +inline bool parseScalar(std::string_view text, std::int64_t& out) { + return parseIntegerScalar(text, out); +} + +template +inline bool parseFloatScalar(std::string_view text, T& out) { + if (text.empty()) return false; + std::string storage(text); + char* end = nullptr; + errno = 0; + double parsed = std::strtod(storage.c_str(), &end); + if (end == nullptr || *end != '\0' || errno == ERANGE) return false; + out = static_cast(parsed); + return true; +} + +inline bool parseScalar(std::string_view text, float& out) { + return parseFloatScalar(text, out); +} + +inline bool parseScalar(std::string_view text, double& out) { + return parseFloatScalar(text, out); +} + +} // namespace {{apiNamespace}} + +#endif // {{apiHeaderGuardPrefix}}_SERVER_PARAM_CODECS_H_ diff --git a/modules/openapi-generator/src/main/resources/cpp-boost-beast-server/problem-header.mustache b/modules/openapi-generator/src/main/resources/cpp-boost-beast-server/problem-header.mustache new file mode 100644 index 000000000000..b232e06200a7 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/cpp-boost-beast-server/problem-header.mustache @@ -0,0 +1,163 @@ +{{>licenseInfo}} +// ============================================================================ +// Problem.h - RFC 9457 problem details responses. +// ============================================================================ +#ifndef {{apiHeaderGuardPrefix}}_SERVER_PROBLEM_H_ +#define {{apiHeaderGuardPrefix}}_SERVER_PROBLEM_H_ + +#include + +#include +#include +#include +#include +#include + +namespace {{apiNamespace}} { + +/// One validation error occurrence: a location pointer plus a message. +struct ProblemError { + std::string path; + std::string message; +}; +/// RFC 9457 problem details object. +struct Problem { + unsigned status = 500; + std::string type = "about:blank"; + std::string title; + std::string detail; + std::string instance; + std::vector errors; + + Problem& withError(std::string path, std::string message) { + errors.push_back(ProblemError{std::move(path), std::move(message)}); + return *this; + } + + static Problem badRequest(std::string detail) { + Problem p; + p.status = 400; + p.title = "Bad Request"; + p.detail = std::move(detail); + return p; + } + + static Problem unauthorized() { + Problem p; + p.status = 401; + p.title = "Unauthorized"; + p.detail = "Missing or invalid credentials"; + return p; + } + + static Problem notFound(std::string const& target) { + Problem p; + p.status = 404; + p.title = "Not Found"; + p.detail = "The resource '" + target + "' was not found."; + p.instance = target; + return p; + } + + static Problem methodNotAllowed(std::string const& allow) { + Problem p; + p.status = 405; + p.title = "Method Not Allowed"; + p.detail = "Allowed methods: " + allow; + return p; + } + + static Problem unsupportedMediaType(std::string const& received) { + Problem p; + p.status = 415; + p.title = "Unsupported Media Type"; + p.detail = "Content-Type '" + received + "' is not supported"; + return p; + } + + static Problem payloadTooLarge() { + Problem p; + p.status = 413; + p.title = "Content Too Large"; + p.detail = "Request body exceeds the configured limit"; + return p; + } + + static Problem internal() { + Problem p; + p.status = 500; + p.title = "Internal Server Error"; + return p; + } + + static Problem notImplemented(std::string const& operationId) { + Problem p; + p.status = 501; + p.title = "Not Implemented"; + p.detail = "Operation '" + operationId + "' has no implementation"; + return p; + } +}; + +/// Serializes a problem as an HTTP response with application/problem+json. +inline boost::beast::http::response +toProblemResponse(Problem const& problem) { + namespace http = boost::beast::http; + auto jsonEscape = [](std::string const& text) { + std::string out; + out.reserve(text.size()); + for (char c : text) { + switch (c) { + case '"': out += "\\\""; break; + case '\\': out += "\\\\"; break; + case '\n': out += "\\n"; break; + case '\r': out += "\\r"; break; + case '\t': out += "\\t"; break; + default: + if (static_cast(c) < 0x20) { + char buffer[7]; + std::snprintf(buffer, sizeof(buffer), "\\u%04x", + static_cast(static_cast(c))); + out += buffer; + } else { + out.push_back(c); + } + break; + } + } + return out; + }; + http::response res{ + static_cast(problem.status), 11}; + res.set(http::field::server, "openapi-generator-cpp-boost-beast-server"); + res.set(http::field::content_type, "application/problem+json"); + std::string body = "{"; + body += "\"type\":\"" + jsonEscape(problem.type) + "\""; + body += ",\"title\":\"" + jsonEscape(problem.title) + "\""; + body += ",\"status\":" + std::to_string(problem.status); + if (!problem.detail.empty()) { + body += ",\"detail\":\"" + jsonEscape(problem.detail) + "\""; + } + if (!problem.instance.empty()) { + body += ",\"instance\":\"" + jsonEscape(problem.instance) + "\""; + } + if (!problem.errors.empty()) { + body += ",\"errors\":["; + for (std::size_t i = 0; i < problem.errors.size(); ++i) { + if (i != 0) { + body += ","; + } + body += "{\"path\":\"" + jsonEscape(problem.errors[i].path) + + "\",\"message\":\"" + jsonEscape(problem.errors[i].message) + "\"}"; + } + body += "]"; + } + body += "}"; + res.body() = std::move(body); + res.prepare_payload(); + return res; +} + +} // namespace {{apiNamespace}} + +#endif // {{apiHeaderGuardPrefix}}_SERVER_PROBLEM_H_ diff --git a/modules/openapi-generator/src/main/resources/cpp-boost-beast-server/responder-header.mustache b/modules/openapi-generator/src/main/resources/cpp-boost-beast-server/responder-header.mustache new file mode 100644 index 000000000000..edeb0f1311c2 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/cpp-boost-beast-server/responder-header.mustache @@ -0,0 +1,93 @@ +{{>licenseInfo}} +// ============================================================================ +// Responder.h - completion port for one request. Generated per-operation +// responders wrap a shared ResponderCore; completion posts the response +// onto the connection strand exactly once — later completions are rejected +// and logged, the response discarded. +// ============================================================================ +#ifndef {{apiHeaderGuardPrefix}}_SERVER_RESPONDER_H_ +#define {{apiHeaderGuardPrefix}}_SERVER_RESPONDER_H_ + +#include "BodyJson.h" +#include "Problem.h" + +#include + +#include +#include +#include +#include +#include + +namespace {{apiNamespace}} { + +/// Type-erased single-shot response completion. The write is posted to the +/// connection's executor; the strand serializes it against other I/O. +class ResponderCore { +public: + using Sink = std::function&&)>; + + explicit ResponderCore(Sink sink) : sink_(std::move(sink)) {} + + /// Completes the request with an already-built response. Second and + /// later completions are rejected; the response is discarded. + void complete(boost::beast::http::response&& response) { + if (completed_) { + std::cerr << "cpp-boost-beast-server: duplicate responder completion for " + << operationId_ << " ignored\n"; + return; + } + completed_ = true; + if (sink_) { + sink_(std::move(response)); + } + } + + /// Completes the request with a problem response. + void sendProblem(Problem problem) { + complete(toProblemResponse(std::move(problem))); + } + + /// Completes the request with a JSON body and status. + template + void sendJson(unsigned status, T const& value, std::string const& contentType) { + namespace http = boost::beast::http; + http::response res{ + static_cast(status), 11}; + res.set(http::field::server, "openapi-generator-cpp-boost-beast-server"); + res.set(http::field::content_type, contentType); + res.body() = toJsonBody(value); + res.prepare_payload(); + complete(std::move(res)); + } + + /// Completes the request with an empty body and status. + void sendEmpty(unsigned status) { + namespace http = boost::beast::http; + http::response res{ + static_cast(status), 11}; + res.set(http::field::server, "openapi-generator-cpp-boost-beast-server"); + res.prepare_payload(); + complete(std::move(res)); + } + + void sendNotImplemented(std::string const& operationId) { + complete(toProblemResponse(Problem::notImplemented(operationId))); + } + + void setOperationId(std::string operationId) { + operationId_ = std::move(operationId); + } + + bool completed() const { return completed_; } + +private: + Sink sink_; + bool completed_ = false; + std::string operationId_; +}; + +} // namespace {{apiNamespace}} + +#endif // {{apiHeaderGuardPrefix}}_SERVER_RESPONDER_H_ diff --git a/modules/openapi-generator/src/main/resources/cpp-boost-beast-server/router-header.mustache b/modules/openapi-generator/src/main/resources/cpp-boost-beast-server/router-header.mustache new file mode 100644 index 000000000000..91395f011e95 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/cpp-boost-beast-server/router-header.mustache @@ -0,0 +1,205 @@ +{{>licenseInfo}} +// ============================================================================ +// Router.h - deterministic route table with encoded-segment matching. +// Literal segments match exactly; {param} segments capture the whole raw +// (still percent-encoded) segment so %2F stays inside one path parameter. +// ============================================================================ +#ifndef {{apiHeaderGuardPrefix}}_SERVER_ROUTER_H_ +#define {{apiHeaderGuardPrefix}}_SERVER_ROUTER_H_ + +#include "Responder.h" + +#include +#include +#include +#include +#include + +namespace {{apiNamespace}} { + +/// One security scheme requirement extracted from the OpenAPI document. +struct SchemeRequirement { + std::string name; // scheme name as declared + std::string type; // apiKey | http | unknown + std::string in; // header | query | cookie (apiKey) + std::string paramName; // declared parameter name (apiKey) + std::string httpScheme; // e.g. bearer (http) +}; + +/// OR-of-AND security alternatives for one route. +using SecurityGroups = std::vector>; + +/// Fully-decoded, owning request data handed to generated handlers. +struct RequestContext { + std::string method; + std::string target; // origin-form, encoded + std::multimap query; // decoded key/value pairs + std::map pathParams; // ENCODED raw segments + std::multimap headers; // lowercased names + std::multimap cookies; // decoded + std::string body; + std::string operationId; +}; + +using Handler = std::function)>; + +/// Successful route match. +struct RouteMatch { + Handler handler; + SecurityGroups security; + std::string operationId; + std::map pathParams; +}; + +class Router { +public: + void add(std::string const& method, + std::string const& pathTemplate, + Handler handler, + SecurityGroups security = {}, + std::string operationId = ""); + + /// Matches the encoded origin-form path (query string is ignored). + /// Returns nullopt-shaped result (handler == nullptr) when no template + /// matched the path shape for ANY method. + RouteMatch match(std::string const& method, std::string const& encodedTarget) const; + + /// Comma-separated registered methods for the target path shape, or "". + std::string allowedMethods(std::string const& encodedTarget) const; + + bool empty() const { return routes_.empty(); } + +private: + struct Route { + std::string method; + std::vector segments; // literal text or "{}" placeholder + std::vector paramNames; + Handler handler; + SecurityGroups security; + std::string operationId; + }; + + static std::vector splitPath(std::string const& target); + static bool matches(Route const& route, + std::vector const& segments, + std::map& pathParams); + + std::vector routes_; +}; + +inline void Router::add(std::string const& method, + std::string const& pathTemplate, + Handler handler, + SecurityGroups security, + std::string operationId) { + Route route; + route.method = method; + for (std::string const& segment : splitPath(pathTemplate)) { + if (!segment.empty() && segment.front() == '{' && segment.back() == '}') { + route.segments.push_back("{}"); + route.paramNames.push_back(segment.substr(1, segment.size() - 2)); + } else { + route.segments.push_back(segment); + route.paramNames.push_back(""); + } + } + route.handler = std::move(handler); + route.security = std::move(security); + route.operationId = std::move(operationId); + routes_.push_back(std::move(route)); +} + +inline std::vector Router::splitPath(std::string const& target) { + std::string path = target; + std::size_t query = path.find('?'); + if (query != std::string::npos) { + path.resize(query); + } + std::vector segments; + std::size_t start = 0; + if (!path.empty() && path.front() == '/') { + start = 1; + } + while (start <= path.size()) { + std::size_t slash = path.find('/', start); + if (slash == std::string::npos) { + segments.push_back(path.substr(start)); + break; + } + segments.push_back(path.substr(start, slash - start)); + start = slash + 1; + } + return segments; +} + +inline bool Router::matches(Route const& route, + std::vector const& segments, + std::map& pathParams) { + if (route.segments.size() != segments.size()) { + return false; + } + for (std::size_t i = 0; i < segments.size(); ++i) { + if (route.segments[i] == "{}") { + if (segments[i].empty()) { + return false; + } + pathParams[route.paramNames[i]] = segments[i]; + } else if (route.segments[i] != segments[i]) { + return false; + } + } + return true; +} + +inline RouteMatch Router::match( + std::string const& method, std::string const& encodedTarget) const { + std::vector segments = splitPath(encodedTarget); + for (Route const& route : routes_) { + std::map pathParams; + if (route.method == method && matches(route, segments, pathParams)) { + RouteMatch result; + result.handler = route.handler; + result.security = route.security; + result.operationId = route.operationId; + result.pathParams = std::move(pathParams); + return result; + } + } + RouteMatch none; + none.handler = nullptr; + return none; +} + +inline std::string Router::allowedMethods( + std::string const& encodedTarget) const { + std::vector segments = splitPath(encodedTarget); + std::vector allowed; + for (Route const& route : routes_) { + std::map ignored; + if (matches(route, segments, ignored)) { + bool seen = false; + for (std::string const& existing : allowed) { + if (existing == route.method) { + seen = true; + break; + } + } + if (!seen) { + allowed.push_back(route.method); + } + } + } + std::string joined; + for (std::size_t i = 0; i < allowed.size(); ++i) { + if (i != 0) { + joined += ", "; + } + joined += allowed[i]; + } + return joined; +} + +} // namespace {{apiNamespace}} + +#endif // {{apiHeaderGuardPrefix}}_SERVER_ROUTER_H_ From 5db096e9c6fc882552a21bd8eec065f2d33c8baa Mon Sep 17 00:00:00 2001 From: Benjamin Oldenburg Date: Wed, 26 Aug 2026 21:34:30 +0700 Subject: [PATCH 03/41] test(cpp-boost-beast-server): rejection, surface, and loopback runtime suites Add the server-regression OAS 3.1 fixture (path/query/header/cookie styles, enum/pattern/bound constraints, bearer + apiKey security), 12-test codegen suite (defaults, contract emission, stubs, IR stripping, multipart/x-www-form-urlencoded/text-*/event-stream/content-style/ cookie-matrix/ambiguous-route rejections, 3.0 compatibility), and the native loopback runtime test compiling and running the generated server against real sockets: 200/201/204 happy paths, 400 problem+json with errors[] for every constraint class, 404, 405+Allow, 413, 415, 401 with and without credentials, keep-alive, label-pattern paths, and pipe-delimited collections. Header params now look up lowercased field names; enum allow-lists render unescaped. --- ...oostBeastServerTemplateModelAssembler.java | 2 +- .../api-source.mustache | 8 +- .../param-codecs-header.mustache | 8 + .../CppBoostBeastServerCodegenTest.java | 329 ++++++++++++++++ .../CppBoostBeastServerRuntimeTest.java | 145 +++++++ .../3_1/cpp-boost-beast-server/petstore.yaml | 167 ++++++++ .../server-regression.yaml | 218 +++++++++++ .../server-runtime-regression.cpp | 361 ++++++++++++++++++ 8 files changed, 1233 insertions(+), 5 deletions(-) create mode 100644 modules/openapi-generator/src/test/java/org/openapitools/codegen/cppboostbeastserver/CppBoostBeastServerCodegenTest.java create mode 100644 modules/openapi-generator/src/test/java/org/openapitools/codegen/cppboostbeastserver/CppBoostBeastServerRuntimeTest.java create mode 100644 modules/openapi-generator/src/test/resources/3_1/cpp-boost-beast-server/petstore.yaml create mode 100644 modules/openapi-generator/src/test/resources/3_1/cpp-boost-beast-server/server-regression.yaml create mode 100644 modules/openapi-generator/src/test/resources/3_1/cpp-boost-beast-server/server-runtime-regression.cpp diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/CppBoostBeastServerTemplateModelAssembler.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/CppBoostBeastServerTemplateModelAssembler.java index 92af55da8dca..64ee8d74d019 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/CppBoostBeastServerTemplateModelAssembler.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/CppBoostBeastServerTemplateModelAssembler.java @@ -89,7 +89,7 @@ OperationsMap assemble(OperationsMap objs, List allModels) { private List> serverParams(CodegenOperation op, Operation raw) { List> params = new ArrayList<>(); for (CodegenParameter param : op.allParams) { - if (param == null) { + if (param == null || param.isBodyParam) { continue; } Map facts = new LinkedHashMap<>(); diff --git a/modules/openapi-generator/src/main/resources/cpp-boost-beast-server/api-source.mustache b/modules/openapi-generator/src/main/resources/cpp-boost-beast-server/api-source.mustache index e55350edd07f..d5cafc4b721c 100644 --- a/modules/openapi-generator/src/main/resources/cpp-boost-beast-server/api-source.mustache +++ b/modules/openapi-generator/src/main/resources/cpp-boost-beast-server/api-source.mustache @@ -74,7 +74,7 @@ void {{classname}}::attach(HttpServer& server, std::shared_ptr<{{classname}}> im } {{#stringKind}}{{#hasEnum}} if (!invalid) { - static std::vector const kAllowed = { {{#enumValues}}{{.}}, {{/enumValues}} }; + static std::vector const kAllowed = { {{#enumValues}}{{{.}}}, {{/enumValues}} }; if (std::find(kAllowed.begin(), kAllowed.end(), request.{{cppName}}) == kAllowed.end()) { problem.withError("{{baseName}}", "value is not one of the allowed enum members"); invalid = true; @@ -166,7 +166,7 @@ void {{classname}}::attach(HttpServer& server, std::shared_ptr<{{classname}}> im } {{#stringKind}}{{#hasEnum}} if (!invalid && present) { - static std::vector const kAllowed = { {{#enumValues}}{{.}}, {{/enumValues}} }; + static std::vector const kAllowed = { {{#enumValues}}{{{.}}}, {{/enumValues}} }; if (std::find(kAllowed.begin(), kAllowed.end(), request.{{cppName}}) == kAllowed.end()) { problem.withError("{{baseName}}", "value is not one of the allowed enum members"); invalid = true; @@ -261,7 +261,7 @@ void {{classname}}::attach(HttpServer& server, std::shared_ptr<{{classname}}> im {{/isQuery}} {{#isHeader}} {{^isContainer}} - auto values = ctx.headers.equal_range("{{baseName}}"); + auto values = ctx.headers.equal_range(lowercaseHeaderName("{{baseName}}")); bool present = values.first != values.second; if (!present) { {{#required}} @@ -277,7 +277,7 @@ void {{classname}}::attach(HttpServer& server, std::shared_ptr<{{classname}}> im } {{/isContainer}} {{#isContainer}} - auto values = ctx.headers.equal_range("{{baseName}}"); + auto values = ctx.headers.equal_range(lowercaseHeaderName("{{baseName}}")); bool present = values.first != values.second; if (!present) { {{#required}} diff --git a/modules/openapi-generator/src/main/resources/cpp-boost-beast-server/param-codecs-header.mustache b/modules/openapi-generator/src/main/resources/cpp-boost-beast-server/param-codecs-header.mustache index 8010c1220a8c..1cb55dafb0b7 100644 --- a/modules/openapi-generator/src/main/resources/cpp-boost-beast-server/param-codecs-header.mustache +++ b/modules/openapi-generator/src/main/resources/cpp-boost-beast-server/param-codecs-header.mustache @@ -19,6 +19,14 @@ namespace {{apiNamespace}} { +/// Lowercases an HTTP field name for RequestContext header lookups. +inline std::string lowercaseHeaderName(std::string name) { + for (char& c : name) { + c = static_cast(std::tolower(static_cast(c))); + } + return name; +} + /// Percent-decodes a URI component (%XX sequences). Invalid escapes pass /// through unchanged; '+' is NOT translated to space (that is form encoding). inline std::string percentDecode(std::string_view encoded) { diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/cppboostbeastserver/CppBoostBeastServerCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/cppboostbeastserver/CppBoostBeastServerCodegenTest.java new file mode 100644 index 000000000000..8b2e97c63b8e --- /dev/null +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/cppboostbeastserver/CppBoostBeastServerCodegenTest.java @@ -0,0 +1,329 @@ +/* + * Copyright 2026 OpenAPI-Generator Contributors (https://openapi-generator.tech) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.openapitools.codegen.cppboostbeastserver; + +import org.openapitools.codegen.CodegenType; +import org.openapitools.codegen.DefaultGenerator; +import org.openapitools.codegen.config.CodegenConfigurator; +import org.testng.Assert; +import org.testng.annotations.Test; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +public class CppBoostBeastServerCodegenTest { + + private static final String SERVER_REGRESSION_SPEC = + "src/test/resources/3_1/cpp-boost-beast-server/server-regression.yaml"; + + private static Path generate(String spec, java.util.Map properties) + throws IOException { + Path outputRoot = Files.createDirectories(Path.of("target")); + Path output = Files.createTempDirectory(outputRoot, "cpp-boost-beast-server-test-"); + output.toFile().deleteOnExit(); + CodegenConfigurator configurator = new CodegenConfigurator() + .setGeneratorName("cpp-boost-beast-server") + .setInputSpec(spec) + .setOutputDir(output.toString()); + properties.forEach(configurator::addAdditionalProperty); + new DefaultGenerator().opts(configurator.toClientOptInput()).generate(); + return output; + } + + private static Path writeTempSpec(String... lines) throws IOException { + Path spec = Files.createTempFile("cpp-boost-beast-server-spec-", ".yaml"); + spec.toFile().deleteOnExit(); + Files.writeString(spec, String.join("\n", lines) + "\n"); + return spec.toAbsolutePath(); + } + + private static final String[] HEADER = { + "openapi: 3.1.0", "info: {title: t, version: '1'}", "paths:"}; + + private static String[] spec(String... body) { + String[] all = new String[body.length + HEADER.length]; + System.arraycopy(HEADER, 0, all, 0, HEADER.length); + System.arraycopy(body, 0, all, HEADER.length, body.length); + return all; + } + + @Test + public void defaultsAreConfigured() { + org.openapitools.codegen.languages.CppBoostBeastServerCodegen codegen = + new org.openapitools.codegen.languages.CppBoostBeastServerCodegen(); + Assert.assertEquals(codegen.getName(), "cpp-boost-beast-server"); + Assert.assertEquals(codegen.getTag(), CodegenType.SERVER); + Assert.assertEquals(codegen.modelPackage(), "org.openapitools.server.model"); + Assert.assertEquals(codegen.apiPackage(), "org.openapitools.server.api"); + Assert.assertTrue(codegen.getOutputDir().contains("cpp-boost-beast-server")); + List destinations = codegen.supportingFiles().stream() + .map(file -> file.getDestinationFilename()) + .sorted() + .collect(java.util.stream.Collectors.toList()); + Assert.assertTrue(destinations.contains("HttpServer.h"), + "runtime HttpServer.h must be a supporting file"); + Assert.assertTrue(destinations.contains("BodyJson.h"), + "runtime BodyJson.h must be a supporting file"); + Assert.assertTrue(destinations.contains("Oas31Validator.h"), + "shared validation header must be a supporting file"); + Assert.assertTrue(destinations.contains("schema_ir.generated.cpp"), + "schema IR source must be a supporting file"); + Assert.assertFalse(destinations.contains("main.cpp"), + "main.cpp must not be generated without addApiImplStubs"); + } + + @Test + public void generatesFullContractFromRegressionSpec() throws IOException { + Path output = generate(SERVER_REGRESSION_SPEC, java.util.Map.of()); + + String apiHeader = Files.readString(output.resolve("api/DefaultApi.h")); + Assert.assertTrue(apiHeader.contains("virtual void getPetById("), + "service interface must declare getPetById"); + Assert.assertTrue(apiHeader.contains("struct GetPetByIdRequest"), + "request struct must be named from the operationId"); + Assert.assertTrue(apiHeader.contains("void send200(Pet value) const"), + "responder must expose send200 for the Pet response"); + Assert.assertTrue(apiHeader.contains("void send204() const"), + "responder must expose the empty 204"); + Assert.assertTrue(apiHeader.contains( + "void sendDefault(ErrorResponse value, unsigned status) const"), + "responder must expose the default ErrorResponse sender"); + + String apiSource = Files.readString(output.resolve("api/DefaultApi.cpp")); + Assert.assertTrue(apiSource.contains("router->add("), + "route registration must be emitted"); + Assert.assertTrue(apiSource.contains("splitOn(values.first->second, '|')"), + "pipe-delimited query collection must split on '|'"); + Assert.assertTrue(apiSource.contains("std::regex"), + "pattern constraints must emit a regex check"); + Assert.assertTrue(apiSource.contains("kAllowed"), + "enum constraints must emit an allow-list check"); + + Assert.assertTrue(Files.exists(output.resolve("server/HttpServer.h"))); + Assert.assertTrue(Files.exists(output.resolve("server/Router.h"))); + Assert.assertTrue(Files.exists(output.resolve("server/Responder.h"))); + Assert.assertTrue(Files.exists(output.resolve("server/Problem.h"))); + Assert.assertTrue(Files.exists(output.resolve("server/ParamCodecs.h"))); + Assert.assertTrue(Files.exists(output.resolve("server/BodyJson.h"))); + Assert.assertTrue(Files.exists(output.resolve("model/Pet.h")), + "models must be generated"); + } + + @Test + public void addApiImplStubsEmitsMainAndStubs() throws IOException { + Path output = generate(SERVER_REGRESSION_SPEC, + java.util.Map.of("addApiImplStubs", Boolean.TRUE)); + Assert.assertTrue(Files.exists(output.resolve("main.cpp")), + "addApiImplStubs must generate main.cpp"); + String apiHeader = Files.readString(output.resolve("api/DefaultApi.h")); + Assert.assertTrue(apiHeader.contains("class DefaultApiStub : public DefaultApi"), + "addApiImplStubs must generate a stub service"); + String main = Files.readString(output.resolve("main.cpp")); + Assert.assertTrue( + main.contains("DefaultApi::attach(server, std::make_shared())"), + "main.cpp must attach the stub service"); + } + + @Test + public void compileWithValidationFalseStripsSchemaIr() throws IOException { + Path output = generate(SERVER_REGRESSION_SPEC, + java.util.Map.of("compileWithValidation", Boolean.FALSE)); + Assert.assertFalse(Files.exists(output.resolve("model/schema_ir.generated.cpp")), + "IR source must be stripped when validation is disabled"); + Assert.assertFalse(Files.exists(output.resolve("model/Oas31SchemaRegistry.h")), + "IR registry must be stripped when validation is disabled"); + String cmake = Files.readString(output.resolve("CMakeLists.txt")); + Assert.assertFalse(cmake.contains("schema_ir.generated"), + "CMake must not reference the stripped IR"); + Assert.assertTrue(Files.exists(output.resolve("model/Oas31Validator.h")), + "header-only validator must remain"); + } + + @Test + public void rejectsMultipartRequestBody() throws IOException { + Path spec = writeTempSpec(spec( + " /upload:", + " post:", + " operationId: upload", + " requestBody:", + " content:", + " multipart/form-data:", + " schema:", + " type: object", + " responses:", + " '200': {description: ok}")); + IllegalArgumentException error = Assert.expectThrows( + IllegalArgumentException.class, + () -> generate(spec.toString(), java.util.Map.of())); + Assert.assertTrue(error.getMessage().contains("multipart/form-data"), + "diagnostic must name the media type: " + error.getMessage()); + Assert.assertTrue(error.getMessage().startsWith("cpp-boost-beast-server: ")); + } + + @Test + public void rejectsFormUrlencodedBody() throws IOException { + Path spec = writeTempSpec(spec( + " /form:", + " post:", + " operationId: submit", + " requestBody:", + " content:", + " application/x-www-form-urlencoded:", + " schema:", + " type: object", + " responses:", + " '200': {description: ok}")); + IllegalArgumentException error = Assert.expectThrows( + IllegalArgumentException.class, + () -> generate(spec.toString(), java.util.Map.of())); + Assert.assertTrue(error.getMessage().contains("application/x-www-form-urlencoded")); + } + + @Test + public void rejectsPlainTextResponse() throws IOException { + Path spec = writeTempSpec(spec( + " /text:", + " get:", + " operationId: getText", + " responses:", + " '200':", + " description: ok", + " content:", + " text/plain:", + " schema: {type: string}")); + IllegalArgumentException error = Assert.expectThrows( + IllegalArgumentException.class, + () -> generate(spec.toString(), java.util.Map.of())); + Assert.assertTrue(error.getMessage().contains("text/plain")); + } + + @Test + public void rejectsEventStreamResponse() throws IOException { + Path spec = writeTempSpec(spec( + " /stream:", + " get:", + " operationId: stream", + " responses:", + " '200':", + " description: events", + " content:", + " text/event-stream:", + " schema: {type: string}")); + IllegalArgumentException error = Assert.expectThrows( + IllegalArgumentException.class, + () -> generate(spec.toString(), java.util.Map.of())); + Assert.assertTrue(error.getMessage().contains("text/event-stream")); + } + + @Test + public void rejectsContentStyleParameter() throws IOException { + Path spec = writeTempSpec(spec( + " /p:", + " get:", + " operationId: op", + " parameters:", + " - name: token", + " in: query", + " content:", + " application/json:", + " schema: {type: string}", + " responses:", + " '200': {description: ok}")); + IllegalArgumentException error = Assert.expectThrows( + IllegalArgumentException.class, + () -> generate(spec.toString(), java.util.Map.of())); + Assert.assertTrue(error.getMessage().contains("content-style")); + } + + @Test + public void rejectsCookieMatrixStyle() throws IOException { + Path spec = writeTempSpec(spec( + " /c:", + " get:", + " operationId: op", + " parameters:", + " - name: session", + " in: cookie", + " style: matrix", + " schema: {type: string}", + " responses:", + " '200': {description: ok}")); + IllegalArgumentException error = Assert.expectThrows( + IllegalArgumentException.class, + () -> generate(spec.toString(), java.util.Map.of())); + Assert.assertTrue(error.getMessage().contains("matrix")); + } + + @Test + public void rejectsAmbiguousRouteShapes() throws IOException { + Path spec = writeTempSpec(spec( + " /a/{x}/c:", + " get:", + " operationId: first", + " responses:", + " '200': {description: ok}", + " /a/{y}/c:", + " get:", + " operationId: second", + " responses:", + " '200': {description: ok}")); + CodegenConfigurator configurator = new CodegenConfigurator() + .setGeneratorName("cpp-boost-beast-server") + .setInputSpec(spec.toString()) + .setOutputDir(Files.createTempDirectory("routes-").toString()) + .setValidateSpec(false); + IllegalArgumentException error = Assert.expectThrows( + IllegalArgumentException.class, + () -> new DefaultGenerator() + .opts(configurator.toClientOptInput()).generate()); + Assert.assertTrue(error.getMessage().contains("/a/{x}/c"), + "diagnostic must list the first template"); + Assert.assertTrue(error.getMessage().contains("/a/{y}/c"), + "diagnostic must list the second template"); + } + + @Test + public void generatesFromOas30Spec() throws IOException { + // The shared pipeline must keep 3.0 documents working (JSON-only). + Path spec = writeTempSpec( + "openapi: 3.0.3", + "info: {title: t, version: '1'}", + "paths:", + " /pets/{petId}:", + " get:", + " operationId: getPet", + " parameters:", + " - name: petId", + " in: path", + " required: true", + " schema: {type: integer, format: int64}", + " responses:", + " '200':", + " description: ok", + " content:", + " application/json:", + " schema: {type: string}"); + Path output = generate(spec.toString(), java.util.Map.of()); + Assert.assertTrue(Files.exists(output.resolve("api/DefaultApi.h")), + "3.0 spec must generate the API"); + Assert.assertTrue(Files.exists(output.resolve("model/ValidationTypes.h")), + "3.0 spec must generate the shared validation runtime"); + } +} diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/cppboostbeastserver/CppBoostBeastServerRuntimeTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/cppboostbeastserver/CppBoostBeastServerRuntimeTest.java new file mode 100644 index 000000000000..55599d519076 --- /dev/null +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/cppboostbeastserver/CppBoostBeastServerRuntimeTest.java @@ -0,0 +1,145 @@ +/* + * Copyright 2026 OpenAPI-Generator Contributors (https://openapi-generator.tech) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.openapitools.codegen.cppboostbeastserver; + +import org.openapitools.codegen.DefaultGenerator; +import org.openapitools.codegen.config.CodegenConfigurator; +import org.testng.Assert; +import org.testng.annotations.Test; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Generates a server from the OAS 3.1 regression spec, compiles it together + * with the loopback driver, runs it against real sockets, and asserts the + * sentinel output. This is the end-to-end behavior proof for the generator: + * routing, parameter codecs, body decoding, security, and error mapping all + * execute in the produced C++ binary. + */ +public class CppBoostBeastServerRuntimeTest { + + @Test + public void generatedServerServesLoopbackRegressions() throws Exception { + Path outputRoot = Files.createDirectories(Path.of("target")); + Path output = Files.createTempDirectory( + outputRoot, "cpp-boost-beast-server-runtime-"); + output.toFile().deleteOnExit(); + + CodegenConfigurator configurator = new CodegenConfigurator() + .setGeneratorName("cpp-boost-beast-server") + .setInputSpec( + "src/test/resources/3_1/cpp-boost-beast-server/server-regression.yaml") + .setOutputDir(output.toString()); + new DefaultGenerator().opts(configurator.toClientOptInput()).generate(); + + Path driver = Path.of( + "src/test/resources/3_1/cpp-boost-beast-server/" + + "server-runtime-regression.cpp"); + Path executable = output.resolve("server-runtime-regression"); + String compiler = System.getenv().getOrDefault("CXX", "c++"); + + List command = new ArrayList<>(); + command.add(compiler); + command.add("-std=c++17"); + command.add("-Wall"); + command.add("-Werror"); + command.add("-DBOOST_ERROR_CODE_HEADER_ONLY"); + command.add("-I" + output); + command.add("-I" + output.resolve("api")); + command.add("-I" + output.resolve("model")); + command.add("-I" + output.resolve("server")); + for (String candidate : new String[]{"/opt/homebrew", "/usr/local"}) { + Path include = Path.of(candidate, "include"); + Path lib = Path.of(candidate, "lib"); + if (Files.isDirectory(include)) { + command.add("-I" + include); + } + if (Files.isDirectory(lib)) { + command.add("-L" + lib); + } + } + command.add(driver.toString()); + try (Stream sources = Files.list(output.resolve("model"))) { + sources.filter(path -> path.getFileName().toString().endsWith(".cpp")) + .map(Path::toString) + .sorted() + .forEach(command::add); + } + try (Stream sources = Files.list(output.resolve("api"))) { + sources.filter(path -> path.getFileName().toString().endsWith(".cpp")) + .map(Path::toString) + .sorted() + .forEach(command::add); + } + command.add(output.resolve("server/HttpServer.cpp").toString()); + command.add("-lboost_json"); + command.add("-lboost_url"); + command.add("-pthread"); + command.add("-o"); + command.add(executable.toString()); + + Process compile = new ProcessBuilder(command) + .redirectErrorStream(true) + .directory(new File(".")) + .start(); + Assert.assertTrue(compile.waitFor(10, TimeUnit.MINUTES), + "server runtime compile timed out"); + String compileOutput = new String( + compile.getInputStream().readAllBytes(), StandardCharsets.UTF_8); + if (compile.exitValue() != 0 && missingBoost(compileOutput)) { + throw new org.testng.SkipException( + "Boost development files are unavailable; " + + "skipping server runtime test: " + + compileOutput.trim()); + } + Assert.assertEquals(compile.exitValue(), 0, + "server runtime compile failed:\n" + compileOutput); + + Process run = new ProcessBuilder(executable.toString()) + .redirectErrorStream(true) + .start(); + Assert.assertTrue(run.waitFor(120, TimeUnit.SECONDS), + "server runtime execution timed out"); + String runOutput = new String( + run.getInputStream().readAllBytes(), StandardCharsets.UTF_8); + Assert.assertEquals(run.exitValue(), 0, + "server runtime test failed:\n" + runOutput); + Assert.assertTrue( + runOutput.contains("cpp-boost-beast-server runtime regressions passed"), + "server runtime test did not report completion: " + runOutput); + } + + private static boolean missingBoost(String compilerOutput) { + String normalized = compilerOutput.toLowerCase(java.util.Locale.ROOT); + boolean missingHeaders = normalized.contains("boost/") + && (normalized.contains("not found") + || normalized.contains("no such file")); + boolean missingLibraries = normalized.contains("cannot find -lboost_") + || normalized.contains("library 'boost_") + || normalized.contains("library not found for -lboost_"); + return missingHeaders || missingLibraries; + } +} diff --git a/modules/openapi-generator/src/test/resources/3_1/cpp-boost-beast-server/petstore.yaml b/modules/openapi-generator/src/test/resources/3_1/cpp-boost-beast-server/petstore.yaml new file mode 100644 index 000000000000..2bea385372b9 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_1/cpp-boost-beast-server/petstore.yaml @@ -0,0 +1,167 @@ +openapi: 3.1.0 +info: + title: Petstore Server + description: Sample petstore server for the cpp-boost-beast-server generator + version: 1.0.0 +servers: + - url: http://petstore.swagger.io/api/v3 +tags: + - name: pets + - name: store + - name: users +paths: + /pets: + get: + tags: [pets] + operationId: listPets + summary: List pets + parameters: + - name: limit + in: query + description: How many items to return at one time (max 100) + schema: + type: integer + format: int32 + minimum: 1 + maximum: 100 + responses: + '200': + description: A paged array of pets + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + post: + tags: [pets] + operationId: createPet + summary: Create a pet + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + responses: + '201': + description: The created pet + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + /pets/{petId}: + get: + tags: [pets] + operationId: showPetById + summary: Info for a specific pet + parameters: + - name: petId + in: path + required: true + description: The id of the pet to retrieve + schema: + type: integer + format: int64 + responses: + '200': + description: Expected response to a valid request + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + '404': + description: No pet found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + delete: + tags: [pets] + operationId: deletePetById + summary: Delete a pet + parameters: + - name: petId + in: path + required: true + schema: + type: integer + format: int64 + responses: + '204': + description: Deleted + /store/inventory: + get: + tags: [store] + operationId: getInventory + summary: Returns pet inventories by status + responses: + '200': + description: Status counts + content: + application/json: + schema: + type: object + additionalProperties: + type: integer + format: int32 + /users/{username}: + get: + tags: [users] + operationId: getUserByName + summary: Get user by username + parameters: + - name: username + in: path + required: true + schema: + type: string + responses: + '200': + description: The user + content: + application/json: + schema: + $ref: '#/components/schemas/User' + '404': + description: No such user + content: + application/json: + schema: + $ref: '#/components/schemas/Error' +components: + schemas: + Pet: + type: object + required: [id, name] + properties: + id: + type: integer + format: int64 + name: + type: string + tag: + type: string + status: + type: string + enum: [available, pending, sold] + User: + type: object + required: [username] + properties: + id: + type: integer + format: int64 + username: + type: string + email: + type: string + Error: + type: object + required: [code, message] + properties: + code: + type: integer + format: int32 + message: + type: string diff --git a/modules/openapi-generator/src/test/resources/3_1/cpp-boost-beast-server/server-regression.yaml b/modules/openapi-generator/src/test/resources/3_1/cpp-boost-beast-server/server-regression.yaml new file mode 100644 index 000000000000..3985e4df2c84 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_1/cpp-boost-beast-server/server-regression.yaml @@ -0,0 +1,218 @@ +openapi: 3.1.0 +info: + title: cpp-boost-beast-server regression + version: 1.0.0 +servers: + - url: http://localhost:8080 +security: + - api_key: [] +paths: + /pets/{petId}: + get: + operationId: getPetById + parameters: + - name: petId + in: path + required: true + description: Pet identifier + schema: + type: integer + format: int64 + minimum: 1 + responses: + '200': + description: The pet + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + '404': + description: No such pet + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + security: [] + put: + operationId: updatePet + parameters: + - name: petId + in: path + required: true + schema: + type: integer + format: int64 + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + responses: + '200': + description: Updated pet + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + delete: + operationId: deletePet + parameters: + - name: petId + in: path + required: true + schema: + type: integer + format: int64 + - name: X-API-KEY + in: header + required: true + description: API key header array (simple style) + schema: + type: array + items: + type: string + style: simple + responses: + '204': + description: Deleted + /pets: + get: + operationId: listPets + parameters: + - name: status + in: query + description: Filter by status enum + schema: + type: string + enum: [available, sold] + - name: tags + in: query + description: Pipe-delimited tag filter + schema: + type: array + items: + type: string + style: pipeDelimited + explode: false + - name: limit + in: query + description: Page size + schema: + type: integer + format: int32 + minimum: 1 + maximum: 100 + responses: + '200': + description: Pet collection + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + security: [] + post: + operationId: createPet + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + responses: + '201': + description: Created pet + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + '400': + description: Invalid pet + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + default: + description: Unexpected error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + security: + - bearer: [] + /reports/{reportId}: + get: + operationId: getReport + parameters: + - name: reportId + in: path + required: true + schema: + type: string + pattern: '^[A-Z]{2}-[0-9]+$' + style: label + - name: lang + in: cookie + schema: + type: string + responses: + '200': + description: The report + content: + application/json: + schema: + $ref: '#/components/schemas/Report' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + security: + - bearer: [] +components: + securitySchemes: + api_key: + type: apiKey + name: X-API-KEY + in: header + bearer: + type: http + scheme: bearer + schemas: + Pet: + type: object + required: [id, name] + properties: + id: + type: integer + format: int64 + name: + type: string + status: + type: string + enum: [available, sold] + tag: + type: [string, 'null'] + photoUrls: + type: array + items: + type: string + ErrorResponse: + type: object + properties: + code: + type: integer + format: int32 + message: + type: string + Report: + type: object + required: [title] + properties: + title: + type: string + createdAt: + type: [string, 'null'] diff --git a/modules/openapi-generator/src/test/resources/3_1/cpp-boost-beast-server/server-runtime-regression.cpp b/modules/openapi-generator/src/test/resources/3_1/cpp-boost-beast-server/server-runtime-regression.cpp new file mode 100644 index 000000000000..f76efcb25166 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_1/cpp-boost-beast-server/server-runtime-regression.cpp @@ -0,0 +1,361 @@ +// ============================================================================ +// server-runtime-regression.cpp - end-to-end loopback driver for the +// cpp-boost-beast-server runtime test. Implements every service method, +// serves on 127.0.0.1:0, then asserts wire behavior with raw sockets. +// ============================================================================ +#include "HttpServer.h" +#include "ParamCodecs.h" +#include "Problem.h" +#include "Responder.h" +#include "Router.h" +#include "DefaultApi.h" + +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace api = org::openapitools::server::api; +namespace model = org::openapitools::server::model; + +static int failures = 0; + +static void expect(bool condition, std::string const& what) { + if (!condition) { + ++failures; + std::cerr << "FAIL: " << what << "\n"; + } +} + +// --------------------------------------------------------------------------- +// Service implementation with deterministic echo behavior. +// --------------------------------------------------------------------------- +class RegressionApi : public api::DefaultApi { +public: + void getPetById(api::GetPetByIdRequest request, + api::RequestContext&, + api::GetPetByIdResponder responder) override { + model::Pet pet; + pet.setId(request.petId); + pet.setName("pet-" + std::to_string(request.petId)); + pet.setStatus(std::string("available")); + responder.send200(std::move(pet)); + } + + void updatePet(api::UpdatePetRequest request, + api::RequestContext&, + api::UpdatePetResponder responder) override { + responder.send200(request.body); + } + + void deletePet(api::DeletePetRequest, + api::RequestContext&, + api::DeletePetResponder responder) override { + responder.send204(); + } + + void createPet(api::CreatePetRequest request, + api::RequestContext&, + api::CreatePetResponder responder) override { + responder.send201(request.body); + } + + void listPets(api::ListPetsRequest request, + api::RequestContext&, + api::ListPetsResponder responder) override { + std::vector> pets; + int count = request.limit > 0 ? static_cast(request.limit) : 2; + for (int i = 1; i <= count; ++i) { + auto pet = std::make_shared(); + pet->setId(i); + pet->setName("pet-" + std::to_string(i)); + if (!request.status.empty()) { + pet->setStatus(request.status); + } + pets.push_back(std::move(pet)); + } + responder.send200(std::move(pets)); + } + + void getReport(api::GetReportRequest, + api::RequestContext&, + api::GetReportResponder responder) override { + model::Report report; + report.setTitle("report"); + responder.send200(std::move(report)); + } +}; + +class RegressionAuthorizer : public api::Authorizer { +public: + bool authorize(std::string const&, api::AuthCredentials const& credentials) override { + return credentials.httpAuthorization != "Bearer deny"; + } +}; + +// --------------------------------------------------------------------------- +// Raw-socket HTTP client helpers. +// --------------------------------------------------------------------------- +struct RawResponse { + unsigned status = 0; + std::string allow; + std::string contentType; + std::string body; +}; + +static RawResponse roundtrip( + boost::asio::io_context& ioc, + unsigned port, + std::string const& request, + bool closeConnection = true, + boost::asio::ip::tcp::socket* persistent = nullptr) { + RawResponse result; + boost::asio::ip::tcp::socket owned(ioc); + boost::asio::ip::tcp::socket& target = + persistent != nullptr ? *persistent : owned; + if (persistent == nullptr) { + target.connect(boost::asio::ip::tcp::endpoint{ + boost::asio::ip::make_address("127.0.0.1"), + static_cast(port)}); + } + boost::asio::write(target, boost::asio::buffer(request)); + + std::string accumulated; + char buffer[4096]; + boost::system::error_code error; + for (;;) { + std::size_t received = target.read_some( + boost::asio::buffer(buffer), error); + if (error) { + break; + } + accumulated.append(buffer, received); + std::size_t headerEnd = accumulated.find("\r\n\r\n"); + if (headerEnd == std::string::npos) { + continue; + } + std::size_t contentLength = 0; + std::istringstream headerStream(accumulated.substr(0, headerEnd)); + std::string line; + while (std::getline(headerStream, line)) { + std::string lower; + lower.reserve(line.size()); + for (char c : line) { + lower.push_back(static_cast(std::tolower( + static_cast(c)))); + } + std::string const contentLengthPrefix = "content-length: "; + std::string const allowPrefix = "allow: "; + std::string const contentTypePrefix = "content-type: "; + if (lower.size() > contentLengthPrefix.size() + && lower.compare(0, contentLengthPrefix.size(), + contentLengthPrefix) == 0) { + contentLength = static_cast( + std::stoul(lower.substr(contentLengthPrefix.size()))); + } + if (lower.size() > allowPrefix.size() + && lower.compare(0, allowPrefix.size(), allowPrefix) == 0) { + result.allow = line.substr(allowPrefix.size()); + } + if (lower.size() > contentTypePrefix.size() + && lower.compare(0, contentTypePrefix.size(), + contentTypePrefix) == 0) { + result.contentType = line.substr(contentTypePrefix.size()); + } + } + if (accumulated.size() - headerEnd - 4 >= contentLength) { + std::string headerBlock = accumulated.substr(0, headerEnd); + std::size_t statusStart = headerBlock.find(' '); + result.status = static_cast(std::stoul( + headerBlock.substr(statusStart + 1, 3))); + result.body = accumulated.substr(headerEnd + 4, contentLength); + break; + } + } + if (closeConnection) { + boost::system::error_code ignored; + target.shutdown(boost::asio::ip::tcp::socket::shutdown_both, ignored); + } + return result; +} + +/// Builds a request string, computing Content-Length from the actual body. +static std::string request( + std::string const& methodAndPath, + std::string const& headers, + std::string const& body) { + std::string fixedHeaders = headers; + if (!body.empty() && fixedHeaders.find("Content-Length") == std::string::npos) { + fixedHeaders += "Content-Length: " + std::to_string(body.size()) + "\r\n"; + } + return methodAndPath + " HTTP/1.1\r\nHost: t\r\n" + fixedHeaders + + "\r\n" + body; +} + +int main() { + boost::asio::io_context ioc; + auto router = std::make_shared(); + api::ServerOptions options; + options.readTimeoutSeconds = 30; + options.bodyLimitBytes = 1024; + options.authorizer = std::make_shared(); + auto server = std::make_shared(ioc, router, options); + api::DefaultApi::attach(*server, std::make_shared()); + server->listen(boost::asio::ip::tcp::endpoint{ + boost::asio::ip::make_address("127.0.0.1"), + static_cast(0)}); + unsigned port = server->localEndpoint().port(); + + std::thread serverThread([&ioc] { ioc.run(); }); + + // 200 + JSON body on a valid GET. + RawResponse ok = roundtrip(ioc, port, + "GET /pets/42 HTTP/1.1\r\nHost: t\r\nConnection: close\r\n\r\n"); + expect(ok.status == 200, "valid pet GET should be 200"); + expect(ok.body.find("\"id\":42") != std::string::npos, + "pet body should carry id 42"); + + // 400 problem on int64 path failure and minimum violation. + RawResponse badId = roundtrip(ioc, port, + "GET /pets/abc HTTP/1.1\r\nHost: t\r\nConnection: close\r\n\r\n"); + expect(badId.status == 400, "non-numeric petId should be 400"); + expect(badId.contentType.find("application/problem+json") != std::string::npos, + "problem content type on bad petId"); + expect(badId.body.find("\"errors\"") != std::string::npos, + "problem errors array on bad petId"); + + RawResponse belowMin = roundtrip(ioc, port, + "GET /pets/0 HTTP/1.1\r\nHost: t\r\nConnection: close\r\n\r\n"); + expect(belowMin.status == 400, "petId below minimum should be 400"); + + // 400 on enum failure and limit bound failure. + RawResponse badEnum = roundtrip(ioc, port, + "GET /pets?status=unknown HTTP/1.1\r\nHost: t\r\nConnection: close\r\n\r\n"); + expect(badEnum.status == 400, "invalid status enum should be 400"); + + RawResponse badLimit = roundtrip(ioc, port, + "GET /pets?limit=0 HTTP/1.1\r\nHost: t\r\nConnection: close\r\n\r\n"); + expect(badLimit.status == 400, "limit below minimum should be 400"); + + // 404 on unknown path. + RawResponse missing = roundtrip(ioc, port, + "GET /nope HTTP/1.1\r\nHost: t\r\nConnection: close\r\n\r\n"); + expect(missing.status == 404, "unknown path should be 404"); + + // 405 with Allow on wrong method. + RawResponse wrongMethod = roundtrip(ioc, port, + "PATCH /pets/42 HTTP/1.1\r\nHost: t\r\nConnection: close\r\n\r\n"); + expect(wrongMethod.status == 405, "wrong method should be 405"); + expect(wrongMethod.allow.find("GET") != std::string::npos + && wrongMethod.allow.find("PUT") != std::string::npos + && wrongMethod.allow.find("DELETE") != std::string::npos, + "Allow should list GET, PUT, DELETE"); + + // 400 on malformed JSON body. + RawResponse badJson = roundtrip(ioc, port, request("POST /pets", + "Authorization: Bearer ok\r\nContent-Type: application/json\r\n" + "Connection: close\r\n", "{oops}}")); + expect(badJson.status == 400, "malformed JSON body should be 400"); + + // 400 with errors[] on schema-invalid body (missing required name). + RawResponse missingName = roundtrip(ioc, port, request("POST /pets", + "Authorization: Bearer ok\r\nContent-Type: application/json\r\n" + "Connection: close\r\n", "{\"id\":7}")); + expect(missingName.status == 400, "missing required name should be 400"); + expect(missingName.body.find("\"errors\"") != std::string::npos, + "missing-name problem should carry errors"); + + // 201 on valid body. + RawResponse created = roundtrip(ioc, port, request("POST /pets", + "Authorization: Bearer ok\r\nContent-Type: application/json\r\n" + "Connection: close\r\n", "{\"id\":9,\"name\":\"rex\"}")); + expect(created.status == 201, "valid create should be 201"); + expect(created.body.find("rex") != std::string::npos, + "created body should echo name"); + + // 415 on text/plain. + RawResponse wrongType = roundtrip(ioc, port, request("POST /pets", + "Authorization: Bearer ok\r\nContent-Type: text/plain\r\n" + "Connection: close\r\n", "hi")); + expect(wrongType.status == 415, "text/plain body should be 415"); + + // 401 without credentials on POST. + RawResponse noAuth = roundtrip(ioc, port, request("POST /pets", + "Content-Type: application/json\r\nConnection: close\r\n", + "{\"id\":9,\"name\":\"rex\"}")); + expect(noAuth.status == 401, "POST without bearer should be 401"); + + // 401 on explicitly denied credentials. + RawResponse denied = roundtrip(ioc, port, request("POST /pets", + "Authorization: Bearer deny\r\nContent-Type: application/json\r\n" + "Connection: close\r\n", "{\"id\":9,\"name\":\"rex\"}")); + expect(denied.status == 401, "denied bearer should be 401"); + + // 204 with valid API key (inherited global security) on DELETE. + RawResponse deleted = roundtrip(ioc, port, + "DELETE /pets/42 HTTP/1.1\r\nHost: t\r\nX-API-KEY: k1,k2\r\n" + "Connection: close\r\n\r\n"); + expect(deleted.status == 204, "DELETE with api_key should be 204"); + + // 401 without the API key on DELETE. + RawResponse noKey = roundtrip(ioc, port, + "DELETE /pets/42 HTTP/1.1\r\nHost: t\r\nConnection: close\r\n\r\n"); + expect(noKey.status == 401, "DELETE without api_key should be 401"); + // 413 on a body over the configured limit. + std::string big(2048, 'x'); + RawResponse tooBig = roundtrip(ioc, port, request("POST /pets", + "Authorization: Bearer ok\r\nContent-Type: application/json\r\n" + "Connection: close\r\n", big)); + expect(tooBig.status == 413, "oversized body should be 413"); + + // Keep-alive: two requests on one connection. + { + boost::asio::ip::tcp::socket persistent(ioc); + persistent.connect(boost::asio::ip::tcp::endpoint{ + boost::asio::ip::make_address("127.0.0.1"), + static_cast(port)}); + RawResponse first = roundtrip(ioc, port, + "GET /pets/7 HTTP/1.1\r\nHost: t\r\n\r\n", + false, &persistent); + RawResponse second = roundtrip(ioc, port, + "GET /pets/8 HTTP/1.1\r\nHost: t\r\nConnection: close\r\n\r\n", + true, &persistent); + expect(first.status == 200 && second.status == 200, + "keep-alive should serve two requests on one connection"); + expect(second.body.find("\"id\":8") != std::string::npos, + "second keep-alive response should be for pet 8"); + } + + // Label-style pattern path parameter: valid then invalid. + RawResponse labelOk = roundtrip(ioc, port, + "GET /reports/.AB-12 HTTP/1.1\r\nHost: t\r\n" + "Authorization: Bearer ok\r\nCookie: lang=en\r\n" + "Connection: close\r\n\r\n"); + expect(labelOk.status == 200, "label path with valid pattern should be 200"); + + RawResponse labelBad = roundtrip(ioc, port, + "GET /reports/.ab12 HTTP/1.1\r\nHost: t\r\n" + "Authorization: Bearer ok\r\nConnection: close\r\n\r\n"); + expect(labelBad.status == 400, "label path violating pattern should be 400"); + + // Pipe-delimited array query parameter. + RawResponse piped = roundtrip(ioc, port, + "GET /pets?tags=red%7Cblue HTTP/1.1\r\nHost: t\r\n" + "Connection: close\r\n\r\n"); + expect(piped.status == 200, "pipe-delimited tags should parse"); + + ioc.stop(); + serverThread.join(); + + if (failures != 0) { + std::cerr << failures << " server runtime assertion(s) failed\n"; + return 1; + } + std::cout << "cpp-boost-beast-server runtime regressions passed\n"; + return 0; +} From 2f49df3cc0f61a343d5a66a1e5f774d9639d1caf Mon Sep 17 00:00:00 2001 From: Benjamin Oldenburg Date: Wed, 26 Aug 2026 21:47:33 +0700 Subject: [PATCH 04/41] feat(cpp-boost-beast-server): petstore sample, generator docs, and CI Add the deterministic cpp-boost-beast-server petstore sample (builds clean with -Wall and serves 501 stubs / 400 / 404 over HTTP/1.1), the generated generator documentation, and a three-OS sample build workflow covering Boost json+url consumers. API headers now emit the model namespace using-directive only when an API actually references a generated model class (map-only APIs such as store inventory compile standalone). --- .../samples-cpp-boost-beast-server.yaml | 55 + .../cpp-boost-beast-server-petstore.yaml | 8 + docs/generators/cpp-boost-beast-server.md | 277 ++++ ...oostBeastServerTemplateModelAssembler.java | 25 + .../api-header.mustache | 5 +- .../.openapi-generator-ignore | 23 + .../.openapi-generator/FILES | 34 + .../.openapi-generator/VERSION | 1 + .../cpp-boost-beast-server/CMakeLists.txt | 121 ++ .../petstore/cpp-boost-beast-server/README.md | 88 ++ .../cpp-boost-beast-server/api/PetsApi.cpp | 253 ++++ .../cpp-boost-beast-server/api/PetsApi.h | 244 +++ .../cpp-boost-beast-server/api/StoreApi.cpp | 73 + .../cpp-boost-beast-server/api/StoreApi.h | 108 ++ .../cpp-boost-beast-server/api/UsersApi.cpp | 91 ++ .../cpp-boost-beast-server/api/UsersApi.h | 115 ++ .../petstore/cpp-boost-beast-server/main.cpp | 67 + .../cpp-boost-beast-server/model/AnyType.h | 38 + .../cpp-boost-beast-server/model/Error.cpp | 762 ++++++++++ .../cpp-boost-beast-server/model/Error.h | 88 ++ .../model/NullableField.h | 187 +++ .../model/Oas31DeepEqual.h | 93 ++ .../model/Oas31ExactJson.h | 394 +++++ .../model/Oas31ExactNumber.cpp | 294 ++++ .../model/Oas31ExactNumber.h | 93 ++ .../model/Oas31SchemaIr.h | 266 ++++ .../model/Oas31SchemaRegistry.h | 23 + .../model/Oas31Validator.h | 1315 +++++++++++++++++ .../cpp-boost-beast-server/model/Pet.cpp | 921 ++++++++++++ .../cpp-boost-beast-server/model/Pet.h | 104 ++ .../cpp-boost-beast-server/model/User.cpp | 792 ++++++++++ .../cpp-boost-beast-server/model/User.h | 97 ++ .../model/ValidationTypes.h | 425 ++++++ .../model/schema_ir.generated.cpp | 201 +++ .../server/Authorizer.h | 47 + .../cpp-boost-beast-server/server/BodyJson.h | 255 ++++ .../server/HttpServer.cpp | 366 +++++ .../server/HttpServer.h | 69 + .../server/ParamCodecs.h | 209 +++ .../cpp-boost-beast-server/server/Problem.h | 173 +++ .../cpp-boost-beast-server/server/Responder.h | 103 ++ .../cpp-boost-beast-server/server/Router.h | 215 +++ 42 files changed, 9115 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/samples-cpp-boost-beast-server.yaml create mode 100644 bin/configs/cpp-boost-beast-server-petstore.yaml create mode 100644 docs/generators/cpp-boost-beast-server.md create mode 100644 samples/server/petstore/cpp-boost-beast-server/.openapi-generator-ignore create mode 100644 samples/server/petstore/cpp-boost-beast-server/.openapi-generator/FILES create mode 100644 samples/server/petstore/cpp-boost-beast-server/.openapi-generator/VERSION create mode 100644 samples/server/petstore/cpp-boost-beast-server/CMakeLists.txt create mode 100644 samples/server/petstore/cpp-boost-beast-server/README.md create mode 100644 samples/server/petstore/cpp-boost-beast-server/api/PetsApi.cpp create mode 100644 samples/server/petstore/cpp-boost-beast-server/api/PetsApi.h create mode 100644 samples/server/petstore/cpp-boost-beast-server/api/StoreApi.cpp create mode 100644 samples/server/petstore/cpp-boost-beast-server/api/StoreApi.h create mode 100644 samples/server/petstore/cpp-boost-beast-server/api/UsersApi.cpp create mode 100644 samples/server/petstore/cpp-boost-beast-server/api/UsersApi.h create mode 100644 samples/server/petstore/cpp-boost-beast-server/main.cpp create mode 100644 samples/server/petstore/cpp-boost-beast-server/model/AnyType.h create mode 100644 samples/server/petstore/cpp-boost-beast-server/model/Error.cpp create mode 100644 samples/server/petstore/cpp-boost-beast-server/model/Error.h create mode 100644 samples/server/petstore/cpp-boost-beast-server/model/NullableField.h create mode 100644 samples/server/petstore/cpp-boost-beast-server/model/Oas31DeepEqual.h create mode 100644 samples/server/petstore/cpp-boost-beast-server/model/Oas31ExactJson.h create mode 100644 samples/server/petstore/cpp-boost-beast-server/model/Oas31ExactNumber.cpp create mode 100644 samples/server/petstore/cpp-boost-beast-server/model/Oas31ExactNumber.h create mode 100644 samples/server/petstore/cpp-boost-beast-server/model/Oas31SchemaIr.h create mode 100644 samples/server/petstore/cpp-boost-beast-server/model/Oas31SchemaRegistry.h create mode 100644 samples/server/petstore/cpp-boost-beast-server/model/Oas31Validator.h create mode 100644 samples/server/petstore/cpp-boost-beast-server/model/Pet.cpp create mode 100644 samples/server/petstore/cpp-boost-beast-server/model/Pet.h create mode 100644 samples/server/petstore/cpp-boost-beast-server/model/User.cpp create mode 100644 samples/server/petstore/cpp-boost-beast-server/model/User.h create mode 100644 samples/server/petstore/cpp-boost-beast-server/model/ValidationTypes.h create mode 100644 samples/server/petstore/cpp-boost-beast-server/model/schema_ir.generated.cpp create mode 100644 samples/server/petstore/cpp-boost-beast-server/server/Authorizer.h create mode 100644 samples/server/petstore/cpp-boost-beast-server/server/BodyJson.h create mode 100644 samples/server/petstore/cpp-boost-beast-server/server/HttpServer.cpp create mode 100644 samples/server/petstore/cpp-boost-beast-server/server/HttpServer.h create mode 100644 samples/server/petstore/cpp-boost-beast-server/server/ParamCodecs.h create mode 100644 samples/server/petstore/cpp-boost-beast-server/server/Problem.h create mode 100644 samples/server/petstore/cpp-boost-beast-server/server/Responder.h create mode 100644 samples/server/petstore/cpp-boost-beast-server/server/Router.h diff --git a/.github/workflows/samples-cpp-boost-beast-server.yaml b/.github/workflows/samples-cpp-boost-beast-server.yaml new file mode 100644 index 000000000000..497a69f8134e --- /dev/null +++ b/.github/workflows/samples-cpp-boost-beast-server.yaml @@ -0,0 +1,55 @@ +name: Samples cpp boost beast server + +on: + push: + paths: + - "samples/server/petstore/cpp-boost-beast-server/**" + - ".github/workflows/samples-cpp-boost-beast-server.yaml" + pull_request: + paths: + - "samples/server/petstore/cpp-boost-beast-server/**" + - ".github/workflows/samples-cpp-boost-beast-server.yaml" + +jobs: + build: + name: Build cpp boost beast server + strategy: + matrix: + sample: + - samples/server/petstore/cpp-boost-beast-server + os: + - ubuntu-latest + - macOS-latest + - windows-latest + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v7 + + - name: Install dependencies (Linux) + if: matrix.os == 'ubuntu-latest' + run: | + sudo apt-get update + sudo apt-get install -y build-essential cmake libboost-dev libboost-json-dev libboost-url-dev + + - name: Install dependencies (macOS) + if: matrix.os == 'macOS-latest' + run: | + brew install boost cmake + + - name: Install dependencies (Windows) + if: matrix.os == 'windows-latest' + run: | + vcpkg install boost-json:x64-windows boost-url:x64-windows + shell: cmd + timeout-minutes: 20 + + - name: Build + working-directory: ${{ matrix.sample }} + run: | + if [ "${{ matrix.os }}" = "windows-latest" ]; then + cmake -S . -B build -DCMAKE_TOOLCHAIN_FILE="C:/vcpkg/scripts/buildsystems/vcpkg.cmake" + else + cmake -S . -B build -DCMAKE_EXPORT_COMPILE_COMMANDS=1 + fi + cmake --build build + shell: bash diff --git a/bin/configs/cpp-boost-beast-server-petstore.yaml b/bin/configs/cpp-boost-beast-server-petstore.yaml new file mode 100644 index 000000000000..1086e285ddc1 --- /dev/null +++ b/bin/configs/cpp-boost-beast-server-petstore.yaml @@ -0,0 +1,8 @@ +generatorName: cpp-boost-beast-server +outputDir: samples/server/petstore/cpp-boost-beast-server +inputSpec: modules/openapi-generator/src/test/resources/3_1/cpp-boost-beast-server/petstore.yaml +templateDir: modules/openapi-generator/src/main/resources/cpp-boost-beast-server +additionalProperties: + hideGenerationTimestamp: "true" + packageName: CppBoostBeastPetstoreServer + addApiImplStubs: "true" diff --git a/docs/generators/cpp-boost-beast-server.md b/docs/generators/cpp-boost-beast-server.md new file mode 100644 index 000000000000..04cea792fbc3 --- /dev/null +++ b/docs/generators/cpp-boost-beast-server.md @@ -0,0 +1,277 @@ +--- +title: Documentation for the cpp-boost-beast-server Generator +--- + +## METADATA + +| Property | Value | Notes | +| -------- | ----- | ----- | +| generator name | cpp-boost-beast-server | pass this to the generate command after -g | +| generator stability | BETA | | +| generator type | SERVER | | +| generator language | C++ | | +| generator default templating engine | mustache | | +| helpTxt | Generates a C++ Boost.Beast HTTP server. | | + +## 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. + +| Option | Description | Values | Default | +| ------ | ----------- | ------ | ------- | +|addApiImplStubs|Generate API implementation stubs that answer 501 problem+json and a sample main.cpp for quick start| |false| +|apiPackage|C++ namespace for apis (convention: name.space.api).| |org.openapitools.server.api| +|compileWithValidation|Emit schema-validation IR and kValidateOnDecode=true in generated ValidationTypes.h (default). Set to false to omit the IR.| |true| +|modelPackage|C++ namespace for models (convention: name.space.model).| |org.openapitools.server.model| +|packageName|C++ package and library name.| |CppBoostBeastServer| +|preserveAdditionalProperties|Retain undeclared JSON object members in generated object models and re-emit them; set to false for strict handling.| |false| +|tolerateNonNullableNulls|Treat explicit JSON null values as absent for generated model properties whose schemas do not allow null. Enabled by default; set to false for strict schema decoding.| |true| + +## IMPORT MAPPING + +| Type/Alias | Imports | +| ---------- | ------- | +|AnyType|#include "AnyType.h"| +|Null|#include <cstddef>| +|boost::json::value|#include <boost/json.hpp>| +|int32_t|#include <cstdint>| +|int64_t|#include <cstdint>| +|std::map|#include <map>| +|std::monostate|#include <variant>| +|std::nullptr_t|#include <cstddef>| +|std::optional|#include <optional>| +|std::shared_ptr|#include <memory>| +|std::string|#include <string>| +|std::variant|#include <variant>| +|std::vector|#include <vector>| + + +## INSTANTIATION TYPES + +| Type/Alias | Instantiated By | +| ---------- | --------------- | + + +## LANGUAGE PRIMITIVES + +

    +
  • bool
  • +
  • char
  • +
  • double
  • +
  • float
  • +
  • int
  • +
  • long
  • +
  • std::int32_t
  • +
  • std::int64_t
  • +
+ +## RESERVED WORDS + +
    +
  • NULL
  • +
  • alignas
  • +
  • alignof
  • +
  • and
  • +
  • and_eq
  • +
  • asm
  • +
  • auto
  • +
  • bitand
  • +
  • bitor
  • +
  • bool
  • +
  • break
  • +
  • case
  • +
  • catch
  • +
  • char
  • +
  • char16_t
  • +
  • char32_t
  • +
  • class
  • +
  • compl
  • +
  • concept
  • +
  • const
  • +
  • const_cast
  • +
  • constexpr
  • +
  • continue
  • +
  • decltype
  • +
  • default
  • +
  • delete
  • +
  • do
  • +
  • double
  • +
  • dynamic_cast
  • +
  • else
  • +
  • enum
  • +
  • explicit
  • +
  • export
  • +
  • extern
  • +
  • false
  • +
  • float
  • +
  • for
  • +
  • friend
  • +
  • goto
  • +
  • if
  • +
  • inline
  • +
  • int
  • +
  • linux
  • +
  • long
  • +
  • mutable
  • +
  • namespace
  • +
  • new
  • +
  • noexcept
  • +
  • not
  • +
  • not_eq
  • +
  • nullptr
  • +
  • operator
  • +
  • or
  • +
  • or_eq
  • +
  • private
  • +
  • protected
  • +
  • public
  • +
  • register
  • +
  • reinterpret_cast
  • +
  • requires
  • +
  • return
  • +
  • short
  • +
  • signed
  • +
  • sizeof
  • +
  • static
  • +
  • static_assert
  • +
  • static_cast
  • +
  • struct
  • +
  • switch
  • +
  • template
  • +
  • this
  • +
  • thread_local
  • +
  • throw
  • +
  • true
  • +
  • try
  • +
  • typedef
  • +
  • typeid
  • +
  • typename
  • +
  • union
  • +
  • unsigned
  • +
  • using
  • +
  • virtual
  • +
  • void
  • +
  • volatile
  • +
  • wchar_t
  • +
  • while
  • +
  • xor
  • +
  • xor_eq
  • +
+ +## FEATURE SET + + +### Client Modification Feature +| Name | Supported | Defined By | +| ---- | --------- | ---------- | +|BasePath|✗|ToolingExtension +|Authorizations|✗|ToolingExtension +|UserAgent|✗|ToolingExtension +|MockServer|✗|ToolingExtension + +### Data Type Feature +| Name | Supported | Defined By | +| ---- | --------- | ---------- | +|Custom|✗|OAS2,OAS3 +|Int32|✓|OAS2,OAS3 +|Int64|✓|OAS2,OAS3 +|Float|✓|OAS2,OAS3 +|Double|✓|OAS2,OAS3 +|Decimal|✗|ToolingExtension +|String|✓|OAS2,OAS3 +|Byte|✗|OAS2,OAS3 +|Binary|✗|OAS2,OAS3 +|Boolean|✓|OAS2,OAS3 +|Date|✗|OAS2,OAS3 +|DateTime|✗|OAS2,OAS3 +|Password|✗|OAS2,OAS3 +|File|✓|OAS2 +|Uuid|✗| +|Array|✓|OAS2,OAS3 +|Null|✓|OAS3 +|AnyType|✓|OAS2,OAS3 +|Object|✓|OAS2,OAS3 +|Maps|✓|ToolingExtension +|CollectionFormat|✓|OAS2 +|CollectionFormatMulti|✓|OAS2 +|Enum|✓|OAS2,OAS3 +|ArrayOfEnum|✓|ToolingExtension +|ArrayOfModel|✓|ToolingExtension +|ArrayOfCollectionOfPrimitives|✓|ToolingExtension +|ArrayOfCollectionOfModel|✓|ToolingExtension +|ArrayOfCollectionOfEnum|✓|ToolingExtension +|MapOfEnum|✓|ToolingExtension +|MapOfModel|✓|ToolingExtension +|MapOfCollectionOfPrimitives|✓|ToolingExtension +|MapOfCollectionOfModel|✓|ToolingExtension +|MapOfCollectionOfEnum|✓|ToolingExtension + +### Documentation Feature +| Name | Supported | Defined By | +| ---- | --------- | ---------- | +|Readme|✓|ToolingExtension +|Model|✓|ToolingExtension +|Api|✓|ToolingExtension + +### Global Feature +| Name | Supported | Defined By | +| ---- | --------- | ---------- | +|Host|✓|OAS2,OAS3 +|BasePath|✓|OAS2,OAS3 +|Info|✓|OAS2,OAS3 +|Schemes|✗|OAS2,OAS3 +|PartialSchemes|✓|OAS2,OAS3 +|Consumes|✓|OAS2 +|Produces|✓|OAS2 +|ExternalDocumentation|✓|OAS2,OAS3 +|Examples|✓|OAS2,OAS3 +|XMLStructureDefinitions|✗|OAS2,OAS3 +|MultiServer|✓|OAS3 +|ParameterizedServer|✗|OAS3 +|ParameterStyling|✓|OAS3 +|Callbacks|✗|OAS3 +|LinkObjects|✗|OAS3 + +### Parameter Feature +| Name | Supported | Defined By | +| ---- | --------- | ---------- | +|Path|✓|OAS2,OAS3 +|Query|✓|OAS2,OAS3 +|Header|✓|OAS2,OAS3 +|Body|✓|OAS2 +|FormUnencoded|✓|OAS2 +|FormMultipart|✓|OAS2 +|Cookie|✓|OAS3 + +### Schema Support Feature +| Name | Supported | Defined By | +| ---- | --------- | ---------- | +|Simple|✓|OAS2,OAS3 +|Composite|✓|OAS2,OAS3 +|Polymorphism|✓|OAS2,OAS3 +|Union|✓|OAS3 +|allOf|✓|OAS2,OAS3 +|anyOf|✓|OAS3 +|oneOf|✓|OAS3 +|not|✓|OAS3 + +### Security Feature +| Name | Supported | Defined By | +| ---- | --------- | ---------- | +|BasicAuth|✓|OAS2,OAS3 +|ApiKey|✓|OAS2,OAS3 +|OpenIDConnect|✗|OAS3 +|BearerToken|✓|OAS3 +|OAuth2_Implicit|✗|OAS2,OAS3 +|OAuth2_Password|✗|OAS2,OAS3 +|OAuth2_ClientCredentials|✗|OAS2,OAS3 +|OAuth2_AuthorizationCode|✗|OAS2,OAS3 +|SignatureAuth|✗|OAS3 +|AWSV4Signature|✗|ToolingExtension + +### Wire Format Feature +| Name | Supported | Defined By | +| ---- | --------- | ---------- | +|JSON|✓|OAS2,OAS3 +|XML|✗|OAS2,OAS3 +|PROTOBUF|✗|ToolingExtension +|Custom|✗|OAS2,OAS3 diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/CppBoostBeastServerTemplateModelAssembler.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/CppBoostBeastServerTemplateModelAssembler.java index 64ee8d74d019..5c98847c015a 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/CppBoostBeastServerTemplateModelAssembler.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/CppBoostBeastServerTemplateModelAssembler.java @@ -26,9 +26,11 @@ import org.openapitools.codegen.model.OperationsMap; import java.util.ArrayList; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Set; /** * Template-model assembly for the Boost.Beast server generator: converts each @@ -79,6 +81,29 @@ OperationsMap assemble(OperationsMap objs, List allModels) { op.vendorExtensions.put("x-server-security-groups", CppBoostBeastOperationFacts.effectiveSecurityGroups(sourceOpenApi, op)); } + Set modelClassNames = new HashSet<>(); + for (ModelMap modelMap : allModels) { + if (modelMap != null && modelMap.getModel() != null + && modelMap.getModel().classname != null) { + modelClassNames.add(modelMap.getModel().classname); + } + } + boolean anyModelUse = false; + for (CodegenOperation op : objs.getOperations().getOperation()) { + if (op == null || op.imports == null) { + continue; + } + for (String imported : op.imports) { + if (imported != null && modelClassNames.contains(imported)) { + anyModelUse = true; + break; + } + } + if (anyModelUse) { + break; + } + } + objs.put("x-server-has-model-use", anyModelUse); return objs; } diff --git a/modules/openapi-generator/src/main/resources/cpp-boost-beast-server/api-header.mustache b/modules/openapi-generator/src/main/resources/cpp-boost-beast-server/api-header.mustache index f5a49c89b1e5..a301acd5756b 100644 --- a/modules/openapi-generator/src/main/resources/cpp-boost-beast-server/api-header.mustache +++ b/modules/openapi-generator/src/main/resources/cpp-boost-beast-server/api-header.mustache @@ -26,9 +26,8 @@ namespace {{this}} { {{/apiNamespaceDeclarations}} -{{#imports}} -using namespace {{modelNamespace}}; -{{/imports}} +{{#x-server-has-model-use}}using namespace {{modelNamespace}}; +{{/x-server-has-model-use}} {{#operation}} // --------------------------------------------------------------------------- diff --git a/samples/server/petstore/cpp-boost-beast-server/.openapi-generator-ignore b/samples/server/petstore/cpp-boost-beast-server/.openapi-generator-ignore new file mode 100644 index 000000000000..7484ee590a38 --- /dev/null +++ b/samples/server/petstore/cpp-boost-beast-server/.openapi-generator-ignore @@ -0,0 +1,23 @@ +# OpenAPI Generator Ignore +# Generated by openapi-generator https://github.com/openapitools/openapi-generator + +# Use this file to prevent files from being overwritten by the generator. +# The patterns follow closely to .gitignore or .dockerignore. + +# As an example, the C# client generator defines ApiClient.cs. +# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line: +#ApiClient.cs + +# You can match any string of characters against a directory, file or extension with a single asterisk (*): +#foo/*/qux +# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux + +# You can recursively match patterns against a directory, file or extension with a double asterisk (**): +#foo/**/qux +# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux + +# You can also negate patterns with an exclamation (!). +# For example, you can ignore all files in a docs folder with the file extension .md: +#docs/*.md +# Then explicitly reverse the ignore rule for a single file: +#!docs/README.md diff --git a/samples/server/petstore/cpp-boost-beast-server/.openapi-generator/FILES b/samples/server/petstore/cpp-boost-beast-server/.openapi-generator/FILES new file mode 100644 index 000000000000..a12c9a0c3b67 --- /dev/null +++ b/samples/server/petstore/cpp-boost-beast-server/.openapi-generator/FILES @@ -0,0 +1,34 @@ +CMakeLists.txt +README.md +api/PetsApi.cpp +api/PetsApi.h +api/StoreApi.cpp +api/StoreApi.h +api/UsersApi.cpp +api/UsersApi.h +main.cpp +model/AnyType.h +model/Error.cpp +model/Error.h +model/NullableField.h +model/Oas31DeepEqual.h +model/Oas31ExactJson.h +model/Oas31ExactNumber.cpp +model/Oas31ExactNumber.h +model/Oas31SchemaIr.h +model/Oas31SchemaRegistry.h +model/Oas31Validator.h +model/Pet.cpp +model/Pet.h +model/User.cpp +model/User.h +model/ValidationTypes.h +model/schema_ir.generated.cpp +server/Authorizer.h +server/BodyJson.h +server/HttpServer.cpp +server/HttpServer.h +server/ParamCodecs.h +server/Problem.h +server/Responder.h +server/Router.h diff --git a/samples/server/petstore/cpp-boost-beast-server/.openapi-generator/VERSION b/samples/server/petstore/cpp-boost-beast-server/.openapi-generator/VERSION new file mode 100644 index 000000000000..32a8cfaceeb9 --- /dev/null +++ b/samples/server/petstore/cpp-boost-beast-server/.openapi-generator/VERSION @@ -0,0 +1 @@ +7.26.0-SNAPSHOT diff --git a/samples/server/petstore/cpp-boost-beast-server/CMakeLists.txt b/samples/server/petstore/cpp-boost-beast-server/CMakeLists.txt new file mode 100644 index 000000000000..46ccf52a7af8 --- /dev/null +++ b/samples/server/petstore/cpp-boost-beast-server/CMakeLists.txt @@ -0,0 +1,121 @@ +cmake_minimum_required(VERSION 3.14) +project(CppBoostBeastPetstoreServer VERSION 1.0.0 LANGUAGES CXX) + +include(GNUInstallDirs) + +if (POLICY CMP0167) + cmake_policy(SET CMP0167 OLD) +endif () + +set(BOOST_BOOST_TARGET_PREDEFINED FALSE) +set(BOOST_JSON_TARGET_PREDEFINED FALSE) +set(BOOST_URL_TARGET_PREDEFINED FALSE) +if (TARGET Boost::boost) + set(BOOST_BOOST_TARGET_PREDEFINED TRUE) +endif () +if (TARGET Boost::json) + set(BOOST_JSON_TARGET_PREDEFINED TRUE) +endif () +if (TARGET Boost::url) + set(BOOST_URL_TARGET_PREDEFINED TRUE) +endif () + +find_package(Boost 1.81 REQUIRED COMPONENTS json url) +# Imported targets created in this subdirectory are otherwise invisible to +# sibling consumers when this project is included with add_subdirectory(). +if (NOT BOOST_BOOST_TARGET_PREDEFINED) + set_property(TARGET Boost::boost PROPERTY IMPORTED_GLOBAL TRUE) +endif () +if (NOT BOOST_JSON_TARGET_PREDEFINED) + set_property(TARGET Boost::json PROPERTY IMPORTED_GLOBAL TRUE) +endif () +if (NOT BOOST_URL_TARGET_PREDEFINED) + set_property(TARGET Boost::url PROPERTY IMPORTED_GLOBAL TRUE) +endif () +set(THREADS_TARGET_PREDEFINED FALSE) +if (TARGET Threads::Threads) + set(THREADS_TARGET_PREDEFINED TRUE) +endif () +set(THREADS_PREFER_PTHREAD_FLAG TRUE) +find_package(Threads REQUIRED) +if (NOT THREADS_TARGET_PREDEFINED) + set_property(TARGET Threads::Threads PROPERTY IMPORTED_GLOBAL TRUE) +endif () + +# Boost.URL is consumed in header-only mode from HttpServer.cpp. +if (MSVC) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /W4") +else () + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fPIC -Wall") +endif () + + +add_library(CppBoostBeastPetstoreServer STATIC) + +set_property(TARGET CppBoostBeastPetstoreServer PROPERTY CXX_STANDARD 17) +set_property(TARGET CppBoostBeastPetstoreServer PROPERTY CXX_STANDARD_REQUIRED ON) +set_property(TARGET CppBoostBeastPetstoreServer PROPERTY CXX_EXTENSIONS OFF) + +target_sources(CppBoostBeastPetstoreServer PRIVATE +# models + model/Error.cpp + model/Error.h + model/Pet.cpp + model/Pet.h + model/User.cpp + model/User.h +# apis + api/PetsApi.cpp + api/PetsApi.h + api/StoreApi.cpp + api/StoreApi.h + api/UsersApi.cpp + api/UsersApi.h +# server runtime + server/Authorizer.h + server/BodyJson.h + server/HttpServer.cpp + server/HttpServer.h + server/ParamCodecs.h + server/Problem.h + server/Responder.h + server/Router.h +# shared model/validation support + model/AnyType.h + model/NullableField.h + model/Oas31DeepEqual.h + model/Oas31ExactNumber.cpp + model/Oas31ExactNumber.h + model/Oas31SchemaIr.h + model/Oas31ExactJson.h + model/Oas31Validator.h + model/ValidationTypes.h + model/schema_ir.generated.cpp + model/Oas31SchemaRegistry.h +) + +target_link_libraries(CppBoostBeastPetstoreServer + PUBLIC Boost::boost Boost::json Boost::url Threads::Threads) + +target_include_directories(CppBoostBeastPetstoreServer PUBLIC + $ + $ + $ + $ + $ + $ + $ + $) + +install(TARGETS CppBoostBeastPetstoreServer + ARCHIVE DESTINATION "${CMAKE_INSTALL_LIBDIR}") + +install(DIRECTORY api model server + DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME}" + FILES_MATCHING PATTERN "*.h") + +add_executable(${PROJECT_NAME}_main main.cpp) +set_property(TARGET ${PROJECT_NAME}_main PROPERTY CXX_STANDARD 17) +set_property(TARGET ${PROJECT_NAME}_main PROPERTY CXX_STANDARD_REQUIRED ON) +set_property(TARGET ${PROJECT_NAME}_main PROPERTY CXX_EXTENSIONS OFF) +target_link_libraries(${PROJECT_NAME}_main PRIVATE CppBoostBeastPetstoreServer) diff --git a/samples/server/petstore/cpp-boost-beast-server/README.md b/samples/server/petstore/cpp-boost-beast-server/README.md new file mode 100644 index 000000000000..98bef5860c85 --- /dev/null +++ b/samples/server/petstore/cpp-boost-beast-server/README.md @@ -0,0 +1,88 @@ +/** + * Petstore Server + * Sample petstore server for the cpp-boost-beast-server generator + * + * The version of the OpenAPI document: 1.0.0 + * + * NOTE: This class is auto generated by OpenAPI-Generator 7.26.0-SNAPSHOT. + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +# CppBoostBeastPetstoreServer — Boost.Beast server + +Sample petstore server for the cpp-boost-beast-server generator + +Generated from an OpenAPI document by **openapi-generator** (`cpp-boost-beast-server`). + +## Requirements + +- C++17 compiler +- CMake ≥ 3.14 +- Boost ≥ 1.81 (headers, `json`, and URL — Beast/Asio are header-only) + +## Layout + +- `api/` — generated service interfaces, typed request structs, per-operation + responders, and route registration (`Api::attach`) +- `model/` — generated model types plus the shared OAS 3.1 exact-validation + runtime (`Oas31*`) and schema registry +- `server/` — HTTP/1.1 runtime: `HttpServer`, `Router`, `Responder`, + `Problem` (RFC 9457), `Authorizer`, parameter codecs, JSON body conversion + +## Building + +```sh +cmake -S . -B build +cmake --build build +``` + +## Using + +Implement the generated `Api` service interfaces and attach them: + +```cpp +namespace api = org::openapitools::server::api; +namespace model = org::openapitools::server::model; + +class MyDefaultApi : public api::DefaultApi { + void getPetById(api::GetPetByIdRequest request, + api::RequestContext& context, + api::GetPetByIdResponder responder) override { + model::Pet pet; + pet.setId(request.petId); + responder.send200(std::move(pet)); + } +}; + +int main() { + boost::asio::io_context ioc; + auto router = std::make_shared(); + api::ServerOptions options; + options.authorizer = std::make_shared(); + auto server = std::make_shared( + ioc, router, options); + api::DefaultApi::attach(*server, + std::make_shared()); + server->listen(boost::asio::ip::tcp::endpoint{ + boost::asio::ip::make_address("0.0.0.0"), 8080}); + ioc.run(); +} +``` + +Requests are fully decoded and schema-validated before the service method +runs; serialization failures, malformed input, unknown routes (404), wrong +methods (405 + `Allow`), unsupported media types (415), oversized bodies +(413), and security denials (401) produce RFC 9457 `application/problem+json` +responses without application code. + +With `addApiImplStubs=true` a `main.cpp` and stub services (501 responses) +are generated for quick start. + +## Security + +Declared OpenAPI security requirements are enforced before dispatch: +credentials are extracted per scheme (API keys by location, raw +`Authorization` for HTTP schemes) and handed to your `Authorizer`. Without +an authorizer, secured operations deny by default. Credential values are +never logged. diff --git a/samples/server/petstore/cpp-boost-beast-server/api/PetsApi.cpp b/samples/server/petstore/cpp-boost-beast-server/api/PetsApi.cpp new file mode 100644 index 000000000000..4fe58dc6a138 --- /dev/null +++ b/samples/server/petstore/cpp-boost-beast-server/api/PetsApi.cpp @@ -0,0 +1,253 @@ +/** + * Petstore Server + * Sample petstore server for the cpp-boost-beast-server generator + * + * The version of the OpenAPI document: 1.0.0 + * + * NOTE: This class is auto generated by OpenAPI-Generator 7.26.0-SNAPSHOT. + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +/* + * PetsApi.cpp + */ + +#include "PetsApi.h" + +#include "BodyJson.h" +#include "ParamCodecs.h" +#include "Problem.h" +#include "Responder.h" +#include "Router.h" + +#include +#include +#include +#include +#include +#include + +namespace org { +namespace openapitools { +namespace server { +namespace api { + +void PetsApi::attach(HttpServer& server, std::shared_ptr impl) { + auto router = server.routerPtr(); + + // ------------------------------------------------------------------ + // POST /pets (createPet) + // ------------------------------------------------------------------ + { + SecurityGroups security; + + router->add( + "POST", + "/pets", + [impl](RequestContext& ctx, std::shared_ptr responderCore) { + CreatePetRequest request; + Problem problem; + bool invalid = false; + + + // ---- request body (Pet) ---- + { + auto contentTypeEntry = ctx.headers.find("content-type"); + std::string contentType = + contentTypeEntry != ctx.headers.end() + ? contentTypeEntry->second : std::string(); + std::size_t semicolon = contentType.find(';'); + if (semicolon != std::string::npos) { + contentType.resize(semicolon); + } + while (!contentType.empty() && contentType.back() == ' ') { + contentType.pop_back(); + } + static std::vector const kMediaTypes = { + "application/json" }; + bool supported = !contentType.empty() + ? std::find(kMediaTypes.begin(), kMediaTypes.end(), contentType) != kMediaTypes.end() + || std::find(kMediaTypes.begin(), kMediaTypes.end(), "*/*") != kMediaTypes.end() + : kMediaTypes.size() == 1; + if (!supported) { + responderCore->sendProblem(Problem::unsupportedMediaType(contentType)); + return; + } + try { + fromJsonBody(ctx.body, request.body); + } catch (std::invalid_argument const& error) { + Problem parseProblem = Problem::badRequest(error.what()); + parseProblem.withError("body", error.what()); + responderCore->sendProblem(std::move(parseProblem)); + return; + } + } + + if (invalid) { + problem.status = 400; + problem.title = "Bad Request"; + responderCore->sendProblem(std::move(problem)); + return; + } + + CreatePetResponder responder(responderCore); + impl->createPet(std::move(request), ctx, std::move(responder)); + }, + std::move(security), + "createPet"); + } + // ------------------------------------------------------------------ + // DELETE /pets/{petId} (deletePetById) + // ------------------------------------------------------------------ + { + SecurityGroups security; + + router->add( + "DELETE", + "/pets/{petId}", + [impl](RequestContext& ctx, std::shared_ptr responderCore) { + DeletePetByIdRequest request; + Problem problem; + bool invalid = false; + + // ---- parameter petId (path, simple) ---- + { + auto rawSegment = ctx.pathParams.find("petId"); + std::string encoded = + rawSegment != ctx.pathParams.end() ? rawSegment->second : std::string(); + std::string text; + text = percentDecode(encoded); + if (text.empty()) { + problem.withError("petId", "path parameter is missing or empty"); + invalid = true; + } else if (!parseScalar(text, request.petId)) { + problem.withError("petId", "path parameter is not a valid std::int64_t"); + invalid = true; + } + + + + } + + + if (invalid) { + problem.status = 400; + problem.title = "Bad Request"; + responderCore->sendProblem(std::move(problem)); + return; + } + + DeletePetByIdResponder responder(responderCore); + impl->deletePetById(std::move(request), ctx, std::move(responder)); + }, + std::move(security), + "deletePetById"); + } + // ------------------------------------------------------------------ + // GET /pets (listPets) + // ------------------------------------------------------------------ + { + SecurityGroups security; + + router->add( + "GET", + "/pets", + [impl](RequestContext& ctx, std::shared_ptr responderCore) { + ListPetsRequest request; + Problem problem; + bool invalid = false; + + // ---- parameter limit (query, form) ---- + { + auto values = ctx.query.equal_range("limit"); + bool present = values.first != values.second; + if (!present) { + // absent optional query parameter keeps its default value + } else if (!parseScalar(values.first->second, request.limit)) { + problem.withError("limit", "query parameter is not a valid std::int32_t"); + invalid = true; + } + + + + if (!invalid && present && static_cast(request.limit) < 1L) { + problem.withError("limit", "value is below the minimum"); + invalid = true; + } + + if (!invalid && present && static_cast(request.limit) > 100L) { + problem.withError("limit", "value is above the maximum"); + invalid = true; + } + + + } + + + if (invalid) { + problem.status = 400; + problem.title = "Bad Request"; + responderCore->sendProblem(std::move(problem)); + return; + } + + ListPetsResponder responder(responderCore); + impl->listPets(std::move(request), ctx, std::move(responder)); + }, + std::move(security), + "listPets"); + } + // ------------------------------------------------------------------ + // GET /pets/{petId} (showPetById) + // ------------------------------------------------------------------ + { + SecurityGroups security; + + router->add( + "GET", + "/pets/{petId}", + [impl](RequestContext& ctx, std::shared_ptr responderCore) { + ShowPetByIdRequest request; + Problem problem; + bool invalid = false; + + // ---- parameter petId (path, simple) ---- + { + auto rawSegment = ctx.pathParams.find("petId"); + std::string encoded = + rawSegment != ctx.pathParams.end() ? rawSegment->second : std::string(); + std::string text; + text = percentDecode(encoded); + if (text.empty()) { + problem.withError("petId", "path parameter is missing or empty"); + invalid = true; + } else if (!parseScalar(text, request.petId)) { + problem.withError("petId", "path parameter is not a valid std::int64_t"); + invalid = true; + } + + + + } + + + if (invalid) { + problem.status = 400; + problem.title = "Bad Request"; + responderCore->sendProblem(std::move(problem)); + return; + } + + ShowPetByIdResponder responder(responderCore); + impl->showPetById(std::move(request), ctx, std::move(responder)); + }, + std::move(security), + "showPetById"); + } +} + +} +} +} +} diff --git a/samples/server/petstore/cpp-boost-beast-server/api/PetsApi.h b/samples/server/petstore/cpp-boost-beast-server/api/PetsApi.h new file mode 100644 index 000000000000..cf8fe7dce7cf --- /dev/null +++ b/samples/server/petstore/cpp-boost-beast-server/api/PetsApi.h @@ -0,0 +1,244 @@ +/** + * Petstore Server + * Sample petstore server for the cpp-boost-beast-server generator + * + * The version of the OpenAPI document: 1.0.0 + * + * NOTE: This class is auto generated by OpenAPI-Generator 7.26.0-SNAPSHOT. + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +/* + * PetsApi.h + * + * + */ + +#ifndef ORG_OPENAPITOOLS_SERVER_API_PetsApi_H_ +#define ORG_OPENAPITOOLS_SERVER_API_PetsApi_H_ + +#include +#include +#include +#include +#include + +#include "HttpServer.h" +#include "Problem.h" +#include "Responder.h" +#include "Router.h" + +#include "Error.h" +#include "Pet.h" +#include + +namespace org { +namespace openapitools { +namespace server { +namespace api { + +using namespace org::openapitools::server::model; + +// --------------------------------------------------------------------------- + +/// Fully decoded request data for createPet. +struct CreatePetRequest { + Pet body{}; +}; + +/// Single-shot responder for createPet. Movable, thread-safe value +/// type; the second and later completions are ignored. +class CreatePetResponder { +public: + explicit CreatePetResponder( + std::shared_ptr core) + : core_(std::move(core)) {} + + void send201(Pet value) const { + core_->sendJson(201, value, "application/json"); + } + + void sendProblem(Problem problem) const { + core_->sendProblem(std::move(problem)); + } + + void sendNotImplemented(std::string const& operationId) const { + core_->sendNotImplemented(operationId); + } + +private: + std::shared_ptr core_; +}; + +// --------------------------------------------------------------------------- + +/// Fully decoded request data for deletePetById. +struct DeletePetByIdRequest { + std::int64_t petId{}; +}; + +/// Single-shot responder for deletePetById. Movable, thread-safe value +/// type; the second and later completions are ignored. +class DeletePetByIdResponder { +public: + explicit DeletePetByIdResponder( + std::shared_ptr core) + : core_(std::move(core)) {} + + void send204() const { + core_->sendEmpty(204); + } + + void sendProblem(Problem problem) const { + core_->sendProblem(std::move(problem)); + } + + void sendNotImplemented(std::string const& operationId) const { + core_->sendNotImplemented(operationId); + } + +private: + std::shared_ptr core_; +}; + +// --------------------------------------------------------------------------- + +/// Fully decoded request data for listPets. +struct ListPetsRequest { + std::int32_t limit{}; +}; + +/// Single-shot responder for listPets. Movable, thread-safe value +/// type; the second and later completions are ignored. +class ListPetsResponder { +public: + explicit ListPetsResponder( + std::shared_ptr core) + : core_(std::move(core)) {} + + void send200(std::vector> value) const { + core_->sendJson(200, value, "application/json"); + } + + void sendProblem(Problem problem) const { + core_->sendProblem(std::move(problem)); + } + + void sendNotImplemented(std::string const& operationId) const { + core_->sendNotImplemented(operationId); + } + +private: + std::shared_ptr core_; +}; + +// --------------------------------------------------------------------------- + +/// Fully decoded request data for showPetById. +struct ShowPetByIdRequest { + std::int64_t petId{}; +}; + +/// Single-shot responder for showPetById. Movable, thread-safe value +/// type; the second and later completions are ignored. +class ShowPetByIdResponder { +public: + explicit ShowPetByIdResponder( + std::shared_ptr core) + : core_(std::move(core)) {} + + void send200(Pet value) const { + core_->sendJson(200, value, "application/json"); + } + void send404(Error value) const { + core_->sendJson(404, value, "application/json"); + } + + void sendProblem(Problem problem) const { + core_->sendProblem(std::move(problem)); + } + + void sendNotImplemented(std::string const& operationId) const { + core_->sendNotImplemented(operationId); + } + +private: + std::shared_ptr core_; +}; + +/** + * Service interface for . Implementations receive fully + * decoded, validated requests and own their response completion. + */ +class PetsApi { +public: + virtual ~PetsApi() = default; + + virtual void createPet( + CreatePetRequest request, + RequestContext& context, + CreatePetResponder responder) = 0; + virtual void deletePetById( + DeletePetByIdRequest request, + RequestContext& context, + DeletePetByIdResponder responder) = 0; + virtual void listPets( + ListPetsRequest request, + RequestContext& context, + ListPetsResponder responder) = 0; + virtual void showPetById( + ShowPetByIdRequest request, + RequestContext& context, + ShowPetByIdResponder responder) = 0; + + /// Registers every PetsApi route on the server. + static void attach(HttpServer& server, std::shared_ptr impl); +}; + +/** + * Quick-start stub service: every operation answers 501 problem+json. + */ +class PetsApiStub : public PetsApi { +public: + void createPet( + CreatePetRequest request, + RequestContext& context, + CreatePetResponder responder) override { + (void)request; + (void)context; + responder.sendNotImplemented("createPet"); + } + void deletePetById( + DeletePetByIdRequest request, + RequestContext& context, + DeletePetByIdResponder responder) override { + (void)request; + (void)context; + responder.sendNotImplemented("deletePetById"); + } + void listPets( + ListPetsRequest request, + RequestContext& context, + ListPetsResponder responder) override { + (void)request; + (void)context; + responder.sendNotImplemented("listPets"); + } + void showPetById( + ShowPetByIdRequest request, + RequestContext& context, + ShowPetByIdResponder responder) override { + (void)request; + (void)context; + responder.sendNotImplemented("showPetById"); + } +}; + + +} +} +} +} + +#endif // ORG_OPENAPITOOLS_SERVER_API_PetsApi_H_ diff --git a/samples/server/petstore/cpp-boost-beast-server/api/StoreApi.cpp b/samples/server/petstore/cpp-boost-beast-server/api/StoreApi.cpp new file mode 100644 index 000000000000..c6fa402094ce --- /dev/null +++ b/samples/server/petstore/cpp-boost-beast-server/api/StoreApi.cpp @@ -0,0 +1,73 @@ +/** + * Petstore Server + * Sample petstore server for the cpp-boost-beast-server generator + * + * The version of the OpenAPI document: 1.0.0 + * + * NOTE: This class is auto generated by OpenAPI-Generator 7.26.0-SNAPSHOT. + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +/* + * StoreApi.cpp + */ + +#include "StoreApi.h" + +#include "BodyJson.h" +#include "ParamCodecs.h" +#include "Problem.h" +#include "Responder.h" +#include "Router.h" + +#include +#include +#include +#include +#include +#include + +namespace org { +namespace openapitools { +namespace server { +namespace api { + +void StoreApi::attach(HttpServer& server, std::shared_ptr impl) { + auto router = server.routerPtr(); + + // ------------------------------------------------------------------ + // GET /store/inventory (getInventory) + // ------------------------------------------------------------------ + { + SecurityGroups security; + + router->add( + "GET", + "/store/inventory", + [impl](RequestContext& ctx, std::shared_ptr responderCore) { + GetInventoryRequest request; + Problem problem; + bool invalid = false; + + + + if (invalid) { + problem.status = 400; + problem.title = "Bad Request"; + responderCore->sendProblem(std::move(problem)); + return; + } + + GetInventoryResponder responder(responderCore); + impl->getInventory(std::move(request), ctx, std::move(responder)); + }, + std::move(security), + "getInventory"); + } +} + +} +} +} +} diff --git a/samples/server/petstore/cpp-boost-beast-server/api/StoreApi.h b/samples/server/petstore/cpp-boost-beast-server/api/StoreApi.h new file mode 100644 index 000000000000..6011b9bb80ee --- /dev/null +++ b/samples/server/petstore/cpp-boost-beast-server/api/StoreApi.h @@ -0,0 +1,108 @@ +/** + * Petstore Server + * Sample petstore server for the cpp-boost-beast-server generator + * + * The version of the OpenAPI document: 1.0.0 + * + * NOTE: This class is auto generated by OpenAPI-Generator 7.26.0-SNAPSHOT. + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +/* + * StoreApi.h + * + * + */ + +#ifndef ORG_OPENAPITOOLS_SERVER_API_StoreApi_H_ +#define ORG_OPENAPITOOLS_SERVER_API_StoreApi_H_ + +#include +#include +#include +#include +#include + +#include "HttpServer.h" +#include "Problem.h" +#include "Responder.h" +#include "Router.h" + +#include + +namespace org { +namespace openapitools { +namespace server { +namespace api { + + +// --------------------------------------------------------------------------- + +/// Fully decoded request data for getInventory. +struct GetInventoryRequest { +}; + +/// Single-shot responder for getInventory. Movable, thread-safe value +/// type; the second and later completions are ignored. +class GetInventoryResponder { +public: + explicit GetInventoryResponder( + std::shared_ptr core) + : core_(std::move(core)) {} + + void send200(std::map value) const { + core_->sendJson(200, value, "application/json"); + } + + void sendProblem(Problem problem) const { + core_->sendProblem(std::move(problem)); + } + + void sendNotImplemented(std::string const& operationId) const { + core_->sendNotImplemented(operationId); + } + +private: + std::shared_ptr core_; +}; + +/** + * Service interface for . Implementations receive fully + * decoded, validated requests and own their response completion. + */ +class StoreApi { +public: + virtual ~StoreApi() = default; + + virtual void getInventory( + GetInventoryRequest request, + RequestContext& context, + GetInventoryResponder responder) = 0; + + /// Registers every StoreApi route on the server. + static void attach(HttpServer& server, std::shared_ptr impl); +}; + +/** + * Quick-start stub service: every operation answers 501 problem+json. + */ +class StoreApiStub : public StoreApi { +public: + void getInventory( + GetInventoryRequest request, + RequestContext& context, + GetInventoryResponder responder) override { + (void)request; + (void)context; + responder.sendNotImplemented("getInventory"); + } +}; + + +} +} +} +} + +#endif // ORG_OPENAPITOOLS_SERVER_API_StoreApi_H_ diff --git a/samples/server/petstore/cpp-boost-beast-server/api/UsersApi.cpp b/samples/server/petstore/cpp-boost-beast-server/api/UsersApi.cpp new file mode 100644 index 000000000000..c77221b48bd2 --- /dev/null +++ b/samples/server/petstore/cpp-boost-beast-server/api/UsersApi.cpp @@ -0,0 +1,91 @@ +/** + * Petstore Server + * Sample petstore server for the cpp-boost-beast-server generator + * + * The version of the OpenAPI document: 1.0.0 + * + * NOTE: This class is auto generated by OpenAPI-Generator 7.26.0-SNAPSHOT. + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +/* + * UsersApi.cpp + */ + +#include "UsersApi.h" + +#include "BodyJson.h" +#include "ParamCodecs.h" +#include "Problem.h" +#include "Responder.h" +#include "Router.h" + +#include +#include +#include +#include +#include +#include + +namespace org { +namespace openapitools { +namespace server { +namespace api { + +void UsersApi::attach(HttpServer& server, std::shared_ptr impl) { + auto router = server.routerPtr(); + + // ------------------------------------------------------------------ + // GET /users/{username} (getUserByName) + // ------------------------------------------------------------------ + { + SecurityGroups security; + + router->add( + "GET", + "/users/{username}", + [impl](RequestContext& ctx, std::shared_ptr responderCore) { + GetUserByNameRequest request; + Problem problem; + bool invalid = false; + + // ---- parameter username (path, simple) ---- + { + auto rawSegment = ctx.pathParams.find("username"); + std::string encoded = + rawSegment != ctx.pathParams.end() ? rawSegment->second : std::string(); + std::string text; + text = percentDecode(encoded); + if (text.empty()) { + problem.withError("username", "path parameter is missing or empty"); + invalid = true; + } else if (!parseScalar(text, request.username)) { + problem.withError("username", "path parameter is not a valid std::string"); + invalid = true; + } + + + + } + + + if (invalid) { + problem.status = 400; + problem.title = "Bad Request"; + responderCore->sendProblem(std::move(problem)); + return; + } + + GetUserByNameResponder responder(responderCore); + impl->getUserByName(std::move(request), ctx, std::move(responder)); + }, + std::move(security), + "getUserByName"); + } +} + +} +} +} +} diff --git a/samples/server/petstore/cpp-boost-beast-server/api/UsersApi.h b/samples/server/petstore/cpp-boost-beast-server/api/UsersApi.h new file mode 100644 index 000000000000..40bc9bd619e1 --- /dev/null +++ b/samples/server/petstore/cpp-boost-beast-server/api/UsersApi.h @@ -0,0 +1,115 @@ +/** + * Petstore Server + * Sample petstore server for the cpp-boost-beast-server generator + * + * The version of the OpenAPI document: 1.0.0 + * + * NOTE: This class is auto generated by OpenAPI-Generator 7.26.0-SNAPSHOT. + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +/* + * UsersApi.h + * + * + */ + +#ifndef ORG_OPENAPITOOLS_SERVER_API_UsersApi_H_ +#define ORG_OPENAPITOOLS_SERVER_API_UsersApi_H_ + +#include +#include +#include +#include +#include + +#include "HttpServer.h" +#include "Problem.h" +#include "Responder.h" +#include "Router.h" + +#include "Error.h" +#include "User.h" +#include + +namespace org { +namespace openapitools { +namespace server { +namespace api { + +using namespace org::openapitools::server::model; + +// --------------------------------------------------------------------------- + +/// Fully decoded request data for getUserByName. +struct GetUserByNameRequest { + std::string username{}; +}; + +/// Single-shot responder for getUserByName. Movable, thread-safe value +/// type; the second and later completions are ignored. +class GetUserByNameResponder { +public: + explicit GetUserByNameResponder( + std::shared_ptr core) + : core_(std::move(core)) {} + + void send200(User value) const { + core_->sendJson(200, value, "application/json"); + } + void send404(Error value) const { + core_->sendJson(404, value, "application/json"); + } + + void sendProblem(Problem problem) const { + core_->sendProblem(std::move(problem)); + } + + void sendNotImplemented(std::string const& operationId) const { + core_->sendNotImplemented(operationId); + } + +private: + std::shared_ptr core_; +}; + +/** + * Service interface for . Implementations receive fully + * decoded, validated requests and own their response completion. + */ +class UsersApi { +public: + virtual ~UsersApi() = default; + + virtual void getUserByName( + GetUserByNameRequest request, + RequestContext& context, + GetUserByNameResponder responder) = 0; + + /// Registers every UsersApi route on the server. + static void attach(HttpServer& server, std::shared_ptr impl); +}; + +/** + * Quick-start stub service: every operation answers 501 problem+json. + */ +class UsersApiStub : public UsersApi { +public: + void getUserByName( + GetUserByNameRequest request, + RequestContext& context, + GetUserByNameResponder responder) override { + (void)request; + (void)context; + responder.sendNotImplemented("getUserByName"); + } +}; + + +} +} +} +} + +#endif // ORG_OPENAPITOOLS_SERVER_API_UsersApi_H_ diff --git a/samples/server/petstore/cpp-boost-beast-server/main.cpp b/samples/server/petstore/cpp-boost-beast-server/main.cpp new file mode 100644 index 000000000000..ce39fa4354af --- /dev/null +++ b/samples/server/petstore/cpp-boost-beast-server/main.cpp @@ -0,0 +1,67 @@ +/** + * Petstore Server + * Sample petstore server for the cpp-boost-beast-server generator + * + * The version of the OpenAPI document: 1.0.0 + * + * NOTE: This class is auto generated by OpenAPI-Generator 7.26.0-SNAPSHOT. + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +// ============================================================================ +// main.cpp - quick-start server entry point (generated with +// addApiImplStubs=true). Every operation answers 501 problem+json until you +// provide a real service implementation. +// ============================================================================ +#include + +#include +#include +#include + +#include "HttpServer.h" +#include "Router.h" + +#include "PetsApi.h" +#include "StoreApi.h" +#include "UsersApi.h" +using namespace org; +using namespace openapitools; +using namespace server; +using namespace api; + +static void attachPetsApi(HttpServer& server) { + PetsApi::attach(server, std::make_shared()); +} +static void attachStoreApi(HttpServer& server) { + StoreApi::attach(server, std::make_shared()); +} +static void attachUsersApi(HttpServer& server) { + UsersApi::attach(server, std::make_shared()); +} + +int main() { + unsigned port = 8080; + if (char const* portText = std::getenv("PORT")) { + port = static_cast(std::strtoul(portText, nullptr, 10)); + } + + try { + boost::asio::io_context ioc; + auto router = std::make_shared(); + auto server = std::make_shared(ioc, router); + attachPetsApi(*server); + attachStoreApi(*server); + attachUsersApi(*server); + server->listen(boost::asio::ip::tcp::endpoint{ + boost::asio::ip::make_address("0.0.0.0"), + static_cast(port)}); + std::cout << "CppBoostBeastPetstoreServer listening on 0.0.0.0:" << port << "\n"; + ioc.run(); + } catch (std::exception const& error) { + std::cerr << "fatal: " << error.what() << "\n"; + return EXIT_FAILURE; + } + return EXIT_SUCCESS; +} diff --git a/samples/server/petstore/cpp-boost-beast-server/model/AnyType.h b/samples/server/petstore/cpp-boost-beast-server/model/AnyType.h new file mode 100644 index 000000000000..86f5ca6f9f57 --- /dev/null +++ b/samples/server/petstore/cpp-boost-beast-server/model/AnyType.h @@ -0,0 +1,38 @@ +/** + * Petstore Server + * Sample petstore server for the cpp-boost-beast-server generator + * + * The version of the OpenAPI document: 1.0.0 + * + * NOTE: This class is auto generated by OpenAPI-Generator 7.26.0-SNAPSHOT. + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +/* + * AnyType.h + * + * Represents any JSON type using boost::json::value + */ + +#ifndef ORG_OPENAPITOOLS_SERVER_MODEL_ANYTYPE_H_ +#define ORG_OPENAPITOOLS_SERVER_MODEL_ANYTYPE_H_ + +#include + +namespace org { +namespace openapitools { +namespace server { +namespace model { + +/** + * AnyType is an alias for boost::json::value to represent any JSON value. + */ +using AnyType = boost::json::value; + +} +} +} +} + +#endif /* ORG_OPENAPITOOLS_SERVER_MODEL_ANYTYPE_H_ */ diff --git a/samples/server/petstore/cpp-boost-beast-server/model/Error.cpp b/samples/server/petstore/cpp-boost-beast-server/model/Error.cpp new file mode 100644 index 000000000000..d3aec207e57f --- /dev/null +++ b/samples/server/petstore/cpp-boost-beast-server/model/Error.cpp @@ -0,0 +1,762 @@ +/** + * Petstore Server + * Sample petstore server for the cpp-boost-beast-server generator + * + * The version of the OpenAPI document: 1.0.0 + * + * NOTE: This class is auto generated by OpenAPI-Generator 7.26.0-SNAPSHOT. + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +// ============================================================================ +// Validation scope (client-side vs full JSON Schema validation) +// ============================================================================ +// The generated client performs structural and composition validation to +// ensure wire-format correctness, but is NOT a full JSON Schema meta-schema +// validator: +// +// ✓ oneOf exactly-one match enforcement (shared schema evaluator per branch) +// ✓ anyOf at-least-one match enforcement (shared schema evaluator per branch) +// ✓ discriminator value enforcement (unknown → fall through to structural) +// ✓ type validation, incl. type arrays with a literal "null" member +// ✓ mathematical integer semantics (1 and 1.0 both accepted as integer) +// ✓ enum / const membership, incl. deep (array/object) JSON members +// ✓ boolean value-schemas (true always matches, false never matches) +// ✓ required property presence in object JSON +// ✓ numeric range validation (minimum, maximum, exclusiveMin, exclusiveMax) +// ✓ numeric multipleOf validation (exact decimal lexemes) +// ✓ string length validation (minLength, maxLength) +// ✓ string pattern validation (ECMA-262 regex subset; fail-closed outside) +// ✓ patternProperties and propertyNames +// ✓ additionalProperties (false → reject; typed schemas densified) +// ✓ array length (minItems / maxItems), items and prefixItems validation +// ✓ array uniqueItems validation +// ✓ minProperties / maxProperties (exact count bounds) +// ✓ dependentRequired, contains (min/maxContains as exact count bounds) +// ✓ `not` subschemas via the shared IR evaluator +// ✓ if/then/else subschemas densified into the IR (bare unreferenced +// conditionals are annotated, not asserted) +// ✓ nested error-path diagnostics (e.g. ".field[0].nested") +// +// Annotation-only per JSON Schema 2020-12 §8.2.6 (no output assertions): +// format, contentEncoding, contentMediaType, contentSchema, $comment, and +// other annotation-vocabulary keywords. Unknown keywords are preserved as +// annotations and never affect accept/reject verdicts. +// +// For full JSON Schema validation, use a dedicated validator library +// (e.g. valijson, nlohmann/json-schema-validator) on the deserialized +// value before application use. The client's checks guarantee correct +// parse/serialization of valid instances matching the generated types. +// ============================================================================ + +#include "Error.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "ValidationTypes.h" +#include "Oas31ExactJson.h" +#include "Oas31Validator.h" +#include "Oas31SchemaRegistry.h" + +namespace org { +namespace openapitools { +namespace server { +namespace model { + +namespace { + +// Trait to detect types with toJsonValue() const member. +template +struct HasModelToJsonValue : std::false_type {}; + +template +struct HasModelToJsonValue().toJsonValue())>> : std::true_type {}; + +// Trait: detects whether a type has fromJsonValue member +template +struct HasFromJsonValueMethod : std::false_type {}; + +template +struct HasFromJsonValueMethod().fromJsonValue(std::declval()))>> + : std::true_type {}; + +// Trait: detects specialization of a template +template class Template> +struct IsSpecialization : std::false_type {}; + +template