diff --git a/docs/generators/dart-dio.md b/docs/generators/dart-dio.md index cd1a3a5974d5..a639ca15e1b6 100644 --- a/docs/generators/dart-dio.md +++ b/docs/generators/dart-dio.md @@ -26,6 +26,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl |equalityCheckMethod|Specify equality check method. Takes effect only in case if serializationLibrary is json_serializable.|
**default**
[DEFAULT] Built in hash code generation method
**equatable**
Uses equatable library for equality checking
|default| |finalProperties|Whether properties are marked as final when using Json Serializable for serialization| |true| |legacyDiscriminatorBehavior|Set to false for generators with better support for discriminators. (Python, Java, Go, PowerShell, C# have this enabled by default).|
**true**
The mapping in the discriminator includes descendent schemas that allOf inherit from self and the discriminator mapping schemas in the OAS document.
**false**
The mapping in the discriminator includes any descendent schemas that allOf inherit from self, any oneOf schemas, any anyOf schemas, any x-discriminator-values, and the discriminator mapping schemas in the OAS document AND Codegen validates that oneOf and anyOf schemas contain the required discriminator and throws an error if the discriminator is missing.
|true| +|patchOnly|Only apply Optional<T> to PATCH operation request bodies (requires useOptional=true)| |false| |prependFormOrBodyParameters|Add form or body parameters to the beginning of the parameter list.| |false| |pubAuthor|Author name in generated pubspec| |Author| |pubAuthorEmail|Email address of the author in generated pubspec| |author@homepage| @@ -42,6 +43,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl |sortParamsByRequiredFlag|Sort method arguments to place required parameters before optional parameters.| |true| |sourceFolder|source folder for generated code| |src| |useEnumExtension|Allow the 'x-enum-values' extension for enums| |false| +|useOptional|Use Optional<T> to distinguish absent, null, and present for optional fields (Dart 3+)| |false| ## IMPORT MAPPING diff --git a/docs/generators/dart.md b/docs/generators/dart.md index e5f4d8227077..e875bd264e3a 100644 --- a/docs/generators/dart.md +++ b/docs/generators/dart.md @@ -23,6 +23,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl |ensureUniqueParams|Whether to ensure parameter names are unique in an operation (rename parameters that are not).| |true| |enumUnknownDefaultCase|If the server adds new enum cases, that are unknown by an old spec/client, the client will fail to parse the network response.With this option enabled, each enum will have a new case, 'unknown_default_open_api', so that when the server sends an enum case that is not known by the client/spec, they can safely fallback to this case.|
**false**
No changes to the enum's are made, this is the default option.
**true**
With this option enabled, each enum will have a new case, 'unknown_default_open_api', so that when the enum case sent by the server is not known by the client/spec, can safely be decoded to this case.
|false| |legacyDiscriminatorBehavior|Set to false for generators with better support for discriminators. (Python, Java, Go, PowerShell, C# have this enabled by default).|
**true**
The mapping in the discriminator includes descendent schemas that allOf inherit from self and the discriminator mapping schemas in the OAS document.
**false**
The mapping in the discriminator includes any descendent schemas that allOf inherit from self, any oneOf schemas, any anyOf schemas, any x-discriminator-values, and the discriminator mapping schemas in the OAS document AND Codegen validates that oneOf and anyOf schemas contain the required discriminator and throws an error if the discriminator is missing.
|true| +|patchOnly|Only apply Optional<T> to PATCH operation request bodies (requires useOptional=true)| |false| |prependFormOrBodyParameters|Add form or body parameters to the beginning of the parameter list.| |false| |pubAuthor|Author name in generated pubspec| |Author| |pubAuthorEmail|Email address of the author in generated pubspec| |author@homepage| @@ -38,6 +39,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl |sortParamsByRequiredFlag|Sort method arguments to place required parameters before optional parameters.| |true| |sourceFolder|source folder for generated code| |src| |useEnumExtension|Allow the 'x-enum-values' extension for enums| |false| +|useOptional|Use Optional<T> to distinguish absent, null, and present for optional fields (Dart 3+)| |false| ## IMPORT MAPPING diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractDartCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractDartCodegen.java index 1c06f6773172..4d7eb4dbe7c1 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractDartCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractDartCodegen.java @@ -1,9 +1,12 @@ package org.openapitools.codegen.languages; import com.google.common.collect.Sets; +import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.Operation; import io.swagger.v3.oas.models.media.Schema; import io.swagger.v3.oas.models.media.StringSchema; +import io.swagger.v3.oas.models.parameters.Parameter; +import io.swagger.v3.oas.models.parameters.RequestBody; import io.swagger.v3.oas.models.servers.Server; import lombok.Setter; import org.apache.commons.io.FilenameUtils; @@ -45,6 +48,8 @@ public abstract class AbstractDartCodegen extends DefaultCodegen { public static final String PUB_REPOSITORY = "pubRepository"; public static final String PUB_PUBLISH_TO = "pubPublishTo"; public static final String USE_ENUM_EXTENSION = "useEnumExtension"; + public static final String USE_OPTIONAL = "useOptional"; + public static final String PATCH_ONLY = "patchOnly"; @Setter protected String pubLibrary = "openapi.api"; @Setter protected String pubName = "openapi"; @@ -56,8 +61,12 @@ public abstract class AbstractDartCodegen extends DefaultCodegen { @Setter protected String pubRepository = null; @Setter protected String pubPublishTo = null; @Setter protected boolean useEnumExtension = false; + @Setter protected boolean useOptional = false; + @Setter protected boolean patchOnly = false; @Setter protected String sourceFolder = "src"; protected String libPath = "lib" + File.separator; + + protected Set patchRequestSchemas = new HashSet<>(); protected String apiDocPath = "doc/"; protected String modelDocPath = "doc/"; protected String apiTestPath = "test" + File.separator; @@ -196,6 +205,8 @@ public AbstractDartCodegen() { addOption(PUB_REPOSITORY, "Repository in generated pubspec", pubRepository); addOption(PUB_PUBLISH_TO, "Publish_to in generated pubspec", pubPublishTo); addOption(USE_ENUM_EXTENSION, "Allow the 'x-enum-values' extension for enums", String.valueOf(useEnumExtension)); + addOption(USE_OPTIONAL, "Use Optional to distinguish absent, null, and present for optional fields (Dart 3+)", String.valueOf(useOptional)); + addOption(PATCH_ONLY, "Only apply Optional to PATCH operation request bodies (requires useOptional=true)", String.valueOf(patchOnly)); addOption(CodegenConstants.SOURCE_FOLDER, CodegenConstants.SOURCE_FOLDER_DESC, sourceFolder); } @@ -302,6 +313,24 @@ public void processOpts() { additionalProperties.put(USE_ENUM_EXTENSION, useEnumExtension); } + if (additionalProperties.containsKey(USE_OPTIONAL)) { + this.setUseOptional(convertPropertyToBooleanAndWriteBack(USE_OPTIONAL)); + } else { + additionalProperties.put(USE_OPTIONAL, useOptional); + } + + if (additionalProperties.containsKey(PATCH_ONLY)) { + this.setPatchOnly(convertPropertyToBooleanAndWriteBack(PATCH_ONLY)); + } else { + additionalProperties.put(PATCH_ONLY, patchOnly); + } + + if (patchOnly && !useOptional) { + LOGGER.warn("patchOnly=true requires useOptional=true. Setting useOptional=true."); + this.setUseOptional(true); + additionalProperties.put(USE_OPTIONAL, true); + } + if (additionalProperties.containsKey(CodegenConstants.SOURCE_FOLDER)) { String srcFolder = (String) additionalProperties.get(CodegenConstants.SOURCE_FOLDER); this.setSourceFolder(srcFolder.replace('/', File.separatorChar)); @@ -545,6 +574,35 @@ public String getTypeDeclaration(Schema p) { return super.getTypeDeclaration(p); } + @Override + public void preprocessOpenAPI(OpenAPI openAPI) { + super.preprocessOpenAPI(openAPI); + + if (patchOnly && openAPI.getPaths() != null) { + openAPI.getPaths().forEach((path, pathItem) -> { + if (pathItem.getPatch() != null) { + Operation patchOp = pathItem.getPatch(); + if (patchOp.getRequestBody() != null) { + RequestBody requestBody = ModelUtils.getReferencedRequestBody(openAPI, patchOp.getRequestBody()); + if (requestBody != null && requestBody.getContent() != null) { + requestBody.getContent().forEach((mediaType, content) -> { + if (content.getSchema() != null) { + String ref = content.getSchema().get$ref(); + if (ref != null) { + String schemaName = ModelUtils.getSimpleRef(ref); + String modelName = toModelName(schemaName); + patchRequestSchemas.add(modelName); + LOGGER.info("Identified '{}' as PATCH request schema (will use Optional)", modelName); + } + } + }); + } + } + } + }); + } + } + @Override public String getSchemaType(Schema p) { String openAPIType = super.getSchemaType(p); @@ -559,7 +617,49 @@ public String getSchemaType(Schema p) { @Override public ModelsMap postProcessModels(ModelsMap objs) { - return postProcessModelsEnum(objs); + objs = postProcessModelsEnum(objs); + + if (useOptional) { + for (ModelMap modelMap : objs.getModels()) { + CodegenModel model = modelMap.getModel(); + + boolean shouldUseOptional; + + if (patchOnly) { + shouldUseOptional = patchRequestSchemas.contains(model.classname); + } else { + Boolean schemaUseOptional = (Boolean) model.vendorExtensions.get("x-use-optional"); + shouldUseOptional = schemaUseOptional == null || schemaUseOptional; + } + + if (shouldUseOptional) { + for (CodegenProperty prop : model.vars) { + if (!prop.required && !prop.dataType.startsWith("Optional<")) { + wrapPropertyWithOptional(prop); + } + } + } + } + } + + return objs; + } + + private void wrapPropertyWithOptional(CodegenProperty property) { + property.vendorExtensions.put("x-unwrapped-datatype", property.dataType); + property.vendorExtensions.put("x-is-optional", true); + property.vendorExtensions.put("x-original-is-number", property.isNumber); + property.vendorExtensions.put("x-original-is-integer", property.isInteger); + + boolean hasNullableSuffix = property.dataType.endsWith("?"); + String baseType = hasNullableSuffix ? property.dataType.substring(0, property.dataType.length() - 1) : property.dataType; + property.dataType = "Optional<" + baseType + "?" + ">"; + + if (property.datatypeWithEnum != null && !property.datatypeWithEnum.startsWith("Optional<")) { + hasNullableSuffix = property.datatypeWithEnum.endsWith("?"); + baseType = hasNullableSuffix ? property.datatypeWithEnum.substring(0, property.datatypeWithEnum.length() - 1) : property.datatypeWithEnum; + property.datatypeWithEnum = "Optional<" + baseType + "?" + ">"; + } } @Override @@ -624,6 +724,19 @@ public CodegenProperty fromProperty(String name, Schema p, boolean required) { return property; } + @Override + public CodegenParameter fromParameter(Parameter parameter, Set imports) { + final CodegenParameter param = super.fromParameter(parameter, imports); + + if (useOptional && param.dataType != null && param.dataType.startsWith("Optional<")) { + param.dataType = param.dataType.substring("Optional<".length(), param.dataType.length() - 1); + param.vendorExtensions.remove("x-is-optional"); + param.vendorExtensions.remove("x-unwrapped-datatype"); + } + + return param; + } + @Override public CodegenOperation fromOperation(String path, String httpMethod, Operation operation, List servers) { final CodegenOperation op = super.fromOperation(path, httpMethod, operation, servers); @@ -660,6 +773,13 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List ops = operations.getOperation(); for (CodegenOperation op : ops) { + if (useOptional) { + unwrapOptionalFromParameters(op.pathParams); + unwrapOptionalFromParameters(op.queryParams); + unwrapOptionalFromParameters(op.headerParams); + unwrapOptionalFromParameters(op.formParams); + } + if (op.hasConsumes) { if (!op.formParams.isEmpty() || op.isMultipart) { // DefaultCodegen only sets this if the first consumes mediaType @@ -681,6 +801,16 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List params) { + if (params == null) return; + for (CodegenParameter param : params) { + if (param.dataType != null && param.dataType.startsWith("Optional<")) { + param.dataType = param.dataType.substring("Optional<".length(), param.dataType.length() - 1); + param.vendorExtensions.remove("x-is-optional"); + } + } + } + private List> prioritizeContentTypes(List> consumes) { if (consumes.size() <= 1) { // no need to change any order diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/DartClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/DartClientCodegen.java index a9ada095aa40..bcb5ebe195aa 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/DartClientCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/DartClientCodegen.java @@ -74,6 +74,11 @@ public void processOpts() { supportingFiles.add(new SupportingFile("auth/http_bearer_auth.mustache", authFolder, "http_bearer_auth.dart")); supportingFiles.add(new SupportingFile("auth/api_key_auth.mustache", authFolder, "api_key_auth.dart")); supportingFiles.add(new SupportingFile("auth/oauth.mustache", authFolder, "oauth.dart")); + + if (useOptional) { + supportingFiles.add(new SupportingFile("optional.mustache", libPath, "optional.dart")); + } + supportingFiles.add(new SupportingFile("git_push.sh.mustache", "", "git_push.sh")); supportingFiles.add(new SupportingFile("gitignore.mustache", "", ".gitignore")); supportingFiles.add(new SupportingFile("README.mustache", "", "README.md")); diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/DartDioClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/DartDioClientCodegen.java index d9d038746372..73132481e383 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/DartDioClientCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/DartDioClientCodegen.java @@ -220,6 +220,10 @@ public void processOpts() { supportingFiles.add(new SupportingFile("auth/oauth.mustache", authFolder, "oauth.dart")); supportingFiles.add(new SupportingFile("auth/auth.mustache", authFolder, "auth.dart")); + if (useOptional) { + supportingFiles.add(new SupportingFile("optional.mustache", srcFolder, "optional.dart")); + } + configureSerializationLibrary(srcFolder); configureEqualityCheckMethod(srcFolder); configureDateLibrary(srcFolder); diff --git a/modules/openapi-generator/src/main/resources/dart/libraries/dio/optional.mustache b/modules/openapi-generator/src/main/resources/dart/libraries/dio/optional.mustache new file mode 100644 index 000000000000..9a2747da4e52 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/dart/libraries/dio/optional.mustache @@ -0,0 +1,137 @@ +{{>header}} +import 'package:json_annotation/json_annotation.dart'; + +/// Represents an optional value that can be either absent or present. +/// +/// This is used to distinguish between three states in PATCH operations: +/// - Absent: Field is not set (omitted from JSON) +/// - Present with null: Field is explicitly set to null +/// - Present with value: Field has a value +/// +/// Example usage: +/// ```dart +/// // Field absent - not sent in request +/// final patch1 = Model(); +/// +/// // Field explicitly null - sends {"field": null} +/// final patch2 = Model(field: const Optional.present(null)); +/// +/// // Field has value - sends {"field": "value"} +/// final patch3 = Model(field: const Optional.present('value')); +/// ``` +sealed class Optional { + const Optional(); + + /// Creates an Optional with an absent value (not set). + const factory Optional.absent() = Absent; + + /// Creates an Optional with a present value (can be null). + const factory Optional.present(T value) = Present; + + /// Returns true if this Optional has a value (even if that value is null). + bool get isPresent; + + /// Returns true if this Optional does not have a value. + bool get isEmpty => !isPresent; + + /// Returns the value if present, throws if absent. + T get value; + + /// Returns the value if present, otherwise returns [defaultValue]. + T orElse(T defaultValue); + + /// Returns the value if present, otherwise returns the result of calling [defaultValue]. + T orElseGet(T Function() defaultValue); + + /// Maps the value if present using [transform], otherwise returns an absent Optional. + Optional map(R Function(T value) transform); +} + +/// Represents an absent Optional value. +final class Absent extends Optional { + const Absent(); + + @override + bool get isPresent => false; + + @override + T get value => throw StateError('No value present'); + + @override + T orElse(T defaultValue) => defaultValue; + + @override + T orElseGet(T Function() defaultValue) => defaultValue(); + + @override + Optional map(R Function(T value) transform) => const Absent(); + + @override + bool operator ==(Object other) => other is Absent; + + @override + int get hashCode => 0; + + @override + String toString() => 'Optional.absent()'; +} + +/// Represents a present Optional value. +final class Present extends Optional { + const Present(this._value); + + final T _value; + + @override + bool get isPresent => true; + + @override + T get value => _value; + + @override + T orElse(T defaultValue) => _value; + + @override + T orElseGet(T Function() defaultValue) => _value; + + @override + Optional map(R Function(T value) transform) => Optional.present(transform(_value)); + + @override + bool operator ==(Object other) => + identical(this, other) || + (other is Present && _value == other._value); + + @override + int get hashCode => _value.hashCode; + + @override + String toString() => 'Optional.present($_value)'; +} + +class _OptionalAbsentSentinel { + const _OptionalAbsentSentinel(); +} + +const _optionalAbsentSentinel = _OptionalAbsentSentinel(); + +/// Used with @JsonKey(readValue:) to distinguish absent keys from null values. +Object? readOptionalValue(Map map, String key) { + return map.containsKey(key) ? map[key] : _optionalAbsentSentinel; +} + +class OptionalConverter implements JsonConverter, Object?> { + const OptionalConverter(); + + @override + Optional fromJson(Object? json) { + return json is _OptionalAbsentSentinel + ? const Optional.absent() + : Optional.present(json as T); + } + + @override + Object? toJson(Optional object) { + return object.isPresent ? object.value : null; + } +} diff --git a/modules/openapi-generator/src/main/resources/dart/libraries/dio/serialization/json_serializable/class.mustache b/modules/openapi-generator/src/main/resources/dart/libraries/dio/serialization/json_serializable/class.mustache index bee52d4d3bbd..73e37f9c373e 100644 --- a/modules/openapi-generator/src/main/resources/dart/libraries/dio/serialization/json_serializable/class.mustache +++ b/modules/openapi-generator/src/main/resources/dart/libraries/dio/serialization/json_serializable/class.mustache @@ -5,6 +5,9 @@ import 'package:json_annotation/json_annotation.dart'; {{#useEquatable}} import 'package:equatable/src/equatable_utils.dart'; {{/useEquatable}} +{{#useOptional}} +import 'package:{{pubName}}/src/optional.dart'; +{{/useOptional}} part '{{classFilename}}.g.dart'; @@ -49,11 +52,13 @@ class {{{classname}}} { @Deprecated('{{{name}}} has been deprecated') {{/deprecated}} {{^isBinary}} - @JsonKey( +{{^required}}{{#useOptional}} @OptionalConverter() +{{/useOptional}}{{/required}} @JsonKey( {{#defaultValue}}defaultValue: {{{defaultValue}}},{{/defaultValue}} name: r'{{{baseName}}}', required: {{#required}}true{{/required}}{{^required}}false{{/required}}, - includeIfNull: {{#required}}{{#isNullable}}true{{/isNullable}}{{^isNullable}}false{{/isNullable}}{{/required}}{{^required}}false{{/required}}, + includeIfNull: {{#required}}{{#isNullable}}true{{/isNullable}}{{^isNullable}}false{{/isNullable}}{{/required}}{{^required}}false{{/required}},{{^required}}{{#useOptional}} + readValue: readOptionalValue,{{/useOptional}}{{/required}} {{#isEnumOrRef}} {{#enumUnknownDefaultCase}} unknownEnumValue: {{{datatypeWithEnum}}}.unknownDefaultOpenApi, @@ -70,7 +75,7 @@ class {{{classname}}} { {{#finalProperties}}final {{/finalProperties}}{{{datatypeWithEnum}}}{{#isNullable}}?{{/isNullable}} {{{name}}}; {{/required}} {{^required}} - {{#finalProperties}}final {{/finalProperties}}{{{datatypeWithEnum}}}? {{{name}}}; + {{#finalProperties}}final {{/finalProperties}}{{{datatypeWithEnum}}}{{^useOptional}}?{{/useOptional}} {{{name}}}; {{/required}} @@ -146,4 +151,4 @@ class {{{classname}}} { {{/mostInnerItems}} {{/isContainer}} {{/isEnum}} -{{/vars}} \ No newline at end of file +{{/vars}} diff --git a/modules/openapi-generator/src/main/resources/dart/libraries/dio/serialization/json_serializable/dart_constructor.mustache b/modules/openapi-generator/src/main/resources/dart/libraries/dio/serialization/json_serializable/dart_constructor.mustache index 3b99f0c54016..b96c0d361835 100644 --- a/modules/openapi-generator/src/main/resources/dart/libraries/dio/serialization/json_serializable/dart_constructor.mustache +++ b/modules/openapi-generator/src/main/resources/dart/libraries/dio/serialization/json_serializable/dart_constructor.mustache @@ -6,6 +6,6 @@ A field is required in Dart when it is required && !defaultValue in OAS }} - {{#required}}{{^defaultValue}}required {{/defaultValue}}{{/required}} this.{{{name}}}{{#defaultValue}} = {{#isEnum}}{{^isContainer}}const {{{enumName}}}._({{/isContainer}}{{/isEnum}}{{{defaultValue}}}{{#isEnum}}{{^isContainer}}){{/isContainer}}{{/isEnum}}{{/defaultValue}}, + {{^required}}{{#useOptional}}this.{{{name}}}{{#defaultValue}} = const Optional.present({{#isEnum}}{{^isContainer}}const {{{enumName}}}._({{/isContainer}}{{/isEnum}}{{{defaultValue}}}{{#isEnum}}{{^isContainer}}){{/isContainer}}{{/isEnum}}){{/defaultValue}}{{^defaultValue}} = const Optional.absent(){{/defaultValue}},{{/useOptional}}{{/required}}{{^required}}{{^useOptional}} this.{{{name}}}{{#defaultValue}} = {{#isEnum}}{{^isContainer}}const {{{enumName}}}._({{/isContainer}}{{/isEnum}}{{{defaultValue}}}{{#isEnum}}{{^isContainer}}){{/isContainer}}{{/isEnum}}{{/defaultValue}},{{/useOptional}}{{/required}}{{#required}}{{^defaultValue}}required {{/defaultValue}}{{/required}}{{#required}} this.{{{name}}}{{#defaultValue}} = {{#isEnum}}{{^isContainer}}const {{{enumName}}}._({{/isContainer}}{{/isEnum}}{{{defaultValue}}}{{#isEnum}}{{^isContainer}}){{/isContainer}}{{/isEnum}}{{/defaultValue}},{{/required}} {{/vars}} }); \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/dart2/apilib.mustache b/modules/openapi-generator/src/main/resources/dart2/apilib.mustache index 1b1898d88dda..62524c17f033 100644 --- a/modules/openapi-generator/src/main/resources/dart2/apilib.mustache +++ b/modules/openapi-generator/src/main/resources/dart2/apilib.mustache @@ -18,6 +18,9 @@ part 'auth/api_key_auth.dart'; part 'auth/oauth.dart'; part 'auth/http_basic_auth.dart'; part 'auth/http_bearer_auth.dart'; +{{#useOptional}} +part 'optional.dart'; +{{/useOptional}} {{#apiInfo}}{{#apis}}part 'api/{{{classFilename}}}.dart'; {{/apis}}{{/apiInfo}} diff --git a/modules/openapi-generator/src/main/resources/dart2/dart_constructor.mustache b/modules/openapi-generator/src/main/resources/dart2/dart_constructor.mustache index dc5dfec5f2a7..29f362d5e58e 100644 --- a/modules/openapi-generator/src/main/resources/dart2/dart_constructor.mustache +++ b/modules/openapi-generator/src/main/resources/dart2/dart_constructor.mustache @@ -5,6 +5,6 @@ A field is required in Dart when it is required && !defaultValue in OAS }} - {{#required}}{{^defaultValue}}required {{/defaultValue}}{{/required}}this.{{{name}}}{{#defaultValue}} = {{#isEnum}}{{^isContainer}}const {{{enumName}}}._({{/isContainer}}{{/isEnum}}{{{.}}}{{#isEnum}}{{^isContainer}}){{/isContainer}}{{/isEnum}}{{/defaultValue}}, + {{^required}}{{#vendorExtensions.x-is-optional}}this.{{{name}}}{{#defaultValue}} = const Optional.present({{#isEnum}}{{^isContainer}}const {{{enumName}}}._({{/isContainer}}{{/isEnum}}{{{.}}}{{#isEnum}}{{^isContainer}}){{/isContainer}}{{/isEnum}}){{/defaultValue}}{{^defaultValue}} = const Optional.absent(){{/defaultValue}},{{/vendorExtensions.x-is-optional}}{{/required}}{{^required}}{{^vendorExtensions.x-is-optional}}this.{{{name}}}{{#defaultValue}} = {{#isEnum}}{{^isContainer}}const {{{enumName}}}._({{/isContainer}}{{/isEnum}}{{{.}}}{{#isEnum}}{{^isContainer}}){{/isContainer}}{{/isEnum}}{{/defaultValue}},{{/vendorExtensions.x-is-optional}}{{/required}}{{#required}}{{^defaultValue}}required {{/defaultValue}}{{/required}}{{#required}}this.{{{name}}}{{#defaultValue}} = {{#isEnum}}{{^isContainer}}const {{{enumName}}}._({{/isContainer}}{{/isEnum}}{{{.}}}{{#isEnum}}{{^isContainer}}){{/isContainer}}{{/isEnum}}{{/defaultValue}},{{/required}} {{/vars}} }); diff --git a/modules/openapi-generator/src/main/resources/dart2/optional.mustache b/modules/openapi-generator/src/main/resources/dart2/optional.mustache new file mode 100644 index 000000000000..3cb3f0852daa --- /dev/null +++ b/modules/openapi-generator/src/main/resources/dart2/optional.mustache @@ -0,0 +1,110 @@ +{{>header}} +{{>part_of}} + +/// Represents an optional value that can be either absent or present. +/// +/// This is used to distinguish between three states in PATCH operations: +/// - Absent: Field is not set (omitted from JSON) +/// - Present with null: Field is explicitly set to null +/// - Present with value: Field has a value +/// +/// Example usage: +/// ```dart +/// // Field absent - not sent in request +/// final patch1 = Model(); +/// +/// // Field explicitly null - sends {"field": null} +/// final patch2 = Model(field: const Optional.present(null)); +/// +/// // Field has value - sends {"field": "value"} +/// final patch3 = Model(field: const Optional.present('value')); +/// ``` +abstract class Optional { + const Optional(); + + /// Creates an Optional with an absent value (not set). + const factory Optional.absent() = Absent; + + /// Creates an Optional with a present value (can be null). + const factory Optional.present(T value) = Present; + + /// Returns true if this Optional has a value (even if that value is null). + bool get isPresent; + + /// Returns true if this Optional does not have a value. + bool get isEmpty => !isPresent; + + /// Returns the value if present, throws if absent. + T get value; + + /// Returns the value if present, otherwise returns [defaultValue]. + T orElse(T defaultValue); + + /// Returns the value if present, otherwise returns the result of calling [defaultValue]. + T orElseGet(T Function() defaultValue); + + /// Maps the value if present using [transform], otherwise returns an absent Optional. + Optional map(R Function(T value) transform); +} + +/// Represents an absent Optional value. +class Absent extends Optional { + const Absent(); + + @override + bool get isPresent => false; + + @override + T get value => throw StateError('No value present'); + + @override + T orElse(T defaultValue) => defaultValue; + + @override + T orElseGet(T Function() defaultValue) => defaultValue(); + + @override + Optional map(R Function(T value) transform) => const Absent(); + + @override + bool operator ==(Object other) => other is Absent; + + @override + int get hashCode => 0; + + @override + String toString() => 'Optional.absent()'; +} + +/// Represents a present Optional value. +class Present extends Optional { + const Present(this._value); + + final T _value; + + @override + bool get isPresent => true; + + @override + T get value => _value; + + @override + T orElse(T defaultValue) => _value; + + @override + T orElseGet(T Function() defaultValue) => _value; + + @override + Optional map(R Function(T value) transform) => Optional.present(transform(_value)); + + @override + bool operator ==(Object other) => + identical(this, other) || + (other is Present && _value == other._value); + + @override + int get hashCode => _value.hashCode; + + @override + String toString() => 'Optional.present($_value)'; +} diff --git a/modules/openapi-generator/src/main/resources/dart2/serialization/native/native_class.mustache b/modules/openapi-generator/src/main/resources/dart2/serialization/native/native_class.mustache index 0c9bffad1e00..ddc6d6c06133 100644 --- a/modules/openapi-generator/src/main/resources/dart2/serialization/native/native_class.mustache +++ b/modules/openapi-generator/src/main/resources/dart2/serialization/native/native_class.mustache @@ -33,7 +33,17 @@ class {{{classname}}} { {{/required}} {{/isNullable}} {{/isEnum}} - {{{datatypeWithEnum}}}{{#isNullable}}?{{/isNullable}}{{^isNullable}}{{^required}}{{^defaultValue}}?{{/defaultValue}}{{/required}}{{/isNullable}} {{{name}}}; + {{#required}} + {{{datatypeWithEnum}}}{{#isNullable}}?{{/isNullable}} {{{name}}}; + {{/required}} + {{^required}} + {{#vendorExtensions.x-is-optional}} + {{{datatypeWithEnum}}} {{{name}}}; + {{/vendorExtensions.x-is-optional}} + {{^vendorExtensions.x-is-optional}} + {{{datatypeWithEnum}}}{{#isNullable}}?{{/isNullable}}{{^isNullable}}{{^defaultValue}}?{{/defaultValue}}{{/isNullable}} {{{name}}}; + {{/vendorExtensions.x-is-optional}} + {{/required}} {{/vars}} @override @@ -55,6 +65,37 @@ class {{{classname}}} { Map toJson() { final json = {}; {{#vars}} + {{#vendorExtensions.x-is-optional}} + if (this.{{{name}}}.isPresent) { + final value = this.{{{name}}}.value; + {{#isDateTime}} + {{#pattern}} + json[r'{{{baseName}}}'] = value == null ? null : (_isEpochMarker(r'{{{pattern}}}') + ? value.millisecondsSinceEpoch + : value.toUtc().toIso8601String()); + {{/pattern}} + {{^pattern}} + json[r'{{{baseName}}}'] = value == null ? null : value.toUtc().toIso8601String(); + {{/pattern}} + {{/isDateTime}} + {{#isDate}} + {{#pattern}} + json[r'{{{baseName}}}'] = value == null ? null : (_isEpochMarker(r'{{{pattern}}}') + ? value.millisecondsSinceEpoch + : _dateFormatter.format(value.toUtc())); + {{/pattern}} + {{^pattern}} + json[r'{{{baseName}}}'] = value == null ? null : _dateFormatter.format(value.toUtc()); + {{/pattern}} + {{/isDate}} + {{^isDateTime}} + {{^isDate}} + json[r'{{{baseName}}}'] = value{{#isArray}}{{#uniqueItems}} == null ? null : value.toList(growable: false){{/uniqueItems}}{{/isArray}}; + {{/isDate}} + {{/isDateTime}} + } + {{/vendorExtensions.x-is-optional}} + {{^vendorExtensions.x-is-optional}} {{#isNullable}} if (this.{{{name}}} != null) { {{/isNullable}} @@ -104,6 +145,7 @@ class {{{classname}}} { {{/defaultValue}} {{/required}} {{/isNullable}} + {{/vendorExtensions.x-is-optional}} {{/vars}} return json; } @@ -133,10 +175,20 @@ class {{{classname}}} { return {{{classname}}}( {{#vars}} {{#isDateTime}} + {{#vendorExtensions.x-is-optional}} + {{{name}}}: json.containsKey(r'{{{baseName}}}') ? Optional.present(mapDateTime(json, r'{{{baseName}}}', r'{{{pattern}}}')) : const Optional.absent(), + {{/vendorExtensions.x-is-optional}} + {{^vendorExtensions.x-is-optional}} {{{name}}}: mapDateTime(json, r'{{{baseName}}}', r'{{{pattern}}}'){{#required}}{{^isNullable}}!{{/isNullable}}{{/required}}{{^required}}{{#defaultValue}} ?? {{{.}}}{{/defaultValue}}{{/required}}, + {{/vendorExtensions.x-is-optional}} {{/isDateTime}} {{#isDate}} + {{#vendorExtensions.x-is-optional}} + {{{name}}}: json.containsKey(r'{{{baseName}}}') ? Optional.present(mapDateTime(json, r'{{{baseName}}}', r'{{{pattern}}}')) : const Optional.absent(), + {{/vendorExtensions.x-is-optional}} + {{^vendorExtensions.x-is-optional}} {{{name}}}: mapDateTime(json, r'{{{baseName}}}', r'{{{pattern}}}'){{#required}}{{^isNullable}}!{{/isNullable}}{{/required}}{{^required}}{{#defaultValue}} ?? {{{.}}}{{/defaultValue}}{{/required}}, + {{/vendorExtensions.x-is-optional}} {{/isDate}} {{^isDateTime}} {{^isDate}} @@ -194,7 +246,12 @@ class {{{classname}}} { {{{name}}}: null, // No support for decoding binary content from JSON {{/isBinary}} {{^isBinary}} + {{#vendorExtensions.x-is-optional}} + {{{name}}}: json.containsKey(r'{{{baseName}}}') ? Optional.present({{{complexType}}}.fromJson(json[r'{{{baseName}}}'])) : const Optional.absent(), + {{/vendorExtensions.x-is-optional}} + {{^vendorExtensions.x-is-optional}} {{{name}}}: {{{complexType}}}.fromJson(json[r'{{{baseName}}}']){{#required}}{{^isNullable}}!{{/isNullable}}{{/required}}{{^required}}{{#defaultValue}} ?? {{{.}}}{{/defaultValue}}{{/required}}, + {{/vendorExtensions.x-is-optional}} {{/isBinary}} {{/isMap}} {{/isArray}} @@ -221,12 +278,32 @@ class {{{classname}}} { : {{/isNullable}}{{{datatypeWithEnum}}}.parse('${json[r'{{{baseName}}}']}'), {{/isNumber}} {{^isNumber}} - {{^isEnum}} + {{#vendorExtensions.x-original-is-integer}} + {{{name}}}: json.containsKey(r'{{{baseName}}}') ? Optional.present(json[r'{{{baseName}}}'] == null ? null : int.parse('${json[r'{{{baseName}}}']}')) : const Optional.absent(), + {{/vendorExtensions.x-original-is-integer}} + {{^vendorExtensions.x-original-is-integer}} + {{#vendorExtensions.x-original-is-number}} + {{{name}}}: json.containsKey(r'{{{baseName}}}') ? Optional.present(json[r'{{{baseName}}}'] == null ? null : num.parse('${json[r'{{{baseName}}}']}')) : const Optional.absent(), + {{/vendorExtensions.x-original-is-number}} + {{^vendorExtensions.x-original-is-number}} + {{^isEnum}} + {{#vendorExtensions.x-is-optional}} + {{{name}}}: json.containsKey(r'{{{baseName}}}') ? Optional.present(mapValueOfType<{{#vendorExtensions.x-unwrapped-datatype}}{{{vendorExtensions.x-unwrapped-datatype}}}{{/vendorExtensions.x-unwrapped-datatype}}{{^vendorExtensions.x-unwrapped-datatype}}{{{datatypeWithEnum}}}{{/vendorExtensions.x-unwrapped-datatype}}>(json, r'{{{baseName}}}')) : const Optional.absent(), + {{/vendorExtensions.x-is-optional}} + {{^vendorExtensions.x-is-optional}} {{{name}}}: mapValueOfType<{{{datatypeWithEnum}}}>(json, r'{{{baseName}}}'){{#required}}{{^isNullable}}!{{/isNullable}}{{/required}}{{^required}}{{#defaultValue}} ?? {{{.}}}{{/defaultValue}}{{/required}}, - {{/isEnum}} - {{#isEnum}} + {{/vendorExtensions.x-is-optional}} + {{/isEnum}} + {{#isEnum}} + {{#vendorExtensions.x-is-optional}} + {{{name}}}: json.containsKey(r'{{{baseName}}}') ? Optional.present({{{enumName}}}.fromJson(json[r'{{{baseName}}}'])) : const Optional.absent(), + {{/vendorExtensions.x-is-optional}} + {{^vendorExtensions.x-is-optional}} {{{name}}}: {{{enumName}}}.fromJson(json[r'{{{baseName}}}']){{#required}}{{^isNullable}}!{{/isNullable}}{{/required}}{{^required}}{{#defaultValue}} ?? const {{{enumName}}}._({{{.}}}){{/defaultValue}}{{/required}}, - {{/isEnum}} + {{/vendorExtensions.x-is-optional}} + {{/isEnum}} + {{/vendorExtensions.x-original-is-number}} + {{/vendorExtensions.x-original-is-integer}} {{/isNumber}} {{/isMap}} {{/isArray}} diff --git a/modules/openapi-generator/src/main/resources/dart2/serialization/native/native_enum.mustache b/modules/openapi-generator/src/main/resources/dart2/serialization/native/native_enum.mustache index 84217b702b9e..044c40d64ff6 100644 --- a/modules/openapi-generator/src/main/resources/dart2/serialization/native/native_enum.mustache +++ b/modules/openapi-generator/src/main/resources/dart2/serialization/native/native_enum.mustache @@ -4,12 +4,12 @@ class {{{classname}}} { const {{{classname}}}._(this.value); /// The underlying value of this enum member. - final {{{dataType}}} value; + final {{#vendorExtensions.x-unwrapped-datatype}}{{{vendorExtensions.x-unwrapped-datatype}}}{{/vendorExtensions.x-unwrapped-datatype}}{{^vendorExtensions.x-unwrapped-datatype}}{{{dataType}}}{{/vendorExtensions.x-unwrapped-datatype}} value; @override String toString() => {{#isString}}value{{/isString}}{{^isString}}value.toString(){{/isString}}; - {{{dataType}}} toJson() => value; + {{#vendorExtensions.x-unwrapped-datatype}}{{{vendorExtensions.x-unwrapped-datatype}}}{{/vendorExtensions.x-unwrapped-datatype}}{{^vendorExtensions.x-unwrapped-datatype}}{{{dataType}}}{{/vendorExtensions.x-unwrapped-datatype}} toJson() => value; {{#allowableValues}} {{#enumVars}} @@ -49,7 +49,7 @@ class {{{classname}}}TypeTransformer { const {{{classname}}}TypeTransformer._(); - {{{dataType}}} encode({{{classname}}} data) => data.value; + {{#vendorExtensions.x-unwrapped-datatype}}{{{vendorExtensions.x-unwrapped-datatype}}}{{/vendorExtensions.x-unwrapped-datatype}}{{^vendorExtensions.x-unwrapped-datatype}}{{{dataType}}}{{/vendorExtensions.x-unwrapped-datatype}} encode({{{classname}}} data) => data.value; /// Decodes a [dynamic value][data] to a {{{classname}}}. /// diff --git a/modules/openapi-generator/src/main/resources/dart2/serialization/native/native_enum_inline.mustache b/modules/openapi-generator/src/main/resources/dart2/serialization/native/native_enum_inline.mustache index 1fe4428dcb31..0cf2463b1454 100644 --- a/modules/openapi-generator/src/main/resources/dart2/serialization/native/native_enum_inline.mustache +++ b/modules/openapi-generator/src/main/resources/dart2/serialization/native/native_enum_inline.mustache @@ -4,12 +4,12 @@ class {{{enumName}}} { const {{{enumName}}}._(this.value); /// The underlying value of this enum member. - final {{{dataType}}} value; + final {{#vendorExtensions.x-unwrapped-datatype}}{{{vendorExtensions.x-unwrapped-datatype}}}{{/vendorExtensions.x-unwrapped-datatype}}{{^vendorExtensions.x-unwrapped-datatype}}{{{dataType}}}{{/vendorExtensions.x-unwrapped-datatype}} value; @override String toString() => {{#isString}}value{{/isString}}{{^isString}}value.toString(){{/isString}}; - {{{dataType}}} toJson() => value; + {{#vendorExtensions.x-unwrapped-datatype}}{{{vendorExtensions.x-unwrapped-datatype}}}{{/vendorExtensions.x-unwrapped-datatype}}{{^vendorExtensions.x-unwrapped-datatype}}{{{dataType}}}{{/vendorExtensions.x-unwrapped-datatype}} toJson() => value; {{#allowableValues}} {{#enumVars}} @@ -49,7 +49,7 @@ class {{{enumName}}}TypeTransformer { const {{{enumName}}}TypeTransformer._(); - {{{dataType}}} encode({{{enumName}}} data) => data.value; + {{#vendorExtensions.x-unwrapped-datatype}}{{{vendorExtensions.x-unwrapped-datatype}}}{{/vendorExtensions.x-unwrapped-datatype}}{{^vendorExtensions.x-unwrapped-datatype}}{{{dataType}}}{{/vendorExtensions.x-unwrapped-datatype}} encode({{{enumName}}} data) => data.value; /// Decodes a [dynamic value][data] to a {{{enumName}}}. /// diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/dart/DartClientOptionsTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/dart/DartClientOptionsTest.java index 87dabbbfd00d..23c49f4221fc 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/dart/DartClientOptionsTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/dart/DartClientOptionsTest.java @@ -53,5 +53,7 @@ protected void verifyOptions() { verify(clientCodegen).setSourceFolder(DartClientOptionsProvider.SOURCE_FOLDER_VALUE); verify(clientCodegen).setUseEnumExtension(Boolean.parseBoolean(DartClientOptionsProvider.USE_ENUM_EXTENSION)); verify(clientCodegen).setEnumUnknownDefaultCase(Boolean.parseBoolean(DartClientOptionsProvider.ENUM_UNKNOWN_DEFAULT_CASE_VALUE)); + verify(clientCodegen).setUseOptional(Boolean.parseBoolean(DartClientOptionsProvider.USE_OPTIONAL_VALUE)); + verify(clientCodegen).setPatchOnly(Boolean.parseBoolean(DartClientOptionsProvider.PATCH_ONLY_VALUE)); } } diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/dart/DartModelTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/dart/DartModelTest.java index ce6796966067..b8cfdcd6dde5 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/dart/DartModelTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/dart/DartModelTest.java @@ -19,9 +19,14 @@ 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.*; +import io.swagger.v3.oas.models.parameters.RequestBody; import org.openapitools.codegen.*; +import org.openapitools.codegen.languages.AbstractDartCodegen; import org.openapitools.codegen.languages.DartClientCodegen; +import org.openapitools.codegen.model.OperationMap; +import org.openapitools.codegen.model.OperationsMap; import org.testng.Assert; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; @@ -564,4 +569,151 @@ public void dateTest() { Assert.assertEquals(op.returnType, "DateTime"); Assert.assertEquals(op.bodyParam.dataType, "DateTime"); } + + @Test(description = "useOptional flag wrapping non-required properties") + public void testUseOptionalFlagWrappingNonRequiredProperties() { + final Schema model = new Schema() + .description("a model with required and optional properties") + .addProperties("id", new IntegerSchema()) + .addProperties("name", new StringSchema()) + .addProperties("description", new StringSchema()) + .addRequiredItem("id"); + + final DartClientCodegen codegen = new DartClientCodegen(); + codegen.setUseOptional(true); + OpenAPI openAPI = TestUtils.createOpenAPIWithOneSchema("sample", model); + codegen.setOpenAPI(openAPI); + codegen.processOpts(); + + final CodegenModel cm = codegen.fromModel("sample", model); + codegen.postProcessModels(createCodegenModelWrapper(cm)); + + final CodegenProperty idProp = cm.vars.get(0); + Assert.assertEquals(idProp.baseName, "id"); + Assert.assertFalse(idProp.dataType.startsWith("Optional<"), "Required property should not be wrapped"); + + final CodegenProperty nameProp = cm.vars.get(1); + Assert.assertEquals(nameProp.baseName, "name"); + Assert.assertTrue(nameProp.dataType.startsWith("Optional<"), "Non-required property should be wrapped"); + Assert.assertTrue((Boolean) nameProp.vendorExtensions.get("x-is-optional")); + + final CodegenProperty descProp = cm.vars.get(2); + Assert.assertEquals(descProp.baseName, "description"); + Assert.assertTrue(descProp.dataType.startsWith("Optional<"), "Non-required property should be wrapped"); + } + + @Test(description = "patchOnly mode PATCH schema detection") + public void testPatchOnlyModePatchSchemaDetection() { + final Schema patchBodySchema = new Schema() + .description("PATCH request body schema") + .addProperties("id", new IntegerSchema()) + .addProperties("name", new StringSchema()) + .addRequiredItem("id"); + + final Schema postBodySchema = new Schema() + .description("POST request body schema") + .addProperties("id", new IntegerSchema()) + .addProperties("title", new StringSchema()) + .addRequiredItem("id"); + + OpenAPI openAPI = TestUtils.createOpenAPI(); + openAPI.getComponents().addSchemas("PatchBody", patchBodySchema); + openAPI.getComponents().addSchemas("PostBody", postBodySchema); + + Operation patchOp = new Operation(); + RequestBody patchRequestBody = new RequestBody(); + MediaType patchMediaType = new MediaType(); + patchMediaType.setSchema(new Schema().$ref("#/components/schemas/PatchBody")); + patchRequestBody.setContent(new Content().addMediaType("application/json", patchMediaType)); + patchOp.setRequestBody(patchRequestBody); + patchOp.setOperationId("updateResource"); + + Operation postOp = new Operation(); + RequestBody postRequestBody = new RequestBody(); + MediaType postMediaType = new MediaType(); + postMediaType.setSchema(new Schema().$ref("#/components/schemas/PostBody")); + postRequestBody.setContent(new Content().addMediaType("application/json", postMediaType)); + postOp.setRequestBody(postRequestBody); + postOp.setOperationId("createResource"); + + PathItem pathItem = new PathItem(); + pathItem.setPatch(patchOp); + pathItem.setPost(postOp); + openAPI.getPaths().addPathItem("/resource", pathItem); + + final DartClientCodegen codegen = new DartClientCodegen(); + codegen.setPatchOnly(true); + codegen.setOpenAPI(openAPI); + codegen.processOpts(); + codegen.preprocessOpenAPI(openAPI); + + final CodegenModel patchModel = codegen.fromModel("PatchBody", patchBodySchema); + codegen.postProcessModels(createCodegenModelWrapper(patchModel)); + + final CodegenModel postModel = codegen.fromModel("PostBody", postBodySchema); + codegen.postProcessModels(createCodegenModelWrapper(postModel)); + + final CodegenProperty patchNameProp = patchModel.vars.get(1); + Assert.assertEquals(patchNameProp.baseName, "name"); + Assert.assertTrue(patchNameProp.dataType.startsWith("Optional<"), + "PATCH body non-required property should be wrapped with Optional"); + + final CodegenProperty postTitleProp = postModel.vars.get(1); + Assert.assertEquals(postTitleProp.baseName, "title"); + Assert.assertFalse(postTitleProp.dataType.startsWith("Optional<"), + "POST body non-required property should NOT be wrapped when patchOnly=true"); + } + + @Test(description = "patchOnly=true auto-enabling useOptional") + public void testPatchOnlyTrueAutoEnablingUseOptional() { + final DartClientCodegen codegen = new DartClientCodegen(); + codegen.additionalProperties().put(AbstractDartCodegen.PATCH_ONLY, true); + + codegen.processOpts(); + + Assert.assertTrue((Boolean) codegen.additionalProperties().get(AbstractDartCodegen.USE_OPTIONAL), + "patchOnly=true should auto-enable useOptional"); + } + + @Test(description = "Parameter unwrapping behavior") + public void testParameterUnwrappingBehavior() { + final Schema model = new Schema() + .description("a model") + .addProperties("id", new IntegerSchema()) + .addProperties("name", new StringSchema()); + + OpenAPI openAPI = TestUtils.createOpenAPIWithOneSchema("sample", model); + + Operation getOp = new Operation(); + io.swagger.v3.oas.models.parameters.QueryParameter queryParam = + new io.swagger.v3.oas.models.parameters.QueryParameter(); + queryParam.setName("filter"); + queryParam.setSchema(new StringSchema()); + queryParam.setRequired(false); + getOp.addParametersItem(queryParam); + getOp.setOperationId("getResource"); + + PathItem pathItem = new PathItem(); + pathItem.setGet(getOp); + openAPI.getPaths().addPathItem("/resource", pathItem); + + final DartClientCodegen codegen = new DartClientCodegen(); + codegen.setUseOptional(true); + codegen.setOpenAPI(openAPI); + codegen.processOpts(); + + final CodegenOperation op = codegen.fromOperation("/resource", "get", getOp, null); + + OperationsMap opsMap = new OperationsMap(); + OperationMap opMap = new OperationMap(); + opMap.setOperation(Collections.singletonList(op)); + opsMap.setOperation(opMap); + + codegen.postProcessOperationsWithModels(opsMap, Collections.emptyList()); + + Assert.assertFalse(op.queryParams.isEmpty(), "Should have query params"); + CodegenParameter filterParam = op.queryParams.get(0); + Assert.assertFalse(filterParam.dataType.startsWith("Optional<"), + "Query parameter should not be wrapped with Optional"); + } } diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/dart/dio/DartDioClientOptionsTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/dart/dio/DartDioClientOptionsTest.java index cb3350d57e2e..d19d388a9402 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/dart/dio/DartDioClientOptionsTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/dart/dio/DartDioClientOptionsTest.java @@ -55,5 +55,7 @@ protected void verifyOptions() { verify(clientCodegen).setLibrary(DartDioClientCodegen.SERIALIZATION_LIBRARY_DEFAULT); verify(clientCodegen).setEqualityCheckMethod(DartDioClientCodegen.EQUALITY_CHECK_METHOD_DEFAULT); verify(clientCodegen).setEnumUnknownDefaultCase(Boolean.parseBoolean(DartDioClientOptionsProvider.ENUM_UNKNOWN_DEFAULT_CASE_VALUE)); + verify(clientCodegen).setUseOptional(Boolean.parseBoolean(DartDioClientOptionsProvider.USE_OPTIONAL_VALUE)); + verify(clientCodegen).setPatchOnly(Boolean.parseBoolean(DartDioClientOptionsProvider.PATCH_ONLY_VALUE)); } } diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/options/DartClientOptionsProvider.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/options/DartClientOptionsProvider.java index 50bbfdb5e9b5..e8ae5313da8b 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/options/DartClientOptionsProvider.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/options/DartClientOptionsProvider.java @@ -41,6 +41,8 @@ public class DartClientOptionsProvider implements OptionsProvider { public static final String ALLOW_UNICODE_IDENTIFIERS_VALUE = "false"; public static final String PREPEND_FORM_OR_BODY_PARAMETERS_VALUE = "true"; public static final String ENUM_UNKNOWN_DEFAULT_CASE_VALUE = "false"; + public static final String USE_OPTIONAL_VALUE = "true"; + public static final String PATCH_ONLY_VALUE = "true"; @Override public String getLanguage() { @@ -70,6 +72,8 @@ public Map createOptions() { .put(CodegenConstants.DISALLOW_ADDITIONAL_PROPERTIES_IF_NOT_PRESENT, "true") .put("serializationLibrary", "custom") .put(CodegenConstants.ENUM_UNKNOWN_DEFAULT_CASE, ENUM_UNKNOWN_DEFAULT_CASE_VALUE) + .put(DartClientCodegen.USE_OPTIONAL, USE_OPTIONAL_VALUE) + .put(DartClientCodegen.PATCH_ONLY, PATCH_ONLY_VALUE) .build(); } diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/options/DartDioClientOptionsProvider.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/options/DartDioClientOptionsProvider.java index 648332073401..a8e0f8381df0 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/options/DartDioClientOptionsProvider.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/options/DartDioClientOptionsProvider.java @@ -40,6 +40,8 @@ public class DartDioClientOptionsProvider implements OptionsProvider { public static final String PUB_REPOSITORY_VALUE = "Repository"; public static final String PUB_PUBLISH_TO_VALUE = "Publish to"; public static final String ENUM_UNKNOWN_DEFAULT_CASE_VALUE = "false"; + public static final String USE_OPTIONAL_VALUE = "true"; + public static final String PATCH_ONLY_VALUE = "true"; @Override public String getLanguage() { @@ -73,6 +75,8 @@ public Map createOptions() { .put(CodegenConstants.DISALLOW_ADDITIONAL_PROPERTIES_IF_NOT_PRESENT, "true") .put(CodegenConstants.ENUM_UNKNOWN_DEFAULT_CASE, ENUM_UNKNOWN_DEFAULT_CASE_VALUE) .put(DartDioClientCodegen.SKIP_COPY_WITH, DartDioClientCodegen.SKIP_COPY_WITH_DEFAULT_VALUE) + .put(DartDioClientCodegen.USE_OPTIONAL, USE_OPTIONAL_VALUE) + .put(DartDioClientCodegen.PATCH_ONLY, PATCH_ONLY_VALUE) .build(); }