Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/generators/dart-dio.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.|<dl><dt>**default**</dt><dd>[DEFAULT] Built in hash code generation method</dd><dt>**equatable**</dt><dd>Uses equatable library for equality checking</dd></dl>|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).|<dl><dt>**true**</dt><dd>The mapping in the discriminator includes descendent schemas that allOf inherit from self and the discriminator mapping schemas in the OAS document.</dd><dt>**false**</dt><dd>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.</dd></dl>|true|
|patchOnly|Only apply Optional&lt;T&gt; 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|
Expand All @@ -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&lt;T&gt; to distinguish absent, null, and present for optional fields (Dart 3+)| |false|

## IMPORT MAPPING

Expand Down
2 changes: 2 additions & 0 deletions docs/generators/dart.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.|<dl><dt>**false**</dt><dd>No changes to the enum's are made, this is the default option.</dd><dt>**true**</dt><dd>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.</dd></dl>|false|
|legacyDiscriminatorBehavior|Set to false for generators with better support for discriminators. (Python, Java, Go, PowerShell, C# have this enabled by default).|<dl><dt>**true**</dt><dd>The mapping in the discriminator includes descendent schemas that allOf inherit from self and the discriminator mapping schemas in the OAS document.</dd><dt>**false**</dt><dd>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.</dd></dl>|true|
|patchOnly|Only apply Optional&lt;T&gt; 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|
Expand All @@ -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&lt;T&gt; to distinguish absent, null, and present for optional fields (Dart 3+)| |false|

## IMPORT MAPPING

Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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";
Expand All @@ -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<String> patchRequestSchemas = new HashSet<>();
protected String apiDocPath = "doc/";
protected String modelDocPath = "doc/";
protected String apiTestPath = "test" + File.separator;
Expand Down Expand Up @@ -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<T> to distinguish absent, null, and present for optional fields (Dart 3+)", String.valueOf(useOptional));
addOption(PATCH_ONLY, "Only apply Optional<T> to PATCH operation request bodies (requires useOptional=true)", String.valueOf(patchOnly));
addOption(CodegenConstants.SOURCE_FOLDER, CodegenConstants.SOURCE_FOLDER_DESC, sourceFolder);
}

Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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<T>)", modelName);
}
}
});
}
}
}
});
}
}

@Override
public String getSchemaType(Schema p) {
String openAPIType = super.getSchemaType(p);
Expand All @@ -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
Expand Down Expand Up @@ -624,6 +724,19 @@ public CodegenProperty fromProperty(String name, Schema p, boolean required) {
return property;
}

@Override
public CodegenParameter fromParameter(Parameter parameter, Set<String> 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<Server> servers) {
final CodegenOperation op = super.fromOperation(path, httpMethod, operation, servers);
Expand Down Expand Up @@ -660,6 +773,13 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List<Mo
if (operations != null) {
List<CodegenOperation> 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
Expand All @@ -681,6 +801,16 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List<Mo
return objs;
}

private void unwrapOptionalFromParameters(List<CodegenParameter> 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<Map<String, String>> prioritizeContentTypes(List<Map<String, String>> consumes) {
if (consumes.size() <= 1) {
// no need to change any order
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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<T> {
const Optional();

/// Creates an Optional with an absent value (not set).
const factory Optional.absent() = Absent<T>;

/// Creates an Optional with a present value (can be null).
const factory Optional.present(T value) = Present<T>;

/// 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<R> map<R>(R Function(T value) transform);
}

/// Represents an absent Optional value.
final class Absent<T> extends Optional<T> {
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<R> map<R>(R Function(T value) transform) => const Absent();

@override
bool operator ==(Object other) => other is Absent<T>;

@override
int get hashCode => 0;

@override
String toString() => 'Optional.absent()';
}

/// Represents a present Optional value.
final class Present<T> extends Optional<T> {
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<R> map<R>(R Function(T value) transform) => Optional.present(transform(_value));

@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is Present<T> && _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<T> implements JsonConverter<Optional<T>, Object?> {
const OptionalConverter();

@override
Optional<T> fromJson(Object? json) {
return json is _OptionalAbsentSentinel
? const Optional.absent()
: Optional.present(json as T);
}

@override
Object? toJson(Optional<T> object) {
return object.isPresent ? object.value : null;
}
}
Loading
Loading