diff --git a/docs/generators/dart-dio.md b/docs/generators/dart-dio.md index 7e62f92f0782..ad2785b266d2 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 79148a7e4791..746afd0a64d0 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 f002ee8d8d6a..1eb4d221cf4a 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; @@ -195,6 +204,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); } @@ -301,6 +312,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)); @@ -544,6 +573,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); @@ -558,7 +616,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 @@ -623,6 +723,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); @@ -659,6 +772,21 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List ops = operations.getOperation(); for (CodegenOperation op : ops) { + if (patchOnly && "PATCH".equalsIgnoreCase(op.httpMethod)) { + if (op.bodyParam != null && op.bodyParam.dataType != null) { + String modelName = getString(op); + patchRequestSchemas.add(modelName); + LOGGER.debug("Marked schema '{}' for Optional wrapping (PATCH request body)", modelName); + } + } + + 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 @@ -680,6 +808,29 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List'); + if (start > 0 && end > start) { + modelName = modelName.substring(start, end); + } + } + modelName = modelName.replace("?", ""); + return modelName; + } + + private void unwrapOptionalFromParameters(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..b734d8440500 --- /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)'; +} + +/// JSON converter for Optional values. +/// +/// This converter handles serialization and deserialization of Optional values: +/// - Optional.absent() -> field omitted from JSON +/// - Optional.present(null) -> {"field": null} +/// - Optional.present(value) -> {"field": value} +/// +/// Usage with json_serializable: +/// ```dart +/// @JsonKey() +/// @OptionalConverter() +/// final Optional field; +/// ``` +class OptionalConverter implements JsonConverter, T?> { + const OptionalConverter(); + + @override + Optional fromJson(T? json) { + return json == null ? const Optional.absent() : Optional.present(json); + } + + @override + T? 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..e608aeb1e29f 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,6 +52,11 @@ class {{{classname}}} { @Deprecated('{{{name}}} has been deprecated') {{/deprecated}} {{^isBinary}} +{{^required}} +{{#useOptional}} + @OptionalConverter() +{{/useOptional}} +{{/required}} @JsonKey( {{#defaultValue}}defaultValue: {{{defaultValue}}},{{/defaultValue}} name: r'{{{baseName}}}', @@ -70,7 +78,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 +154,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 b4ba0b716a12..19bfa96997c8 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; } @@ -129,10 +171,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}} @@ -190,7 +242,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}} @@ -217,12 +274,32 @@ class {{{classname}}} { : {{/isNullable}}{{{datatypeWithEnum}}}.parse('${json[r'{{{baseName}}}']}'), {{/isNumber}} {{^isNumber}} - {{^isEnum}} + {{#vendorExtensions.x-original-is-integer}} + {{{name}}}: json[r'{{{baseName}}}'] == null ? const Optional.absent() : Optional.present(int.parse('${json[r'{{{baseName}}}']}')), + {{/vendorExtensions.x-original-is-integer}} + {{^vendorExtensions.x-original-is-integer}} + {{#vendorExtensions.x-original-is-number}} + {{{name}}}: json[r'{{{baseName}}}'] == null ? const Optional.absent() : Optional.present(num.parse('${json[r'{{{baseName}}}']}')), + {{/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}} ?? {{{.}}}{{/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/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(); }