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 08bc2c910a43..ac4ebe43aca4 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 @@ -41,6 +41,7 @@ import org.openapitools.codegen.templating.GeneratorTemplateContentLocator; import org.openapitools.codegen.templating.MustacheEngineAdapter; import org.openapitools.codegen.templating.TemplateManagerOptions; +import org.openapitools.codegen.utils.DiscriminatorUtils; import org.openapitools.codegen.utils.ModelUtils; import org.openapitools.codegen.utils.ProcessUtils; import org.slf4j.Logger; @@ -79,6 +80,9 @@ public class DartDioClientCodegen extends AbstractDartCodegen { public static final String SKIP_COPY_WITH_DEFAULT_VALUE = "false"; private static final String CLIENT_NAME = "clientName"; + private static final String X_DISCRIMINATOR_MAPPED_MODELS_NONSELF = "x-discriminator-mapped-models-nonself"; + private static final String X_HAS_DISCRIMINATOR_SELF_MAPPING = "x-has-discriminator-self-mapping"; + private static final String X_DISCRIMINATOR_SELF_MAPPING_NAME = "x-discriminator-self-mapping-name"; @Getter @Setter private String dateLibrary; @@ -307,6 +311,7 @@ private void configureSerializationLibraryBuiltValue(String srcFolder) { private void configureSerializationLibraryJsonSerializable(String srcFolder) { supportingFiles.add(new SupportingFile("serialization/json_serializable/build.yaml.mustache", "" /* main project dir */, "build.yaml")); + supportingFiles.add(new SupportingFile("serialization/json_serializable/api_util.mustache", srcFolder, "api_util.dart")); supportingFiles.add(new SupportingFile("serialization/json_serializable/deserialize.mustache", srcFolder, "deserialize.dart")); @@ -639,36 +644,278 @@ private void adaptToDartInheritance(Map objs) { } } - /// override the default behavior of createDiscriminator - /// to remove extra mappings added as a side effect of setLegacyDiscriminatorBehavior(false) - /// this ensures 1-1 schema mapping instead of 1-many + /** + * Computes the maximum allOf inheritance distance from {@code schemaName} to + * {@code ancestorSchemaName}. + * + *

Returns {@code 0} when both schema names are equal, and {@code -1} when no + * inheritance path exists. The {@code visited} set prevents infinite recursion on + * cyclic graphs. + */ + private int getSchemaInheritanceDepth(String schemaName, String ancestorSchemaName, Set visited) { + if (schemaName == null || ancestorSchemaName == null) { + return -1; + } + if (schemaName.equals(ancestorSchemaName)) { + return 0; + } + + Schema currentSchema = ModelUtils.getSchema(openAPI, schemaName); + if (currentSchema == null || currentSchema.getAllOf() == null || currentSchema.getAllOf().isEmpty()) { + return -1; + } + + int maxDepth = -1; + for (Object parentObj : currentSchema.getAllOf()) { + if (!(parentObj instanceof Schema)) { + continue; + } + Schema parentSchema = (Schema) parentObj; + String parentRef = parentSchema.get$ref(); + if (parentRef == null) { + continue; + } + + String parentSchemaName = ModelUtils.getSimpleRef(parentRef); + if (ancestorSchemaName.equals(parentSchemaName)) { + maxDepth = Math.max(maxDepth, 1); + continue; + } + + if (parentSchemaName != null && visited.add(parentSchemaName)) { + int parentDepth = getSchemaInheritanceDepth(parentSchemaName, ancestorSchemaName, visited); + if (parentDepth >= 0) { + maxDepth = Math.max(maxDepth, parentDepth + 1); + } + visited.remove(parentSchemaName); + } + } + + return maxDepth; + } + + /** + * Builds discriminator metadata and removes implicit/over-broad mappings so Dart + * generation keeps a strict one-schema-per-discriminator-entry behavior. + * + *

For schema-local discriminators, only explicitly declared mappings are kept. + * For inherited discriminators, mappings are restricted to true allOf descendants + * of the current schema. + */ @Override protected CodegenDiscriminator createDiscriminator(String schemaName, Schema schema) { CodegenDiscriminator sub = super.createDiscriminator(schemaName, schema); - Discriminator originalDiscriminator = schema.getDiscriminator(); + if (sub == null) { + return null; + } + + if (sub.getMapping() != null) { + // Defensive copy: avoid mutating shared mapping objects from the parsed spec. + sub.setMapping(new LinkedHashMap<>(sub.getMapping())); + } + sub.setVendorExtensions(new LinkedHashMap<>(ObjectUtils.firstNonNull(sub.getVendorExtensions(), Collections.emptyMap()))); + + Discriminator originalDiscriminator = getSchemaLocalDiscriminator(schema); if (originalDiscriminator != null) { Map originalMapping = originalDiscriminator.getMapping(); if (originalMapping != null && !originalMapping.isEmpty()) { - //we already have a discriminator mapping, remove everything else - for (MappedModel currentMappings : new HashSet<>(sub.getMappedModels())) { - if (originalMapping.containsKey(currentMappings.getMappingName())) { - //all good - } else { - sub.getMapping().remove(currentMappings.getMappingName()); - sub.getMappedModels().remove(currentMappings); - } - } + // keep only explicitly declared mappings on the schema-local discriminator + filterMappedModels(sub, mappedModel -> originalMapping.containsKey(mappedModel.getMappingName())); } + orderMappedModelsBySchemaSpecificity(sub, schemaName); + prepareDiscriminatorTemplateData(sub, schemaName, toModelName(schemaName)); + return sub; + } + + // For inherited discriminators, keep real allOf descendants of this schema + // (e.g. Reptile keeps Crocodile/Turtle, but not Bird from Animal's mapping). + // Also preserve alternatives declared directly in this schema's oneOf/anyOf. + Set descendantSchemaNames = DiscriminatorUtils.getAllOfDescendants(openAPI, schemaName).stream() + .map(MappedModel::getSchemaName) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + Set declaredAlternatives = getComposedAlternativeSchemaNames(schema); + + if (ModelUtils.isComposedSchema(schema) && schema.getAllOf() != null) { + filterMappedModels(sub, mappedModel -> descendantSchemaNames.contains(mappedModel.getSchemaName()) + || declaredAlternatives.contains(mappedModel.getSchemaName()) + || schemaName.equals(mappedModel.getSchemaName())); } + + orderMappedModelsBySchemaSpecificity(sub, schemaName); + prepareDiscriminatorTemplateData(sub, schemaName, toModelName(schemaName)); return sub; } + /** + * Prepares discriminator vendor extensions consumed by Dart templates. + * + *

The method separates non-self mapped models and records whether the + * discriminator includes a self-mapping plus its mapping key. + */ + private void prepareDiscriminatorTemplateData(CodegenDiscriminator discriminator, String schemaName, String modelName) { + if (discriminator == null || discriminator.getMappedModels() == null) { + return; + } + + String selfMappingName = null; + List nonSelfMappedModels = new ArrayList<>(); + for (MappedModel mappedModel : discriminator.getMappedModels()) { + boolean isSelfMapping = Objects.equals(schemaName, mappedModel.getSchemaName()) + || Objects.equals(modelName, mappedModel.getModelName()); + if (isSelfMapping) { + selfMappingName = mappedModel.getMappingName(); + } else { + nonSelfMappedModels.add(mappedModel); + } + } + + Map discriminatorVendorExtensions = discriminator.getVendorExtensions(); + if (discriminatorVendorExtensions == null) { + discriminatorVendorExtensions = new LinkedHashMap<>(); + discriminator.setVendorExtensions(discriminatorVendorExtensions); + } + + discriminatorVendorExtensions.put(X_DISCRIMINATOR_MAPPED_MODELS_NONSELF, nonSelfMappedModels); + discriminatorVendorExtensions.put(X_HAS_DISCRIMINATOR_SELF_MAPPING, selfMappingName != null); + if (selfMappingName != null) { + discriminatorVendorExtensions.put(X_DISCRIMINATOR_SELF_MAPPING_NAME, selfMappingName); + } else { + discriminatorVendorExtensions.remove(X_DISCRIMINATOR_SELF_MAPPING_NAME); + } + } + + /** + * Orders discriminator mapped models by schema specificity relative to the owner + * schema (deepest descendants first). + * + *

When two mappings have the same depth, the original insertion order is + * preserved for deterministic output. + */ + private void orderMappedModelsBySchemaSpecificity(CodegenDiscriminator discriminator, String ownerSchemaName) { + if (discriminator.getMappedModels() == null || discriminator.getMappedModels().size() < 2) { + return; + } + + List ordered = new ArrayList<>(discriminator.getMappedModels()); + Map inheritanceDepthBySchema = new HashMap<>(); + Map originalOrder = new HashMap<>(); + for (int i = 0; i < ordered.size(); i++) { + MappedModel mappedModel = ordered.get(i); + originalOrder.put(mappedModel, i); + inheritanceDepthBySchema.computeIfAbsent( + mappedModel.getSchemaName(), + schemaName -> getSchemaInheritanceDepth(schemaName, ownerSchemaName, new HashSet<>()) + ); + } + + ordered.sort((left, right) -> { + int leftDepth = inheritanceDepthBySchema.getOrDefault(left.getSchemaName(), -1); + int rightDepth = inheritanceDepthBySchema.getOrDefault(right.getSchemaName(), -1); + if (leftDepth != rightDepth) { + return Integer.compare(rightDepth, leftDepth); + } + + return Integer.compare(originalOrder.get(left), originalOrder.get(right)); + }); + + discriminator.setMappedModels(new LinkedHashSet<>(ordered)); + } + + /** + * Removes discriminator mapped models that do not satisfy the provided predicate. + * + *

Both the mapped model set and the optional mapping-name lookup map are kept + * in sync. + */ + private void filterMappedModels(CodegenDiscriminator discriminator, java.util.function.Predicate keepPredicate) { + for (MappedModel mappedModel : new HashSet<>(discriminator.getMappedModels())) { + if (!keepPredicate.test(mappedModel)) { + if (discriminator.getMapping() != null) { + discriminator.getMapping().remove(mappedModel.getMappingName()); + } + discriminator.getMappedModels().remove(mappedModel); + } + } + } + + /** + * Returns the discriminator defined on the schema itself, including an inline + * allOf segment, but excluding discriminators inherited from parent schemas. + */ + private Discriminator getSchemaLocalDiscriminator(Schema schema) { + if (schema == null) { + return null; + } + + if (schema.getDiscriminator() != null) { + return schema.getDiscriminator(); + } + + if (ModelUtils.isComposedSchema(schema) && schema.getAllOf() != null) { + // Prefer inline allOf discriminator (child-local) over inherited parent discriminators. + for (Object allOfSchemaObj : schema.getAllOf()) { + if (!(allOfSchemaObj instanceof Schema)) { + continue; + } + Schema allOfSchema = (Schema) allOfSchemaObj; + if (allOfSchema.getDiscriminator() != null) { + return allOfSchema.getDiscriminator(); + } + } + } + + return null; + } + + /** + * Gets schema names referenced directly by oneOf/anyOf in the provided schema. + */ + private Set getComposedAlternativeSchemaNames(Schema schema) { + Set alternatives = new HashSet<>(); + if (schema == null || !ModelUtils.isComposedSchema(schema)) { + return alternatives; + } + + List oneOfSchemas = schema.getOneOf(); + if (oneOfSchemas != null) { + for (Schema oneOfSchema : oneOfSchemas) { + String ref = oneOfSchema != null ? oneOfSchema.get$ref() : null; + if (ref != null) { + alternatives.add(ModelUtils.getSimpleRef(ref)); + } + } + } + + List anyOfSchemas = schema.getAnyOf(); + if (anyOfSchemas != null) { + for (Schema anyOfSchema : anyOfSchemas) { + String ref = anyOfSchema != null ? anyOfSchema.get$ref() : null; + if (ref != null) { + alternatives.add(ModelUtils.getSimpleRef(ref)); + } + } + } + + return alternatives; + } + @Override public Map postProcessAllModels(Map objs) { objs = super.postProcessAllModels(objs); if (SERIALIZATION_LIBRARY_BUILT_VALUE.equals(library)) { adaptToDartInheritance(objs); syncRootTypesWithInnerVars(objs); + for (ModelsMap entry : objs.values()) { + for (ModelMap mo : entry.getModels()) { + CodegenModel cm = mo.getModel(); + if (cm != null && cm.discriminator != null) { + String ownerSchemaName = ObjectUtils.firstNonNull(cm.getSchemaName(), cm.getName(), cm.getClassname()); + orderMappedModelsBySchemaSpecificity(cm.discriminator, ownerSchemaName); + prepareDiscriminatorTemplateData(cm.discriminator, cm.getSchemaName(), cm.classname); + } + } + } } // loop through models to update the imports @@ -931,7 +1178,8 @@ private void processImports(List operationList, java.util.func } } - if (SERIALIZATION_LIBRARY_BUILT_VALUE.equals(library) && (op.getHasFormParams() || op.getHasQueryParams() || op.getHasPathParams())) { + if ((SERIALIZATION_LIBRARY_BUILT_VALUE.equals(library) || SERIALIZATION_LIBRARY_JSON_SERIALIZABLE.equals(library)) + && (op.getHasFormParams() || op.getHasQueryParams() || op.getHasPathParams())) { resultImports.add("package:" + pubName + "/" + sourceFolder + "/api_util.dart"); } diff --git a/modules/openapi-generator/src/main/resources/dart/libraries/dio/serialization/built_value/api/form_param.mustache b/modules/openapi-generator/src/main/resources/dart/libraries/dio/serialization/built_value/api/form_param.mustache new file mode 100644 index 000000000000..e54abbe1ee26 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/dart/libraries/dio/serialization/built_value/api/form_param.mustache @@ -0,0 +1 @@ +{{#isContainer}}{{#isMap}}{{#collectionFormat}}encodeParameter(_serializers, {{{paramName}}}, const FullType(BuiltMap, [FullType(String), FullType({{{baseType}}})]), asString: true, format: ListFormat.{{collectionFormat}}){{/collectionFormat}}{{^collectionFormat}}encodeParameter(_serializers, {{{paramName}}}, const FullType(BuiltMap, [FullType(String), FullType({{{baseType}}})])){{/collectionFormat}}{{/isMap}}{{^isMap}}{{#collectionFormat}}encodeParameter<{{{baseType}}}>(_serializers, {{{paramName}}}, const FullType(Built{{#isArray}}{{#uniqueItems}}Set{{/uniqueItems}}{{^uniqueItems}}List{{/uniqueItems}}{{/isArray}}, [FullType({{{baseType}}})]), asString: true, format: ListFormat.{{collectionFormat}}){{/collectionFormat}}{{^collectionFormat}}encodeParameter<{{{baseType}}}>(_serializers, {{{paramName}}}, const FullType(Built{{#isArray}}{{#uniqueItems}}Set{{/uniqueItems}}{{^uniqueItems}}List{{/uniqueItems}}{{/isArray}}, [FullType({{{baseType}}})])){{/collectionFormat}}{{/isMap}}{{/isContainer}}{{^isContainer}}encodeParameter(_serializers, {{{paramName}}}, const FullType({{{dataType}}}), asString: true){{/isContainer}} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/dart/libraries/dio/serialization/built_value/api/path_param.mustache b/modules/openapi-generator/src/main/resources/dart/libraries/dio/serialization/built_value/api/path_param.mustache index 1d633242d7cd..5803b2bbc126 100644 --- a/modules/openapi-generator/src/main/resources/dart/libraries/dio/serialization/built_value/api/path_param.mustache +++ b/modules/openapi-generator/src/main/resources/dart/libraries/dio/serialization/built_value/api/path_param.mustache @@ -1 +1 @@ -Uri.encodeComponent({{#includeLibraryTemplate}}api/query_param{{/includeLibraryTemplate}}.toString()) \ No newline at end of file +{{#isContainer}}{{#isMap}}encodePathParameter(_serializers, {{{paramName}}}, const FullType(BuiltMap, [FullType(String), FullType({{{baseType}}})])){{/isMap}}{{^isMap}}{{#isNullable}}{{{paramName}}} == null ? '' : {{/isNullable}}encodePathParameter(_serializers, {{{paramName}}}, const FullType(Built{{#isArray}}{{#uniqueItems}}Set{{/uniqueItems}}{{^uniqueItems}}List{{/uniqueItems}}{{/isArray}}, [FullType({{{baseType}}})]){{#collectionFormat}}, format: ListFormat.{{collectionFormat}}{{/collectionFormat}}){{/isMap}}{{/isContainer}}{{^isContainer}}encodePathParameter(_serializers, {{{paramName}}}, const FullType({{{dataType}}})){{/isContainer}} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/dart/libraries/dio/serialization/built_value/api/query_param.mustache b/modules/openapi-generator/src/main/resources/dart/libraries/dio/serialization/built_value/api/query_param.mustache index aa47f4f43940..189bc55509ed 100644 --- a/modules/openapi-generator/src/main/resources/dart/libraries/dio/serialization/built_value/api/query_param.mustache +++ b/modules/openapi-generator/src/main/resources/dart/libraries/dio/serialization/built_value/api/query_param.mustache @@ -1 +1 @@ -{{#isContainer}}{{#isMap}}encodeQueryParameter{{/isMap}}{{^isMap}}encodeCollectionQueryParameter<{{{baseType}}}>{{/isMap}}{{/isContainer}}{{^isContainer}}encodeQueryParameter{{/isContainer}}(_serializers, {{{paramName}}}, const FullType({{^isContainer}}{{{dataType}}}){{/isContainer}}{{#isContainer}}Built{{#isMap}}Map{{/isMap}}{{#isArray}}{{#uniqueItems}}Set{{/uniqueItems}}{{^uniqueItems}}List{{/uniqueItems}}{{/isArray}}, [{{#isMap}}FullType(String), {{/isMap}}FullType({{{baseType}}})]), {{#collectionFormat}}format: ListFormat.{{collectionFormat}},{{/collectionFormat}}{{/isContainer}}) \ No newline at end of file +{{#isContainer}}{{#isMap}}encodeParameter(_serializers, {{{paramName}}}, const FullType(BuiltMap, [FullType(String), FullType{{#items.isNullable}}.nullable{{/items.isNullable}}({{{baseType}}})])){{/isMap}}{{^isMap}}encodeParameter<{{{baseType}}}{{#items.isNullable}}?{{/items.isNullable}}>(_serializers, {{{paramName}}}, const FullType(Built{{#isArray}}{{#uniqueItems}}Set{{/uniqueItems}}{{^uniqueItems}}List{{/uniqueItems}}{{/isArray}}, [FullType{{#items.isNullable}}.nullable{{/items.isNullable}}({{{baseType}}})]), omitNullCollectionItems: true{{#collectionFormat}}, format: ListFormat.{{collectionFormat}}{{/collectionFormat}}){{/isMap}}{{/isContainer}}{{^isContainer}}encodeParameter(_serializers, {{{paramName}}}, const FullType({{{dataType}}})){{/isContainer}} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/dart/libraries/dio/serialization/built_value/api/serialize.mustache b/modules/openapi-generator/src/main/resources/dart/libraries/dio/serialization/built_value/api/serialize.mustache index c4f21f05ff70..69b36f7ab5bc 100644 --- a/modules/openapi-generator/src/main/resources/dart/libraries/dio/serialization/built_value/api/serialize.mustache +++ b/modules/openapi-generator/src/main/resources/dart/libraries/dio/serialization/built_value/api/serialize.mustache @@ -2,16 +2,23 @@ {{#isMultipart}} _bodyData = FormData.fromMap({ {{#formParams}} - {{^required}}if ({{{paramName}}} != null) {{/required}}r'{{{baseName}}}': {{#isFile}}{{{paramName}}}{{#isArray}}.toList(){{/isArray}}{{/isFile}}{{^isFile}}encodeFormParameter(_serializers, {{{paramName}}}, const FullType({{^isContainer}}{{{baseType}}}{{/isContainer}}{{#isContainer}}Built{{#isMap}}Map{{/isMap}}{{#isArray}}{{#uniqueItems}}Set{{/uniqueItems}}{{^uniqueItems}}List{{/uniqueItems}}{{/isArray}}, [{{#isMap}}FullType(String), {{/isMap}}FullType({{{baseType}}})]{{/isContainer}})){{/isFile}}, + {{^required}}if ({{{paramName}}} != null) {{/required}}r'{{{baseName}}}': {{#isFile}}{{{paramName}}}{{#isArray}}.toList(){{/isArray}}{{/isFile}}{{^isFile}}encodeParameter(_serializers, {{{paramName}}}, const FullType({{^isContainer}}{{{baseType}}}{{/isContainer}}{{#isContainer}}Built{{#isMap}}Map{{/isMap}}{{#isArray}}{{#uniqueItems}}Set{{/uniqueItems}}{{^uniqueItems}}List{{/uniqueItems}}{{/isArray}}, [{{#isMap}}FullType(String), {{/isMap}}FullType({{{baseType}}})]{{/isContainer}}), forMultipart: true){{/isFile}}, {{/formParams}} }); {{/isMultipart}} {{^isMultipart}} _bodyData = { {{#formParams}} - {{^required}}if ({{{paramName}}} != null) {{/required}}r'{{{baseName}}}': {{>serialization/built_value/api/query_param}}, + {{^required}}if ({{{paramName}}} != null) {{/required}}r'{{{baseName}}}': {{>serialization/built_value/api/form_param}}, {{/formParams}} }; + removeNullParametersExcept( + _bodyData, + { + {{#formParams}}{{#required}}{{#isNullable}}r'{{{baseName}}}', + {{/isNullable}}{{/required}}{{/formParams}} + }, + ); {{/isMultipart}} {{/hasFormParams}} {{#bodyParam}} diff --git a/modules/openapi-generator/src/main/resources/dart/libraries/dio/serialization/built_value/api_util.mustache b/modules/openapi-generator/src/main/resources/dart/libraries/dio/serialization/built_value/api_util.mustache index 45432b0042dc..757b860a5bcb 100644 --- a/modules/openapi-generator/src/main/resources/dart/libraries/dio/serialization/built_value/api_util.mustache +++ b/modules/openapi-generator/src/main/resources/dart/libraries/dio/serialization/built_value/api_util.mustache @@ -6,34 +6,21 @@ import 'package:built_collection/built_collection.dart'; import 'package:built_value/serializer.dart'; import 'package:dio/dio.dart'; -/// Format the given form parameter object into something that Dio can handle. -/// Returns primitive or String. -/// Returns List/Map if the value is BuildList/BuiltMap. -dynamic encodeFormParameter(Serializers serializers, dynamic value, FullType type) { - if (value == null) { - return ''; - } - if (value is String || value is num || value is bool) { - return value; - } - final serialized = serializers.serialize( - value as Object, - specifiedType: type, - ); - if (serialized is String) { - return serialized; - } - if (value is BuiltList || value is BuiltSet || value is BuiltMap) { - return serialized; - } - return json.encode(serialized); -} - -dynamic encodeQueryParameter( +/// Format normal parameters for query/form bodies. +/// Returns primitive, String, or `ListParam` for collections when `asString` +/// is false. +/// When `omitNullCollectionItems` is true, undefined collection members +/// are removed before query serialization. +dynamic encodeParameter( Serializers serializers, dynamic value, FullType type, -) { + { + ListFormat format = ListFormat.multi, + bool asString = false, + bool forMultipart = false, + bool omitNullCollectionItems = false, +}) { if (value == null) { return ''; } @@ -41,7 +28,6 @@ dynamic encodeQueryParameter( return value; } if (value is Uint8List) { - // Currently not sure how to serialize this return value; } final serialized = serializers.serialize( @@ -49,26 +35,90 @@ dynamic encodeQueryParameter( specifiedType: type, ); if (serialized == null) { - return ''; + return asString ? '' : null; + } + // Match the collection independently of T. Dart's generic type checks are + // reified, so BuiltList does not satisfy BuiltList. + if (value is BuiltList || value is BuiltSet) { + final values = List.of((serialized as Iterable).cast()); + if (omitNullCollectionItems) { + values.removeWhere((item) => item == null); + } + if (asString && format != ListFormat.multi && format != ListFormat.multiCompatible) { + return _joinCollectionValues(values, format); + } + if (forMultipart) { + return serialized; + } + return ListParam(values, format); } - if (serialized is String) { - return serialized; + // Model form fields are JSON text; explicit maps keep their form encoding. + if (asString && value is! BuiltMap && serialized is! String) { + return jsonEncode(serialized); } return serialized; } -ListParam encodeCollectionQueryParameter( +String encodePathParameter( Serializers serializers, dynamic value, FullType type, { ListFormat format = ListFormat.multi, }) { + if (value == null) { + return ''; + } final serialized = serializers.serialize( value as Object, specifiedType: type, ); - if (value is BuiltList || value is BuiltSet) { - return ListParam(List.of((serialized as Iterable).cast()), format); + if (serialized == null) { + return ''; + } + if (serialized is String || serialized is num || serialized is bool) { + return Uri.encodeComponent(serialized.toString()); + } + if (serialized is Iterable) { + final values = serialized.map((item) => _encodeBuiltValuePathValue(serializers, item, format)).toList(); + return _joinCollectionValues(values, format); + } + if (serialized is Map) { + final pairs = []; + serialized.forEach((k, v) { + final serializedKey = _encodeBuiltValuePathValue(serializers, k, format); + final serializedValue = _encodeBuiltValuePathValue(serializers, v, format); + pairs.add('$serializedKey,$serializedValue'); + }); + return pairs.join(','); + } + return Uri.encodeComponent(serialized.toString()); +} + +String _encodeBuiltValuePathValue( + Serializers serializers, + dynamic value, + ListFormat format, +) { + if (value == null || value is String || value is num || value is bool) { + return _encodePathValue(value, format); } - throw ArgumentError('Invalid value passed to encodeCollectionQueryParameter'); + if (value is Iterable) { + final values = value.map((item) => _encodeBuiltValuePathValue(serializers, item, format)).toList(); + return _joinCollectionValues(values, format); + } + if (value is Map) { + final pairs = []; + value.forEach((k, v) { + final serializedKey = _encodeBuiltValuePathValue(serializers, k, format); + final serializedValue = _encodeBuiltValuePathValue(serializers, v, format); + pairs.add('$serializedKey,$serializedValue'); + }); + return pairs.join(','); + } + final serialized = serializers.serialize(value as Object); + return _encodeBuiltValuePathValue(serializers, serialized, format); } + +{{>serialization/common/encode_path_parameter}} + +{{>serialization/common/remove_null_query_parameters}} diff --git a/modules/openapi-generator/src/main/resources/dart/libraries/dio/serialization/built_value/class_discriminator.mustache b/modules/openapi-generator/src/main/resources/dart/libraries/dio/serialization/built_value/class_discriminator.mustache index f867345883b1..a523fded4812 100644 --- a/modules/openapi-generator/src/main/resources/dart/libraries/dio/serialization/built_value/class_discriminator.mustache +++ b/modules/openapi-generator/src/main/resources/dart/libraries/dio/serialization/built_value/class_discriminator.mustache @@ -1,20 +1,31 @@ extension {{classname}}DiscriminatorExt on {{classname}} { String? get discriminatorValue { - {{#mappedModels}} + {{#vendorExtensions.x-discriminator-mapped-models-nonself}} if (this is {{modelName}}) { return r'{{mappingName}}'; } - {{/mappedModels}} + {{/vendorExtensions.x-discriminator-mapped-models-nonself}} + {{#vendorExtensions.x-has-discriminator-self-mapping}} + return r'{{vendorExtensions.x-discriminator-self-mapping-name}}'; + {{/vendorExtensions.x-has-discriminator-self-mapping}} + {{^vendorExtensions.x-has-discriminator-self-mapping}} return null; + {{/vendorExtensions.x-has-discriminator-self-mapping}} } } extension {{classname}}BuilderDiscriminatorExt on {{classname}}Builder { String? get discriminatorValue { - {{#mappedModels}} + {{#vendorExtensions.x-discriminator-mapped-models-nonself}} if (this is {{modelName}}Builder) { return r'{{mappingName}}'; } - {{/mappedModels}} + {{/vendorExtensions.x-discriminator-mapped-models-nonself}} + {{#vendorExtensions.x-has-discriminator-self-mapping}} + return r'{{vendorExtensions.x-discriminator-self-mapping-name}}'; + {{/vendorExtensions.x-has-discriminator-self-mapping}} + {{^vendorExtensions.x-has-discriminator-self-mapping}} return null; + {{/vendorExtensions.x-has-discriminator-self-mapping}} } -} \ No newline at end of file +} + diff --git a/modules/openapi-generator/src/main/resources/dart/libraries/dio/serialization/built_value/class_serializer.mustache b/modules/openapi-generator/src/main/resources/dart/libraries/dio/serialization/built_value/class_serializer.mustache index 4e64f91904af..9651e775ede9 100644 --- a/modules/openapi-generator/src/main/resources/dart/libraries/dio/serialization/built_value/class_serializer.mustache +++ b/modules/openapi-generator/src/main/resources/dart/libraries/dio/serialization/built_value/class_serializer.mustache @@ -91,11 +91,11 @@ class _${{classname}}Serializer implements PrimitiveSerializer<{{classname}}> { {{#hasDiscriminatorWithNonEmptyMapping}} {{#discriminator}} {{! handle discriminator }} - {{#mappedModels}} + {{#vendorExtensions.x-discriminator-mapped-models-nonself}} if (object is {{modelName}}) { return serializers.serialize(object, specifiedType: FullType({{modelName}}))!; } - {{/mappedModels}} + {{/vendorExtensions.x-discriminator-mapped-models-nonself}} {{/discriminator}} {{/hasDiscriminatorWithNonEmptyMapping}} return _serializeProperties(serializers, object, specifiedType: specifiedType).toList(); @@ -287,10 +287,10 @@ class _${{classname}}Serializer implements PrimitiveSerializer<{{classname}}> { final discIndex = serializedList.indexOf({{classname}}.discriminatorFieldName) + 1; final discValue = serializers.deserialize(serializedList[discIndex], specifiedType: FullType(String)) as String; switch (discValue) { - {{#mappedModels}} + {{#vendorExtensions.x-discriminator-mapped-models-nonself}} case r'{{mappingName}}': return serializers.deserialize(serialized, specifiedType: FullType({{modelName}})) as {{modelName}}; - {{/mappedModels}} + {{/vendorExtensions.x-discriminator-mapped-models-nonself}} default: return serializers.deserialize(serialized, specifiedType: FullType(${{classname}})) as ${{classname}}; } diff --git a/modules/openapi-generator/src/main/resources/dart/libraries/dio/serialization/built_value/serializers.mustache b/modules/openapi-generator/src/main/resources/dart/libraries/dio/serialization/built_value/serializers.mustache index 67ff3ea18cda..43aef4e8a8dd 100644 --- a/modules/openapi-generator/src/main/resources/dart/libraries/dio/serialization/built_value/serializers.mustache +++ b/modules/openapi-generator/src/main/resources/dart/libraries/dio/serialization/built_value/serializers.mustache @@ -30,7 +30,7 @@ Serializers serializers = (_$serializers.toBuilder(){{#builtValueSerializers}} {{^fullTypeArgs}} {{#isArray}} const FullType(Built{{#uniqueItems}}Set{{/uniqueItems}}{{^uniqueItems}}List{{/uniqueItems}}, [FullType{{#isNullable}}.nullable{{/isNullable}}({{dataType}})]), - () => {{#uniqueItems}}Set{{/uniqueItems}}{{^uniqueItems}}List{{/uniqueItems}}Builder<{{dataType}}>(), + () => {{#uniqueItems}}Set{{/uniqueItems}}{{^uniqueItems}}List{{/uniqueItems}}Builder<{{dataType}}{{#isNullable}}?{{/isNullable}}>(), {{/isArray}} {{#isMap}} const FullType(BuiltMap, [FullType(String), FullType{{#isNullable}}.nullable{{/isNullable}}({{dataType}})]), diff --git a/modules/openapi-generator/src/main/resources/dart/libraries/dio/serialization/common/encode_path_parameter.mustache b/modules/openapi-generator/src/main/resources/dart/libraries/dio/serialization/common/encode_path_parameter.mustache new file mode 100644 index 000000000000..7647ce5c4aab --- /dev/null +++ b/modules/openapi-generator/src/main/resources/dart/libraries/dio/serialization/common/encode_path_parameter.mustache @@ -0,0 +1,39 @@ +String _encodePathValue(dynamic value, ListFormat format) { + if (value == null) { + return ''; + } + if (value is String || value is num || value is bool || value is Enum) { + return Uri.encodeComponent(value.toString()); + } + if (value is List) { + final values = value.map((item) => _encodePathValue(item, format)).toList(); + return _joinCollectionValues(values, format); + } + if (value is Map) { + final pairs = []; + value.forEach((k, v) { + final serializedKey = _encodePathValue(k, format); + final serializedValue = _encodePathValue(v, format); + pairs.add('$serializedKey,$serializedValue'); + }); + return pairs.join(','); + } + return Uri.encodeComponent(value.toString()); +} + +String _joinCollectionValues(List values, ListFormat format) { + switch (format) { + case ListFormat.csv: + return values.join(','); + case ListFormat.ssv: + return values.join(' '); + case ListFormat.tsv: + return values.join('\t'); + case ListFormat.pipes: + return values.join('|'); + case ListFormat.multi: + return values.join(','); + case ListFormat.multiCompatible: + return values.join(','); + } +} diff --git a/modules/openapi-generator/src/main/resources/dart/libraries/dio/serialization/common/remove_null_query_parameters.mustache b/modules/openapi-generator/src/main/resources/dart/libraries/dio/serialization/common/remove_null_query_parameters.mustache new file mode 100644 index 000000000000..83e8510dcb38 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/dart/libraries/dio/serialization/common/remove_null_query_parameters.mustache @@ -0,0 +1,6 @@ +void removeNullParametersExcept( + Map parameters, + Set requiredParameters, +) { + parameters.removeWhere((key, value) => value == null && !requiredParameters.contains(key)); +} diff --git a/modules/openapi-generator/src/main/resources/dart/libraries/dio/serialization/json_serializable/api/path_param.mustache b/modules/openapi-generator/src/main/resources/dart/libraries/dio/serialization/json_serializable/api/path_param.mustache index 1d633242d7cd..61805066cb59 100644 --- a/modules/openapi-generator/src/main/resources/dart/libraries/dio/serialization/json_serializable/api/path_param.mustache +++ b/modules/openapi-generator/src/main/resources/dart/libraries/dio/serialization/json_serializable/api/path_param.mustache @@ -1 +1 @@ -Uri.encodeComponent({{#includeLibraryTemplate}}api/query_param{{/includeLibraryTemplate}}.toString()) \ No newline at end of file +{{#isContainer}}{{#isNullable}}{{{paramName}}} == null ? '' : encodePathParameter({{{paramName}}}{{#collectionFormat}}, format: ListFormat.{{collectionFormat}}{{/collectionFormat}}){{/isNullable}}{{^isNullable}}encodePathParameter({{{paramName}}}{{#collectionFormat}}, format: ListFormat.{{collectionFormat}}{{/collectionFormat}}){{/isNullable}}{{/isContainer}}{{^isContainer}}{{#isNullable}}{{{paramName}}} == null ? '' : encodePathParameter({{#isModel}}{{{paramName}}}.toJson(){{/isModel}}{{^isModel}}{{{paramName}}}{{/isModel}}){{/isNullable}}{{^isNullable}}encodePathParameter({{#isModel}}{{{paramName}}}.toJson(){{/isModel}}{{^isModel}}{{{paramName}}}{{/isModel}}){{/isNullable}}{{/isContainer}} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/dart/libraries/dio/serialization/json_serializable/api/serialize.mustache b/modules/openapi-generator/src/main/resources/dart/libraries/dio/serialization/json_serializable/api/serialize.mustache index a21e8e7d8831..c44b9414c502 100644 --- a/modules/openapi-generator/src/main/resources/dart/libraries/dio/serialization/json_serializable/api/serialize.mustache +++ b/modules/openapi-generator/src/main/resources/dart/libraries/dio/serialization/json_serializable/api/serialize.mustache @@ -12,6 +12,13 @@ {{^required}}if ({{{paramName}}} != null) {{/required}}r'{{{baseName}}}': {{#isModel}}jsonEncode({{{paramName}}}){{/isModel}}{{^isModel}}{{#items.isModel}}jsonEncode({{{paramName}}}{{#uniqueItems}}.toList(){{/uniqueItems}}){{/items.isModel}}{{^items.isModel}}{{{paramName}}}{{/items.isModel}}{{/isModel}}, {{/formParams}} }; + removeNullParametersExcept( + _bodyData, + { + {{#formParams}}{{#required}}{{#isNullable}}r'{{{baseName}}}', + {{/isNullable}}{{/required}}{{/formParams}} + }, + ); {{/isMultipart}} {{/hasFormParams}} {{#bodyParam}} diff --git a/modules/openapi-generator/src/main/resources/dart/libraries/dio/serialization/json_serializable/api_util.mustache b/modules/openapi-generator/src/main/resources/dart/libraries/dio/serialization/json_serializable/api_util.mustache new file mode 100644 index 000000000000..76eaea8e6094 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/dart/libraries/dio/serialization/json_serializable/api_util.mustache @@ -0,0 +1,16 @@ +{{>header}} +import 'package:dio/dio.dart'; + +String encodePathParameter( + dynamic value, { + ListFormat format = ListFormat.multi, +}) { + if (value == null) { + return ''; + } + return _encodePathValue(value, format); +} + +{{>serialization/common/encode_path_parameter}} + +{{>serialization/common/remove_null_query_parameters}} diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/dart/dio/DartDioClientCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/dart/dio/DartDioClientCodegenTest.java index e501bb1d5c0d..f868d642654b 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/dart/dio/DartDioClientCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/dart/dio/DartDioClientCodegenTest.java @@ -244,20 +244,20 @@ public void testQueryParameterNullGuardsFollowRequiredness() throws IOException // caller omitted the parameter, so the generated request must omit it. TestUtils.assertFileContains(defaultApi, "if (activityId != null)", - "r'activity_id': encodeQueryParameter(_serializers, activityId, const FullType(int)),", + "r'activity_id': encodeParameter(_serializers, activityId, const FullType(int)),", "if (categoryId != null)", - "r'category_id': encodeQueryParameter(_serializers, categoryId, const FullType(int)),"); + "r'category_id': encodeParameter(_serializers, categoryId, const FullType(int)),"); // Required query parameters are always emitted. If the schema is // nullable, null is an explicit supplied value rather than omission. TestUtils.assertFileContains(defaultApi, - "r'required_id': encodeQueryParameter(_serializers, requiredId, const FullType(int)),", - "r'required_nullable_id': encodeQueryParameter(_serializers, requiredNullableId, const FullType(int)),"); + "r'required_id': encodeParameter(_serializers, requiredId, const FullType(int)),", + "r'required_nullable_id': encodeParameter(_serializers, requiredNullableId, const FullType(int)),"); TestUtils.assertFileNotContains(defaultApi, "if (requiredId != null)", "if (requiredNullableId != null)", - "final _queryParameters = {\n r'activity_id': encodeQueryParameter(_serializers, activityId, const FullType(int)),", - "final _queryParameters = {\n r'category_id': encodeQueryParameter(_serializers, categoryId, const FullType(int)),"); + "final _queryParameters = {\n r'activity_id': encodeParameter(_serializers, activityId, const FullType(int)),", + "final _queryParameters = {\n r'category_id': encodeParameter(_serializers, categoryId, const FullType(int)),"); } /** @@ -328,6 +328,7 @@ public void verifyWebhookImports() throws IOException { .setPackageName("my-package") .setInputSpec("src/test/resources/3_1/webhooks.yaml") .setOutputDir(output.getAbsolutePath().replace("\\", "/")); + configurator.addAdditionalProperty("pubName", "my_api"); ClientOptInput opts = configurator.toClientOptInput(); @@ -346,11 +347,13 @@ public void verifyWebhookImports() throws IOException { // Bug symptom #22586: Map-style import with HTML entity encoding // Before fix: import '{import=model.Pet, classname=Pet}'; - // After fix: import 'package:my-package/src/model/pet.dart'; Assert.assertFalse(apiContent.contains("import '{"), - "Webhook should not contain Map-style import (bug #22586 symptom)"); + "Webhook should not contain Map-style imports"); Assert.assertFalse(apiContent.contains("="), - "Webhook should not contain HTML entity encoding (bug #22586 symptom)"); + "Webhook imports should not contain HTML entity encoding"); + Assert.assertTrue( + apiContent.contains("import 'package:my_api/src/model/pet.dart';"), + "Webhook should import the Pet model using a valid Dart package import"); } /** @@ -397,6 +400,374 @@ public void testNestedAdditionalPropertiesGetBuilderFactories() throws IOExcepti "() => MapBuilder>(),"); } + /** + * Regression test for nullable collection item types in serializers.dart. + * + *

When a list or set property has a nullable item type (e.g. + * {@code items: { type: number, nullable: true }}), the generated + * {@code addBuilderFactory} call must use {@code ListBuilder()} + * (or {@code SetBuilder()}) to match the emitted + * {@code FullType.nullable(double)} / {@code FullType.nullable(String)}. + * + *

Before the fix, the builder instantiation was + * {@code ListBuilder()} — without the {@code ?} — causing a + * {@code FullType} mismatch at runtime. + */ + @Test + public void testNullableCollectionItemsGetNullableBuilderFactory() throws IOException { + File output = Files.createTempDirectory("test").toFile(); + output.deleteOnExit(); + + final CodegenConfigurator configurator = new CodegenConfigurator() + .setGeneratorName("dart-dio") + .setInputSpec("src/test/resources/3_0/dart-dio/built_value_nullable_collection_items.yaml") + .setOutputDir(output.getAbsolutePath().replace("\\", "/")); + + ClientOptInput opts = configurator.toClientOptInput(); + Generator generator = new DefaultGenerator().opts(opts); + List files = generator.generate(); + files.forEach(File::deleteOnExit); + + Path serializers = output.toPath().resolve("lib/src/serializers.dart"); + Path defaultApi = output.toPath().resolve("lib/src/api/default_api.dart"); + Path apiUtil = output.toPath().resolve("lib/src/api_util.dart"); + + // BuiltList with nullable double items: FullType must be .nullable and + // the builder factory must carry the ? on the type argument. + TestUtils.assertFileContains(serializers, + "const FullType(BuiltList, [FullType.nullable(double)]),", + "() => ListBuilder(),"); + + // BuiltSet with nullable String items. + TestUtils.assertFileContains(serializers, + "const FullType(BuiltSet, [FullType.nullable(String)]),", + "() => SetBuilder(),"); + + // BuiltMap with nullable String values. + TestUtils.assertFileContains(serializers, + "const FullType(BuiltMap, [FullType(String), FullType.nullable(String)]),", + "() => MapBuilder(),"); + + // Negative: non-nullable builder variants must NOT appear. + TestUtils.assertFileNotContains(serializers, + "() => ListBuilder(),"); + TestUtils.assertFileNotContains(serializers, + "() => SetBuilder(),"); + + // Query collection types must preserve nullable items so built_value + // serializes them with the same FullType used by the generated model. + // ListFormat then maps the OpenAPI wire style to Dio: repeated keys, + // comma-separated, space-separated, and pipe-separated values. + TestUtils.assertFileContains(defaultApi, + "encodeParameter(_serializers, repeated, const FullType(BuiltList, [FullType.nullable(String)]), omitNullCollectionItems: true, format: ListFormat.multi)", + "encodeParameter(_serializers, commaSeparated, const FullType(BuiltList, [FullType.nullable(String)]), omitNullCollectionItems: true, format: ListFormat.csv)", + "encodeParameter(_serializers, spaceSeparated, const FullType(BuiltList, [FullType.nullable(String)]), omitNullCollectionItems: true, format: ListFormat.ssv)", + "encodeParameter(_serializers, pipeSeparated, const FullType(BuiltList, [FullType.nullable(String)]), omitNullCollectionItems: true, format: ListFormat.pipes)", + "encodeParameter(_serializers, nonNullable, const FullType(BuiltList, [FullType(String)]), omitNullCollectionItems: true, format: ListFormat.csv)"); + + // The query-only null filter must not make non-nullable item types + // nullable in either the generic argument or the FullType metadata. + TestUtils.assertFileNotContains(defaultApi, + "encodeParameter(_serializers, nonNullable", + "encodeParameter(_serializers, nonNullable, const FullType(BuiltList, [FullType.nullable(String)])"); + + // Collection detection must not depend on an exact reified generic + // match, otherwise nullable-item collections bypass ListParam. + TestUtils.assertFileContains(apiUtil, + "if (value is BuiltList || value is BuiltSet)", + "if (omitNullCollectionItems)", + "values.removeWhere((item) => item == null);", + "return ListParam(values, format);"); + } + + /** + * Regression test: path parameters whose type is an enum must use + * {@code encodeParameter} (which calls the built_value serializer and + * returns the wire name) rather than a bare {@code .toString()} call (which + * returns the Dart identifier name and differs when the wire name contains + * underscores, e.g. {@code unknown_default_open_api} vs the Dart identifier + * {@code unknownDefaultOpenApi}). + * + *

Both required enum path params must emit + * {@code encodePathParameter(_serializers, param, const FullType(EnumType))}, + * never {@code param.toString()} directly. + */ + @Test + public void testEnumPathParamsUseSerializerNotToString() throws IOException { + File output = Files.createTempDirectory("test").toFile(); + output.deleteOnExit(); + + final CodegenConfigurator configurator = new CodegenConfigurator() + .setGeneratorName("dart-dio") + .setInputSpec("src/test/resources/3_0/dart-dio/path_param_enum_encoding.yaml") + .setOutputDir(output.getAbsolutePath().replace("\\", "/")); + + ClientOptInput opts = configurator.toClientOptInput(); + Generator generator = new DefaultGenerator().opts(opts); + List files = generator.generate(); + files.forEach(File::deleteOnExit); + + Path defaultApi = output.toPath().resolve("lib/src/api/default_api.dart"); + + // Both enum path params must route through the serializer, which returns + // the wire name (e.g. "unknown_default_open_api"), not .toString() which + // returns the Dart identifier name (e.g. "unknownDefaultOpenApi"). + TestUtils.assertFileContains(defaultApi, + "encodePathParameter(_serializers, status, const FullType(OrderStatus))"); + TestUtils.assertFileContains(defaultApi, + "encodePathParameter(_serializers, category, const FullType(CategoryType))"); + + // No param should fall back to a bare .toString() call on the enum value. + TestUtils.assertFileNotContains(defaultApi, + "status.toString()", + "category.toString()"); + } + + /** + * Regression test: built_value collection serializers may return an + * {@code Iterable} that is not a {@code List}, notably for set-valued path + * parameters. Both the top-level and recursive path encoders must handle + * those values as collections instead of encoding their debug strings. + */ + @Test + public void testBuiltValuePathHelperHandlesIterableCollections() throws IOException { + File output = Files.createTempDirectory("test").toFile(); + output.deleteOnExit(); + + final CodegenConfigurator configurator = new CodegenConfigurator() + .setGeneratorName("dart-dio") + .setInputSpec("src/test/resources/3_0/dart-dio/path_param_enum_encoding.yaml") + .setOutputDir(output.getAbsolutePath().replace("\\", "/")); + + ClientOptInput opts = configurator.toClientOptInput(); + Generator generator = new DefaultGenerator().opts(opts); + List files = generator.generate(); + files.forEach(File::deleteOnExit); + + Path apiUtil = output.toPath().resolve("lib/src/api_util.dart"); + TestUtils.assertFileContains(apiUtil, + "if (serialized is Iterable) {", + "if (value is Iterable) {"); + } + + @Test + public void testJsonSerializablePathHelperDelegatesValueEncoding() throws IOException { + File output = Files.createTempDirectory("test").toFile(); + output.deleteOnExit(); + + final CodegenConfigurator configurator = new CodegenConfigurator() + .setGeneratorName("dart-dio") + .setInputSpec("src/test/resources/3_0/dart/petstore-with-fake-endpoints-models-for-testing.yaml") + .setOutputDir(output.getAbsolutePath().replace("\\", "/")) + .addAdditionalProperty(CodegenConstants.SERIALIZATION_LIBRARY, + DartDioClientCodegen.SERIALIZATION_LIBRARY_JSON_SERIALIZABLE); + + ClientOptInput opts = configurator.toClientOptInput(); + Generator generator = new DefaultGenerator().opts(opts); + List files = generator.generate(); + files.forEach(File::deleteOnExit); + + Path apiUtil = output.toPath().resolve("lib/src/api_util.dart"); + TestUtils.assertFileContains(apiUtil, + "if (value == null) {\n" + + " return '';\n" + + " }\n" + + " return _encodePathValue(value, format);\n" + + "}"); + } + + @Test + public void testJsonSerializableModelPathParameterUsesJsonMap() throws IOException { + File output = Files.createTempDirectory("test").toFile(); + output.deleteOnExit(); + + final CodegenConfigurator configurator = new CodegenConfigurator() + .setGeneratorName("dart-dio") + .setInputSpec("src/test/resources/3_0/dart-dio/path_param_enum_encoding.yaml") + .setOutputDir(output.getAbsolutePath().replace("\\", "/")) + .addAdditionalProperty(CodegenConstants.SERIALIZATION_LIBRARY, + DartDioClientCodegen.SERIALIZATION_LIBRARY_JSON_SERIALIZABLE); + + ClientOptInput opts = configurator.toClientOptInput(); + Generator generator = new DefaultGenerator().opts(opts); + List files = generator.generate(); + files.forEach(File::deleteOnExit); + + Path defaultApi = output.toPath().resolve("lib/src/api/default_api.dart"); + TestUtils.assertFileContains(defaultApi, + "encodePathParameter(category.toJson())"); + } + + /** + * Regression test: built_value form array params need both the call site + * and helper definition. Without encodeParameter in api_util, + * generated APIs compile-fail on the form array path. + */ + @Test + public void testCollectionFormParameterHelperIsGenerated() throws IOException { + File output = Files.createTempDirectory("test").toFile(); + output.deleteOnExit(); + + final CodegenConfigurator configurator = new CodegenConfigurator() + .setGeneratorName("dart-dio") + .setInputSpec("src/test/resources/3_0/dart/petstore-with-fake-endpoints-models-for-testing.yaml") + .setOutputDir(output.getAbsolutePath().replace("\\", "/")); + + ClientOptInput opts = configurator.toClientOptInput(); + Generator generator = new DefaultGenerator().opts(opts); + List files = generator.generate(); + files.forEach(File::deleteOnExit); + + Path apiUtil = output.toPath().resolve("lib/src/api_util.dart"); + Path fakeApi = output.toPath().resolve("lib/src/api/fake_api.dart"); + + TestUtils.assertFileContains(apiUtil, + "dynamic encodeParameter(", + "bool asString = false,", + "bool forMultipart = false,", + "import 'dart:convert';", + "if (asString && value is! BuiltMap && serialized is! String)", + "return jsonEncode(serialized);", + "case ListFormat.multiCompatible:", + "return ListParam(values, format);"); + TestUtils.assertFileContains(fakeApi, + "encodeParameter(_serializers, enumFormStringArray, const FullType(BuiltList, [FullType(String)]),", + "asString: true"); + } + + @Test + public void testDefaultMultiFormArrayUsesCollectionEncoding() throws IOException { + File output = Files.createTempDirectory("test").toFile(); + output.deleteOnExit(); + + final CodegenConfigurator configurator = new CodegenConfigurator() + .setGeneratorName("dart-dio") + .setInputSpec("src/test/resources/3_0/issue_10865_default_values.yaml") + .setOutputDir(output.getAbsolutePath().replace("\\", "/")); + + ClientOptInput opts = configurator.toClientOptInput(); + Generator generator = new DefaultGenerator().opts(opts); + List files = generator.generate(); + files.forEach(File::deleteOnExit); + + Path defaultApi = output.toPath().resolve("lib/src/api/default_api.dart"); + + TestUtils.assertFileContains(defaultApi, + "r'fn4': encodeParameter", + "const FullType(BuiltList, [FullType(String)])"); + String generatedApi = Files.readString(defaultApi); + int fn4Start = generatedApi.indexOf("r'fn4':"); + Assert.assertTrue(fn4Start >= 0, "Generated API should contain the fn4 form parameter"); + int fn4End = generatedApi.indexOf('\n', fn4Start); + String fn4Entry = generatedApi.substring(fn4Start, fn4End >= 0 ? fn4End : generatedApi.length()); + Assert.assertFalse(fn4Entry.contains("asString: true"), + "fn4 should use collection encoding rather than string encoding"); + } + + @Test + public void testRequiredNullableFormParametersArePreserved() throws IOException { + File output = Files.createTempDirectory("test").toFile(); + output.deleteOnExit(); + + Path spec = Files.createTempFile("dart-dio-required-nullable-form", ".yaml"); + Files.writeString(spec, + "swagger: '2.0'\n" + + "info:\n" + + " title: Form nullability\n" + + " version: 1.0.0\n" + + "consumes:\n" + + " - application/x-www-form-urlencoded\n" + + "paths:\n" + + " /test:\n" + + " post:\n" + + " operationId: testForm\n" + + " parameters:\n" + + " - name: requiredNullable\n" + + " in: formData\n" + + " required: true\n" + + " type: string\n" + + " x-nullable: true\n" + + " - name: optionalNullable\n" + + " in: formData\n" + + " required: false\n" + + " type: string\n" + + " x-nullable: true\n" + + " responses:\n" + + " '200':\n" + + " description: ok\n"); + spec.toFile().deleteOnExit(); + + final CodegenConfigurator configurator = new CodegenConfigurator() + .setGeneratorName("dart-dio") + .setInputSpec(spec.toAbsolutePath().toString()) + .setOutputDir(output.getAbsolutePath().replace("\\", "/")); + + ClientOptInput opts = configurator.toClientOptInput(); + Generator generator = new DefaultGenerator().opts(opts); + List files = generator.generate(); + files.forEach(File::deleteOnExit); + + Path defaultApi = output.toPath().resolve("lib/src/api/default_api.dart"); + + TestUtils.assertFileContains(defaultApi, + "removeNullParametersExcept(", + "r'requiredNullable',"); + } + + @Test + public void testRequiredNullableQueryParametersUseEmptyValues() throws IOException { + File output = Files.createTempDirectory("test").toFile(); + output.deleteOnExit(); + + Path spec = Files.createTempFile("dart-dio-required-nullable-query", ".yaml"); + Files.writeString(spec, + "swagger: '2.0'\n" + + "info:\n" + + " title: Query nullability\n" + + " version: 1.0.0\n" + + "paths:\n" + + " /test:\n" + + " get:\n" + + " operationId: testQuery\n" + + " parameters:\n" + + " - name: requiredNullable\n" + + " in: query\n" + + " required: true\n" + + " type: string\n" + + " x-nullable: true\n" + + " - name: optionalNullable\n" + + " in: query\n" + + " required: false\n" + + " type: string\n" + + " x-nullable: true\n" + + " responses:\n" + + " '200':\n" + + " description: ok\n"); + spec.toFile().deleteOnExit(); + + final CodegenConfigurator configurator = new CodegenConfigurator() + .setGeneratorName("dart-dio") + .setInputSpec(spec.toAbsolutePath().toString()) + .setOutputDir(output.getAbsolutePath().replace("\\", "/")); + + ClientOptInput opts = configurator.toClientOptInput(); + Generator generator = new DefaultGenerator().opts(opts); + List files = generator.generate(); + files.forEach(File::deleteOnExit); + + Path defaultApi = output.toPath().resolve("lib/src/api/default_api.dart"); + Path apiUtil = output.toPath().resolve("lib/src/api_util.dart"); + + TestUtils.assertFileContains(defaultApi, + "String? requiredNullable,", + "r'requiredNullable': encodeParameter(_serializers, requiredNullable, const FullType(String)),", + "if (optionalNullable != null) r'optionalNullable': encodeParameter(_serializers, optionalNullable, const FullType(String)),"); + TestUtils.assertFileContains(apiUtil, + "if (value == null) {\n" + + " return '';\n" + + " }"); + } + @Test public void testFreezedSerializationLibraryGeneratesModels() throws IOException { File output = Files.createTempDirectory("test").toFile(); @@ -475,7 +846,7 @@ public void testFreezedIgnoresUseOptional() throws IOException { * Path parameters must be URI-encoded as path components: the query_param helpers pass * strings through unchanged, so a value containing '/' or spaces would otherwise break * the request path. Every path substitution must therefore go through - * Uri.encodeComponent. + * a path encoder that applies Uri.encodeComponent. */ @Test public void testPathParamsAreUriEncoded() throws IOException { @@ -504,7 +875,9 @@ public void testPathParamsAreUriEncoded() throws IOException { int substitutions = content.split("\\.replaceAll\\(", -1).length - 1; Assert.assertTrue(substitutions > 0, "expected path substitutions in " + apiFile + ": " + content); - int encodings = content.split("Uri\\.encodeComponent\\(", -1).length - 1; + String encoder = DartDioClientCodegen.SERIALIZATION_LIBRARY_FREEZED.equals(serializationLibrary) + ? "Uri\\.encodeComponent\\(" : "encodePathParameter\\("; + int encodings = content.split(encoder, -1).length - 1; Assert.assertEquals(encodings, substitutions, "every path substitution in " + apiFile + " must be URI-encoded: " + content); } diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/dart/dio/DartDioModelTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/dart/dio/DartDioModelTest.java index 0d602450214c..968d007a84a8 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/dart/dio/DartDioModelTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/dart/dio/DartDioModelTest.java @@ -387,6 +387,136 @@ public void mapModelTest() { Assert.assertEquals(cm.vars.size(), 0); } + @Test(description = "uses schema-local discriminator mapping for dart-dio") + public void localDiscriminatorUsesDeclaredMappingsOnly() { + OpenAPI openAPI = TestUtils.parseSpec("src/test/resources/bugs/issue_15467.json"); + + final DefaultCodegen codegen = new DartDioClientCodegen(); + codegen.additionalProperties().put(CodegenConstants.SERIALIZATION_LIBRARY, DartDioClientCodegen.SERIALIZATION_LIBRARY_BUILT_VALUE); + codegen.processOpts(); + codegen.setOpenAPI(openAPI); + + final String modelName = "Animal"; + final Schema model = openAPI.getComponents().getSchemas().get(modelName); + final CodegenModel cm = codegen.fromModel(modelName, model); + + Assert.assertNotNull(cm.discriminator); + Assert.assertEquals(cm.discriminator.getMapping().size(), 4); + Assert.assertTrue(cm.discriminator.getMapping().containsKey("Bird")); + Assert.assertTrue(cm.discriminator.getMapping().containsKey("Reptile")); + Assert.assertTrue(cm.discriminator.getMapping().containsKey("Crocodile")); + Assert.assertTrue(cm.discriminator.getMapping().containsKey("Turtle")); + Assert.assertFalse(cm.discriminator.getMapping().containsKey("Lizard")); + } + + @Test(description = "does not leak sibling mappings into inherited discriminator for intermediate allOf models") + public void inheritedDiscriminatorKeepsOnlyDescendantsForIntermediateModel() { + OpenAPI openAPI = TestUtils.parseSpec("src/test/resources/bugs/issue_15467.json"); + + final DefaultCodegen codegen = new DartDioClientCodegen(); + codegen.additionalProperties().put(CodegenConstants.SERIALIZATION_LIBRARY, DartDioClientCodegen.SERIALIZATION_LIBRARY_BUILT_VALUE); + codegen.processOpts(); + codegen.setOpenAPI(openAPI); + + final Schema reptileSchema = openAPI.getComponents().getSchemas().get("Reptile"); + final CodegenModel reptileModel = codegen.fromModel("Reptile", reptileSchema); + + Assert.assertNotNull(reptileModel.discriminator); + Assert.assertNotNull(reptileModel.discriminator.getMapping()); + Assert.assertTrue(reptileModel.discriminator.getMapping().containsKey("Crocodile")); + Assert.assertTrue(reptileModel.discriminator.getMapping().containsKey("Turtle")); + Assert.assertTrue(reptileModel.discriminator.getMapping().containsKey("Reptile")); + Assert.assertFalse(reptileModel.discriminator.getMapping().containsKey("Bird")); + + java.util.List reptileMappedModelOrder = reptileModel.discriminator.getMappedModels().stream() + .map(CodegenDiscriminator.MappedModel::getModelName) + .collect(java.util.stream.Collectors.toList()); + Assert.assertEquals(reptileMappedModelOrder.get(reptileMappedModelOrder.size() - 1), "Reptile"); + + Assert.assertEquals(reptileModel.discriminator.getVendorExtensions().get("x-has-discriminator-self-mapping"), Boolean.TRUE); + Assert.assertEquals(reptileModel.discriminator.getVendorExtensions().get("x-discriminator-self-mapping-name"), "Reptile"); + @SuppressWarnings("unchecked") + java.util.List reptileNonSelfMappedModels = + (java.util.List) reptileModel.discriminator.getVendorExtensions().get("x-discriminator-mapped-models-nonself"); + Assert.assertNotNull(reptileNonSelfMappedModels); + Assert.assertEquals( + reptileNonSelfMappedModels.stream() + .map(CodegenDiscriminator.MappedModel::getModelName) + .collect(java.util.stream.Collectors.toList()), + java.util.Arrays.asList("Crocodile", "Turtle")); + + final Schema animalSchema = openAPI.getComponents().getSchemas().get("Animal"); + final CodegenModel animalModel = codegen.fromModel("Animal", animalSchema); + Assert.assertNotNull(animalModel.discriminator); + Assert.assertNotNull(animalModel.discriminator.getMapping()); + Assert.assertTrue(animalModel.discriminator.getMapping().containsKey("Bird")); + Assert.assertTrue(animalModel.discriminator.getMapping().containsKey("Reptile")); + Assert.assertEquals(animalModel.discriminator.getVendorExtensions().get("x-has-discriminator-self-mapping"), Boolean.FALSE); + } + + @Test(description = "orders discriminator mappings so subclasses are checked before ancestor types") + public void discriminatorChecksSubclassesBeforeParentTypes() { + OpenAPI openAPI = TestUtils.parseSpec("src/test/resources/bugs/issue_15467.json"); + + final DefaultCodegen codegen = new DartDioClientCodegen(); + codegen.additionalProperties().put(CodegenConstants.SERIALIZATION_LIBRARY, DartDioClientCodegen.SERIALIZATION_LIBRARY_BUILT_VALUE); + codegen.processOpts(); + codegen.setOpenAPI(openAPI); + + final Schema animalSchema = openAPI.getComponents().getSchemas().get("Animal"); + final CodegenModel animalModel = codegen.fromModel("Animal", animalSchema); + + Assert.assertNotNull(animalModel.discriminator); + java.util.List mappedModelOrder = animalModel.discriminator.getMappedModels().stream() + .map(CodegenDiscriminator.MappedModel::getModelName) + .collect(java.util.stream.Collectors.toList()); + + int turtleIndex = mappedModelOrder.indexOf("Turtle"); + int crocodileIndex = mappedModelOrder.indexOf("Crocodile"); + int reptileIndex = mappedModelOrder.indexOf("Reptile"); + Assert.assertTrue(turtleIndex >= 0, "Turtle must be present in the discriminator mappings"); + Assert.assertTrue(crocodileIndex >= 0, "Crocodile must be present in the discriminator mappings"); + Assert.assertTrue(reptileIndex >= 0, "Reptile must be present in the discriminator mappings"); + Assert.assertTrue(turtleIndex < reptileIndex); + Assert.assertTrue(crocodileIndex < reptileIndex); + + @SuppressWarnings("unchecked") + java.util.List animalNonSelfMappedModels = + (java.util.List) animalModel.discriminator.getVendorExtensions().get("x-discriminator-mapped-models-nonself"); + Assert.assertNotNull(animalNonSelfMappedModels); + java.util.List animalNonSelfMappedModelOrder = animalNonSelfMappedModels.stream() + .map(CodegenDiscriminator.MappedModel::getModelName) + .collect(java.util.stream.Collectors.toList()); + int nonSelfTurtleIndex = animalNonSelfMappedModelOrder.indexOf("Turtle"); + int nonSelfCrocodileIndex = animalNonSelfMappedModelOrder.indexOf("Crocodile"); + int nonSelfReptileIndex = animalNonSelfMappedModelOrder.indexOf("Reptile"); + Assert.assertTrue(nonSelfTurtleIndex >= 0, "Turtle must be present in the non-self discriminator mappings"); + Assert.assertTrue(nonSelfCrocodileIndex >= 0, "Crocodile must be present in the non-self discriminator mappings"); + Assert.assertTrue(nonSelfReptileIndex >= 0, "Reptile must be present in the non-self discriminator mappings"); + Assert.assertTrue(nonSelfTurtleIndex < nonSelfReptileIndex); + Assert.assertTrue(nonSelfCrocodileIndex < nonSelfReptileIndex); + } + + @Test(description = "preserves declared oneOf alternatives on inherited discriminators") + public void inheritedDiscriminatorKeepsDeclaredOneOfAlternatives() { + OpenAPI openAPI = TestUtils.parseSpec("src/test/resources/bugs/issue_23717_inherited_discriminator_oneof.yaml"); + + final DefaultCodegen codegen = new DartDioClientCodegen(); + codegen.additionalProperties().put(CodegenConstants.SERIALIZATION_LIBRARY, DartDioClientCodegen.SERIALIZATION_LIBRARY_BUILT_VALUE); + codegen.processOpts(); + codegen.setOpenAPI(openAPI); + + final Schema reptileSchema = openAPI.getComponents().getSchemas().get("Reptile"); + final CodegenModel reptileModel = codegen.fromModel("Reptile", reptileSchema); + + Assert.assertNotNull(reptileModel.discriminator); + Assert.assertNotNull(reptileModel.discriminator.getMapping()); + Assert.assertTrue(reptileModel.discriminator.getMapping().containsKey("Reptile")); + Assert.assertTrue(reptileModel.discriminator.getMapping().containsKey("Lizard")); + Assert.assertTrue(reptileModel.discriminator.getMapping().containsKey("Snake")); + Assert.assertFalse(reptileModel.discriminator.getMapping().containsKey("Bird")); + } + @DataProvider(name = "modelNames") public static Object[][] modelNames() { return new Object[][]{ diff --git a/modules/openapi-generator/src/test/resources/3_0/dart-dio/built_value_nullable_collection_items.yaml b/modules/openapi-generator/src/test/resources/3_0/dart-dio/built_value_nullable_collection_items.yaml new file mode 100644 index 000000000000..05138bb52257 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/dart-dio/built_value_nullable_collection_items.yaml @@ -0,0 +1,91 @@ +openapi: "3.0.0" +info: + title: Built-value nullable collection item BuilderFactory test + version: "1.0.0" +paths: + /items: + get: + operationId: getItems + parameters: + - name: repeated + in: query + style: form + explode: true + schema: + type: array + items: + type: string + nullable: true + - name: commaSeparated + in: query + style: form + explode: false + schema: + type: array + items: + type: string + nullable: true + - name: spaceSeparated + in: query + style: spaceDelimited + explode: false + schema: + type: array + items: + type: string + nullable: true + - name: pipeSeparated + in: query + style: pipeDelimited + explode: false + schema: + type: array + items: + type: string + nullable: true + - name: nonNullable + in: query + style: form + explode: false + schema: + type: array + items: + type: string + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/Container" +components: + schemas: + Widget: + type: object + properties: + id: + type: integer + # A BuiltList whose item type is nullable (double?). + # Before the fix, serializers.dart emitted ListBuilder() instead + # of ListBuilder(), causing a FullType mismatch at runtime. + Container: + type: object + properties: + scores: + type: array + items: + type: number + format: double + nullable: true + tags: + type: array + uniqueItems: true + items: + type: string + nullable: true + lookup: + type: object + additionalProperties: + type: string + nullable: true + diff --git a/modules/openapi-generator/src/test/resources/3_0/dart-dio/path_param_enum_encoding.yaml b/modules/openapi-generator/src/test/resources/3_0/dart-dio/path_param_enum_encoding.yaml new file mode 100644 index 000000000000..be545b124eec --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/dart-dio/path_param_enum_encoding.yaml @@ -0,0 +1,70 @@ +openapi: "3.0.0" +info: + title: Path parameter enum encoding test + version: "1.0.0" +paths: + /orders/{status}: + get: + operationId: getOrderByStatus + parameters: + - name: status + in: path + required: true + schema: + $ref: "#/components/schemas/OrderStatus" + responses: + "200": + description: OK + /items/{category}: + get: + operationId: getItemByCategory + parameters: + - name: category + in: path + required: true + schema: + $ref: "#/components/schemas/CategoryType" + responses: + "200": + description: OK + /categories/{category}: + get: + operationId: getCategory + parameters: + - name: category + in: path + required: true + schema: + $ref: "#/components/schemas/Category" + responses: + "200": + description: OK +components: + schemas: + # Enum whose wire names differ from their Dart camelCase identifiers. + # unknown_default_open_api -> Dart: unknownDefaultOpenApi + # type_a -> Dart: typeA + # These must be encoded via the serializer (wire name), NOT via + # .toString() (Dart identifier name). + OrderStatus: + type: string + enum: + - placed + - approved + - unknown_default_open_api + CategoryType: + type: string + enum: + - type_a + - type_b + Category: + type: object + required: + - id + - name + properties: + id: + type: integer + format: int64 + name: + type: string diff --git a/modules/openapi-generator/src/test/resources/bugs/issue_15467.json b/modules/openapi-generator/src/test/resources/bugs/issue_15467.json new file mode 100644 index 000000000000..c7c430464c48 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/bugs/issue_15467.json @@ -0,0 +1,75 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "Test API", + "version": "v1" + }, + "paths": {}, + "components": { + "schemas": { + "Animal": { + "type": "object", + "discriminator": { + "propertyName": "type", + "mapping": { + "Bird": "#/components/schemas/Bird", + "Reptile": "#/components/schemas/Reptile", + "Crocodile": "#/components/schemas/Crocodile", + "Turtle": "#/components/schemas/Turtle" + } + }, + "properties": { + "id": {"type": "string", "nullable": true}, + "name": {"type": "string", "nullable": true}, + "age": {"type": "integer", "format": "int32"}, + "type": {"type": "string", "nullable": true} + } + }, + "Bird": { + "allOf": [ + {"$ref": "#/components/schemas/Animal"}, + { + "type": "object", + "properties": { + "wingSpan": {"type": "number", "format": "double"} + } + } + ] + }, + "Reptile": { + "allOf": [ + {"$ref": "#/components/schemas/Animal"}, + { + "type": "object", + "properties": { + "scaleColor": {"type": "string", "nullable": true} + } + } + ] + }, + "Crocodile": { + "allOf": [ + {"$ref": "#/components/schemas/Reptile"}, + { + "type": "object", + "properties": { + "numberOfTeeth": {"type": "integer", "format": "int32"} + } + } + ] + }, + "Turtle": { + "allOf": [ + {"$ref": "#/components/schemas/Reptile"}, + { + "type": "object", + "properties": { + "shellDiameter": {"type": "number", "format": "double"} + } + } + ] + } + } + } +} + diff --git a/modules/openapi-generator/src/test/resources/bugs/issue_23717_inherited_discriminator_oneof.yaml b/modules/openapi-generator/src/test/resources/bugs/issue_23717_inherited_discriminator_oneof.yaml new file mode 100644 index 000000000000..15c944d7f8be --- /dev/null +++ b/modules/openapi-generator/src/test/resources/bugs/issue_23717_inherited_discriminator_oneof.yaml @@ -0,0 +1,51 @@ +openapi: 3.0.0 +info: + title: Test API + version: v1 +paths: {} +components: + schemas: + Animal: + type: object + discriminator: + propertyName: type + mapping: + Bird: '#/components/schemas/Bird' + Reptile: '#/components/schemas/Reptile' + Lizard: '#/components/schemas/Lizard' + Snake: '#/components/schemas/Snake' + properties: + type: + type: string + Bird: + allOf: + - $ref: '#/components/schemas/Animal' + - type: object + properties: + wingSpan: + type: number + format: double + Reptile: + allOf: + - $ref: '#/components/schemas/Animal' + - type: object + properties: + scaleColor: + type: string + oneOf: + - $ref: '#/components/schemas/Lizard' + - $ref: '#/components/schemas/Snake' + Lizard: + allOf: + - $ref: '#/components/schemas/Animal' + - type: object + properties: + canRegrowTail: + type: boolean + Snake: + allOf: + - $ref: '#/components/schemas/Animal' + - type: object + properties: + venomous: + type: boolean diff --git a/samples/openapi3/client/petstore/dart-dio/anyof/lib/src/api_util.dart b/samples/openapi3/client/petstore/dart-dio/anyof/lib/src/api_util.dart index ed3bb12f25b8..ba125b61525b 100644 --- a/samples/openapi3/client/petstore/dart-dio/anyof/lib/src/api_util.dart +++ b/samples/openapi3/client/petstore/dart-dio/anyof/lib/src/api_util.dart @@ -9,34 +9,21 @@ import 'package:built_collection/built_collection.dart'; import 'package:built_value/serializer.dart'; import 'package:dio/dio.dart'; -/// Format the given form parameter object into something that Dio can handle. -/// Returns primitive or String. -/// Returns List/Map if the value is BuildList/BuiltMap. -dynamic encodeFormParameter(Serializers serializers, dynamic value, FullType type) { - if (value == null) { - return ''; - } - if (value is String || value is num || value is bool) { - return value; - } - final serialized = serializers.serialize( - value as Object, - specifiedType: type, - ); - if (serialized is String) { - return serialized; - } - if (value is BuiltList || value is BuiltSet || value is BuiltMap) { - return serialized; - } - return json.encode(serialized); -} - -dynamic encodeQueryParameter( +/// Format normal parameters for query/form bodies. +/// Returns primitive, String, or `ListParam` for collections when `asString` +/// is false. +/// When `omitNullCollectionItems` is true, undefined collection members +/// are removed before query serialization. +dynamic encodeParameter( Serializers serializers, dynamic value, FullType type, -) { + { + ListFormat format = ListFormat.multi, + bool asString = false, + bool forMultipart = false, + bool omitNullCollectionItems = false, +}) { if (value == null) { return ''; } @@ -44,7 +31,6 @@ dynamic encodeQueryParameter( return value; } if (value is Uint8List) { - // Currently not sure how to serialize this return value; } final serialized = serializers.serialize( @@ -52,26 +38,133 @@ dynamic encodeQueryParameter( specifiedType: type, ); if (serialized == null) { - return ''; + return asString ? '' : null; + } + // Match the collection independently of T. Dart's generic type checks are + // reified, so BuiltList does not satisfy BuiltList. + if (value is BuiltList || value is BuiltSet) { + final values = List.of((serialized as Iterable).cast()); + if (omitNullCollectionItems) { + values.removeWhere((item) => item == null); + } + if (asString && format != ListFormat.multi && format != ListFormat.multiCompatible) { + return _joinCollectionValues(values, format); + } + if (forMultipart) { + return serialized; + } + return ListParam(values, format); } - if (serialized is String) { - return serialized; + // Model form fields are JSON text; explicit maps keep their form encoding. + if (asString && value is! BuiltMap && serialized is! String) { + return jsonEncode(serialized); } return serialized; } -ListParam encodeCollectionQueryParameter( +String encodePathParameter( Serializers serializers, dynamic value, FullType type, { ListFormat format = ListFormat.multi, }) { + if (value == null) { + return ''; + } final serialized = serializers.serialize( value as Object, specifiedType: type, ); - if (value is BuiltList || value is BuiltSet) { - return ListParam(List.of((serialized as Iterable).cast()), format); + if (serialized == null) { + return ''; + } + if (serialized is String || serialized is num || serialized is bool) { + return Uri.encodeComponent(serialized.toString()); } - throw ArgumentError('Invalid value passed to encodeCollectionQueryParameter'); + if (serialized is Iterable) { + final values = serialized.map((item) => _encodeBuiltValuePathValue(serializers, item, format)).toList(); + return _joinCollectionValues(values, format); + } + if (serialized is Map) { + final pairs = []; + serialized.forEach((k, v) { + final serializedKey = _encodeBuiltValuePathValue(serializers, k, format); + final serializedValue = _encodeBuiltValuePathValue(serializers, v, format); + pairs.add('$serializedKey,$serializedValue'); + }); + return pairs.join(','); + } + return Uri.encodeComponent(serialized.toString()); +} + +String _encodeBuiltValuePathValue( + Serializers serializers, + dynamic value, + ListFormat format, +) { + if (value == null || value is String || value is num || value is bool) { + return _encodePathValue(value, format); + } + if (value is Iterable) { + final values = value.map((item) => _encodeBuiltValuePathValue(serializers, item, format)).toList(); + return _joinCollectionValues(values, format); + } + if (value is Map) { + final pairs = []; + value.forEach((k, v) { + final serializedKey = _encodeBuiltValuePathValue(serializers, k, format); + final serializedValue = _encodeBuiltValuePathValue(serializers, v, format); + pairs.add('$serializedKey,$serializedValue'); + }); + return pairs.join(','); + } + final serialized = serializers.serialize(value as Object); + return _encodeBuiltValuePathValue(serializers, serialized, format); +} + +String _encodePathValue(dynamic value, ListFormat format) { + if (value == null) { + return ''; + } + if (value is String || value is num || value is bool || value is Enum) { + return Uri.encodeComponent(value.toString()); + } + if (value is List) { + final values = value.map((item) => _encodePathValue(item, format)).toList(); + return _joinCollectionValues(values, format); + } + if (value is Map) { + final pairs = []; + value.forEach((k, v) { + final serializedKey = _encodePathValue(k, format); + final serializedValue = _encodePathValue(v, format); + pairs.add('$serializedKey,$serializedValue'); + }); + return pairs.join(','); + } + return Uri.encodeComponent(value.toString()); +} + +String _joinCollectionValues(List values, ListFormat format) { + switch (format) { + case ListFormat.csv: + return values.join(','); + case ListFormat.ssv: + return values.join(' '); + case ListFormat.tsv: + return values.join('\t'); + case ListFormat.pipes: + return values.join('|'); + case ListFormat.multi: + return values.join(','); + case ListFormat.multiCompatible: + return values.join(','); + } +} + +void removeNullParametersExcept( + Map parameters, + Set requiredParameters, +) { + parameters.removeWhere((key, value) => value == null && !requiredParameters.contains(key)); } diff --git a/samples/openapi3/client/petstore/dart-dio/binary_response/.openapi-generator/FILES b/samples/openapi3/client/petstore/dart-dio/binary_response/.openapi-generator/FILES index 809d28620973..fbf70217c43d 100644 --- a/samples/openapi3/client/petstore/dart-dio/binary_response/.openapi-generator/FILES +++ b/samples/openapi3/client/petstore/dart-dio/binary_response/.openapi-generator/FILES @@ -6,6 +6,7 @@ doc/DefaultApi.md lib/openapi.dart lib/src/api.dart lib/src/api/default_api.dart +lib/src/api_util.dart lib/src/auth/api_key_auth.dart lib/src/auth/auth.dart lib/src/auth/basic_auth.dart diff --git a/samples/openapi3/client/petstore/dart-dio/binary_response/lib/src/api_util.dart b/samples/openapi3/client/petstore/dart-dio/binary_response/lib/src/api_util.dart new file mode 100644 index 000000000000..f903c6a9b06d --- /dev/null +++ b/samples/openapi3/client/petstore/dart-dio/binary_response/lib/src/api_util.dart @@ -0,0 +1,62 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// + +import 'package:dio/dio.dart'; + +String encodePathParameter( + dynamic value, { + ListFormat format = ListFormat.multi, +}) { + if (value == null) { + return ''; + } + return _encodePathValue(value, format); +} + +String _encodePathValue(dynamic value, ListFormat format) { + if (value == null) { + return ''; + } + if (value is String || value is num || value is bool || value is Enum) { + return Uri.encodeComponent(value.toString()); + } + if (value is List) { + final values = value.map((item) => _encodePathValue(item, format)).toList(); + return _joinCollectionValues(values, format); + } + if (value is Map) { + final pairs = []; + value.forEach((k, v) { + final serializedKey = _encodePathValue(k, format); + final serializedValue = _encodePathValue(v, format); + pairs.add('$serializedKey,$serializedValue'); + }); + return pairs.join(','); + } + return Uri.encodeComponent(value.toString()); +} + +String _joinCollectionValues(List values, ListFormat format) { + switch (format) { + case ListFormat.csv: + return values.join(','); + case ListFormat.ssv: + return values.join(' '); + case ListFormat.tsv: + return values.join('\t'); + case ListFormat.pipes: + return values.join('|'); + case ListFormat.multi: + return values.join(','); + case ListFormat.multiCompatible: + return values.join(','); + } +} + +void removeNullParametersExcept( + Map parameters, + Set requiredParameters, +) { + parameters.removeWhere((key, value) => value == null && !requiredParameters.contains(key)); +} diff --git a/samples/openapi3/client/petstore/dart-dio/oneof/lib/src/api_util.dart b/samples/openapi3/client/petstore/dart-dio/oneof/lib/src/api_util.dart index ed3bb12f25b8..ba125b61525b 100644 --- a/samples/openapi3/client/petstore/dart-dio/oneof/lib/src/api_util.dart +++ b/samples/openapi3/client/petstore/dart-dio/oneof/lib/src/api_util.dart @@ -9,34 +9,21 @@ import 'package:built_collection/built_collection.dart'; import 'package:built_value/serializer.dart'; import 'package:dio/dio.dart'; -/// Format the given form parameter object into something that Dio can handle. -/// Returns primitive or String. -/// Returns List/Map if the value is BuildList/BuiltMap. -dynamic encodeFormParameter(Serializers serializers, dynamic value, FullType type) { - if (value == null) { - return ''; - } - if (value is String || value is num || value is bool) { - return value; - } - final serialized = serializers.serialize( - value as Object, - specifiedType: type, - ); - if (serialized is String) { - return serialized; - } - if (value is BuiltList || value is BuiltSet || value is BuiltMap) { - return serialized; - } - return json.encode(serialized); -} - -dynamic encodeQueryParameter( +/// Format normal parameters for query/form bodies. +/// Returns primitive, String, or `ListParam` for collections when `asString` +/// is false. +/// When `omitNullCollectionItems` is true, undefined collection members +/// are removed before query serialization. +dynamic encodeParameter( Serializers serializers, dynamic value, FullType type, -) { + { + ListFormat format = ListFormat.multi, + bool asString = false, + bool forMultipart = false, + bool omitNullCollectionItems = false, +}) { if (value == null) { return ''; } @@ -44,7 +31,6 @@ dynamic encodeQueryParameter( return value; } if (value is Uint8List) { - // Currently not sure how to serialize this return value; } final serialized = serializers.serialize( @@ -52,26 +38,133 @@ dynamic encodeQueryParameter( specifiedType: type, ); if (serialized == null) { - return ''; + return asString ? '' : null; + } + // Match the collection independently of T. Dart's generic type checks are + // reified, so BuiltList does not satisfy BuiltList. + if (value is BuiltList || value is BuiltSet) { + final values = List.of((serialized as Iterable).cast()); + if (omitNullCollectionItems) { + values.removeWhere((item) => item == null); + } + if (asString && format != ListFormat.multi && format != ListFormat.multiCompatible) { + return _joinCollectionValues(values, format); + } + if (forMultipart) { + return serialized; + } + return ListParam(values, format); } - if (serialized is String) { - return serialized; + // Model form fields are JSON text; explicit maps keep their form encoding. + if (asString && value is! BuiltMap && serialized is! String) { + return jsonEncode(serialized); } return serialized; } -ListParam encodeCollectionQueryParameter( +String encodePathParameter( Serializers serializers, dynamic value, FullType type, { ListFormat format = ListFormat.multi, }) { + if (value == null) { + return ''; + } final serialized = serializers.serialize( value as Object, specifiedType: type, ); - if (value is BuiltList || value is BuiltSet) { - return ListParam(List.of((serialized as Iterable).cast()), format); + if (serialized == null) { + return ''; + } + if (serialized is String || serialized is num || serialized is bool) { + return Uri.encodeComponent(serialized.toString()); } - throw ArgumentError('Invalid value passed to encodeCollectionQueryParameter'); + if (serialized is Iterable) { + final values = serialized.map((item) => _encodeBuiltValuePathValue(serializers, item, format)).toList(); + return _joinCollectionValues(values, format); + } + if (serialized is Map) { + final pairs = []; + serialized.forEach((k, v) { + final serializedKey = _encodeBuiltValuePathValue(serializers, k, format); + final serializedValue = _encodeBuiltValuePathValue(serializers, v, format); + pairs.add('$serializedKey,$serializedValue'); + }); + return pairs.join(','); + } + return Uri.encodeComponent(serialized.toString()); +} + +String _encodeBuiltValuePathValue( + Serializers serializers, + dynamic value, + ListFormat format, +) { + if (value == null || value is String || value is num || value is bool) { + return _encodePathValue(value, format); + } + if (value is Iterable) { + final values = value.map((item) => _encodeBuiltValuePathValue(serializers, item, format)).toList(); + return _joinCollectionValues(values, format); + } + if (value is Map) { + final pairs = []; + value.forEach((k, v) { + final serializedKey = _encodeBuiltValuePathValue(serializers, k, format); + final serializedValue = _encodeBuiltValuePathValue(serializers, v, format); + pairs.add('$serializedKey,$serializedValue'); + }); + return pairs.join(','); + } + final serialized = serializers.serialize(value as Object); + return _encodeBuiltValuePathValue(serializers, serialized, format); +} + +String _encodePathValue(dynamic value, ListFormat format) { + if (value == null) { + return ''; + } + if (value is String || value is num || value is bool || value is Enum) { + return Uri.encodeComponent(value.toString()); + } + if (value is List) { + final values = value.map((item) => _encodePathValue(item, format)).toList(); + return _joinCollectionValues(values, format); + } + if (value is Map) { + final pairs = []; + value.forEach((k, v) { + final serializedKey = _encodePathValue(k, format); + final serializedValue = _encodePathValue(v, format); + pairs.add('$serializedKey,$serializedValue'); + }); + return pairs.join(','); + } + return Uri.encodeComponent(value.toString()); +} + +String _joinCollectionValues(List values, ListFormat format) { + switch (format) { + case ListFormat.csv: + return values.join(','); + case ListFormat.ssv: + return values.join(' '); + case ListFormat.tsv: + return values.join('\t'); + case ListFormat.pipes: + return values.join('|'); + case ListFormat.multi: + return values.join(','); + case ListFormat.multiCompatible: + return values.join(','); + } +} + +void removeNullParametersExcept( + Map parameters, + Set requiredParameters, +) { + parameters.removeWhere((key, value) => value == null && !requiredParameters.contains(key)); } diff --git a/samples/openapi3/client/petstore/dart-dio/oneof_polymorphism_and_inheritance/lib/src/api_util.dart b/samples/openapi3/client/petstore/dart-dio/oneof_polymorphism_and_inheritance/lib/src/api_util.dart index ed3bb12f25b8..ba125b61525b 100644 --- a/samples/openapi3/client/petstore/dart-dio/oneof_polymorphism_and_inheritance/lib/src/api_util.dart +++ b/samples/openapi3/client/petstore/dart-dio/oneof_polymorphism_and_inheritance/lib/src/api_util.dart @@ -9,34 +9,21 @@ import 'package:built_collection/built_collection.dart'; import 'package:built_value/serializer.dart'; import 'package:dio/dio.dart'; -/// Format the given form parameter object into something that Dio can handle. -/// Returns primitive or String. -/// Returns List/Map if the value is BuildList/BuiltMap. -dynamic encodeFormParameter(Serializers serializers, dynamic value, FullType type) { - if (value == null) { - return ''; - } - if (value is String || value is num || value is bool) { - return value; - } - final serialized = serializers.serialize( - value as Object, - specifiedType: type, - ); - if (serialized is String) { - return serialized; - } - if (value is BuiltList || value is BuiltSet || value is BuiltMap) { - return serialized; - } - return json.encode(serialized); -} - -dynamic encodeQueryParameter( +/// Format normal parameters for query/form bodies. +/// Returns primitive, String, or `ListParam` for collections when `asString` +/// is false. +/// When `omitNullCollectionItems` is true, undefined collection members +/// are removed before query serialization. +dynamic encodeParameter( Serializers serializers, dynamic value, FullType type, -) { + { + ListFormat format = ListFormat.multi, + bool asString = false, + bool forMultipart = false, + bool omitNullCollectionItems = false, +}) { if (value == null) { return ''; } @@ -44,7 +31,6 @@ dynamic encodeQueryParameter( return value; } if (value is Uint8List) { - // Currently not sure how to serialize this return value; } final serialized = serializers.serialize( @@ -52,26 +38,133 @@ dynamic encodeQueryParameter( specifiedType: type, ); if (serialized == null) { - return ''; + return asString ? '' : null; + } + // Match the collection independently of T. Dart's generic type checks are + // reified, so BuiltList does not satisfy BuiltList. + if (value is BuiltList || value is BuiltSet) { + final values = List.of((serialized as Iterable).cast()); + if (omitNullCollectionItems) { + values.removeWhere((item) => item == null); + } + if (asString && format != ListFormat.multi && format != ListFormat.multiCompatible) { + return _joinCollectionValues(values, format); + } + if (forMultipart) { + return serialized; + } + return ListParam(values, format); } - if (serialized is String) { - return serialized; + // Model form fields are JSON text; explicit maps keep their form encoding. + if (asString && value is! BuiltMap && serialized is! String) { + return jsonEncode(serialized); } return serialized; } -ListParam encodeCollectionQueryParameter( +String encodePathParameter( Serializers serializers, dynamic value, FullType type, { ListFormat format = ListFormat.multi, }) { + if (value == null) { + return ''; + } final serialized = serializers.serialize( value as Object, specifiedType: type, ); - if (value is BuiltList || value is BuiltSet) { - return ListParam(List.of((serialized as Iterable).cast()), format); + if (serialized == null) { + return ''; + } + if (serialized is String || serialized is num || serialized is bool) { + return Uri.encodeComponent(serialized.toString()); } - throw ArgumentError('Invalid value passed to encodeCollectionQueryParameter'); + if (serialized is Iterable) { + final values = serialized.map((item) => _encodeBuiltValuePathValue(serializers, item, format)).toList(); + return _joinCollectionValues(values, format); + } + if (serialized is Map) { + final pairs = []; + serialized.forEach((k, v) { + final serializedKey = _encodeBuiltValuePathValue(serializers, k, format); + final serializedValue = _encodeBuiltValuePathValue(serializers, v, format); + pairs.add('$serializedKey,$serializedValue'); + }); + return pairs.join(','); + } + return Uri.encodeComponent(serialized.toString()); +} + +String _encodeBuiltValuePathValue( + Serializers serializers, + dynamic value, + ListFormat format, +) { + if (value == null || value is String || value is num || value is bool) { + return _encodePathValue(value, format); + } + if (value is Iterable) { + final values = value.map((item) => _encodeBuiltValuePathValue(serializers, item, format)).toList(); + return _joinCollectionValues(values, format); + } + if (value is Map) { + final pairs = []; + value.forEach((k, v) { + final serializedKey = _encodeBuiltValuePathValue(serializers, k, format); + final serializedValue = _encodeBuiltValuePathValue(serializers, v, format); + pairs.add('$serializedKey,$serializedValue'); + }); + return pairs.join(','); + } + final serialized = serializers.serialize(value as Object); + return _encodeBuiltValuePathValue(serializers, serialized, format); +} + +String _encodePathValue(dynamic value, ListFormat format) { + if (value == null) { + return ''; + } + if (value is String || value is num || value is bool || value is Enum) { + return Uri.encodeComponent(value.toString()); + } + if (value is List) { + final values = value.map((item) => _encodePathValue(item, format)).toList(); + return _joinCollectionValues(values, format); + } + if (value is Map) { + final pairs = []; + value.forEach((k, v) { + final serializedKey = _encodePathValue(k, format); + final serializedValue = _encodePathValue(v, format); + pairs.add('$serializedKey,$serializedValue'); + }); + return pairs.join(','); + } + return Uri.encodeComponent(value.toString()); +} + +String _joinCollectionValues(List values, ListFormat format) { + switch (format) { + case ListFormat.csv: + return values.join(','); + case ListFormat.ssv: + return values.join(' '); + case ListFormat.tsv: + return values.join('\t'); + case ListFormat.pipes: + return values.join('|'); + case ListFormat.multi: + return values.join(','); + case ListFormat.multiCompatible: + return values.join(','); + } +} + +void removeNullParametersExcept( + Map parameters, + Set requiredParameters, +) { + parameters.removeWhere((key, value) => value == null && !requiredParameters.contains(key)); } diff --git a/samples/openapi3/client/petstore/dart-dio/oneof_polymorphism_and_inheritance/lib/src/model/bar_ref_or_value.dart b/samples/openapi3/client/petstore/dart-dio/oneof_polymorphism_and_inheritance/lib/src/model/bar_ref_or_value.dart index da9d22c502e6..b9f6a6809b53 100644 --- a/samples/openapi3/client/petstore/dart-dio/oneof_polymorphism_and_inheritance/lib/src/model/bar_ref_or_value.dart +++ b/samples/openapi3/client/petstore/dart-dio/oneof_polymorphism_and_inheritance/lib/src/model/bar_ref_or_value.dart @@ -71,6 +71,8 @@ extension BarRefOrValueBuilderDiscriminatorExt on BarRefOrValueBuilder { } } + + class _$BarRefOrValueSerializer implements PrimitiveSerializer { @override final Iterable types = const [BarRefOrValue, _$BarRefOrValue]; diff --git a/samples/openapi3/client/petstore/dart-dio/oneof_polymorphism_and_inheritance/lib/src/model/entity.dart b/samples/openapi3/client/petstore/dart-dio/oneof_polymorphism_and_inheritance/lib/src/model/entity.dart index 7965d016a273..a4f81c45b519 100644 --- a/samples/openapi3/client/petstore/dart-dio/oneof_polymorphism_and_inheritance/lib/src/model/entity.dart +++ b/samples/openapi3/client/petstore/dart-dio/oneof_polymorphism_and_inheritance/lib/src/model/entity.dart @@ -29,12 +29,12 @@ abstract class Entity implements Addressable, Extensible { static const String discriminatorFieldName = r'@type'; static const Map discriminatorMapping = { + r'PizzaSpeziale': PizzaSpeziale, r'Bar': Bar, r'Bar_Create': BarCreate, r'Foo': Foo, r'Pasta': Pasta, r'Pizza': Pizza, - r'PizzaSpeziale': PizzaSpeziale, }; @BuiltValueSerializer(custom: true) @@ -43,6 +43,9 @@ abstract class Entity implements Addressable, Extensible { extension EntityDiscriminatorExt on Entity { String? get discriminatorValue { + if (this is PizzaSpeziale) { + return r'PizzaSpeziale'; + } if (this is Bar) { return r'Bar'; } @@ -58,14 +61,14 @@ extension EntityDiscriminatorExt on Entity { if (this is Pizza) { return r'Pizza'; } - if (this is PizzaSpeziale) { - return r'PizzaSpeziale'; - } return null; } } extension EntityBuilderDiscriminatorExt on EntityBuilder { String? get discriminatorValue { + if (this is PizzaSpezialeBuilder) { + return r'PizzaSpeziale'; + } if (this is BarBuilder) { return r'Bar'; } @@ -81,13 +84,12 @@ extension EntityBuilderDiscriminatorExt on EntityBuilder { if (this is PizzaBuilder) { return r'Pizza'; } - if (this is PizzaSpezialeBuilder) { - return r'PizzaSpeziale'; - } return null; } } + + class _$EntitySerializer implements PrimitiveSerializer { @override final Iterable types = const [Entity]; @@ -141,6 +143,9 @@ class _$EntitySerializer implements PrimitiveSerializer { Entity object, { FullType specifiedType = FullType.unspecified, }) { + if (object is PizzaSpeziale) { + return serializers.serialize(object, specifiedType: FullType(PizzaSpeziale))!; + } if (object is Bar) { return serializers.serialize(object, specifiedType: FullType(Bar))!; } @@ -156,9 +161,6 @@ class _$EntitySerializer implements PrimitiveSerializer { if (object is Pizza) { return serializers.serialize(object, specifiedType: FullType(Pizza))!; } - if (object is PizzaSpeziale) { - return serializers.serialize(object, specifiedType: FullType(PizzaSpeziale))!; - } return _serializeProperties(serializers, object, specifiedType: specifiedType).toList(); } @@ -172,6 +174,8 @@ class _$EntitySerializer implements PrimitiveSerializer { final discIndex = serializedList.indexOf(Entity.discriminatorFieldName) + 1; final discValue = serializers.deserialize(serializedList[discIndex], specifiedType: FullType(String)) as String; switch (discValue) { + case r'PizzaSpeziale': + return serializers.deserialize(serialized, specifiedType: FullType(PizzaSpeziale)) as PizzaSpeziale; case r'Bar': return serializers.deserialize(serialized, specifiedType: FullType(Bar)) as Bar; case r'Bar_Create': @@ -182,8 +186,6 @@ class _$EntitySerializer implements PrimitiveSerializer { return serializers.deserialize(serialized, specifiedType: FullType(Pasta)) as Pasta; case r'Pizza': return serializers.deserialize(serialized, specifiedType: FullType(Pizza)) as Pizza; - case r'PizzaSpeziale': - return serializers.deserialize(serialized, specifiedType: FullType(PizzaSpeziale)) as PizzaSpeziale; default: return serializers.deserialize(serialized, specifiedType: FullType($Entity)) as $Entity; } diff --git a/samples/openapi3/client/petstore/dart-dio/oneof_polymorphism_and_inheritance/lib/src/model/entity_ref.dart b/samples/openapi3/client/petstore/dart-dio/oneof_polymorphism_and_inheritance/lib/src/model/entity_ref.dart index 9db50cb7d6ac..b95b32a1dfbc 100644 --- a/samples/openapi3/client/petstore/dart-dio/oneof_polymorphism_and_inheritance/lib/src/model/entity_ref.dart +++ b/samples/openapi3/client/petstore/dart-dio/oneof_polymorphism_and_inheritance/lib/src/model/entity_ref.dart @@ -66,6 +66,8 @@ extension EntityRefBuilderDiscriminatorExt on EntityRefBuilder { } } + + class _$EntityRefSerializer implements PrimitiveSerializer { @override final Iterable types = const [EntityRef]; diff --git a/samples/openapi3/client/petstore/dart-dio/oneof_polymorphism_and_inheritance/lib/src/model/foo_ref_or_value.dart b/samples/openapi3/client/petstore/dart-dio/oneof_polymorphism_and_inheritance/lib/src/model/foo_ref_or_value.dart index 5eb0e894671e..bda694e6f853 100644 --- a/samples/openapi3/client/petstore/dart-dio/oneof_polymorphism_and_inheritance/lib/src/model/foo_ref_or_value.dart +++ b/samples/openapi3/client/petstore/dart-dio/oneof_polymorphism_and_inheritance/lib/src/model/foo_ref_or_value.dart @@ -70,6 +70,8 @@ extension FooRefOrValueBuilderDiscriminatorExt on FooRefOrValueBuilder { } } + + class _$FooRefOrValueSerializer implements PrimitiveSerializer { @override final Iterable types = const [FooRefOrValue, _$FooRefOrValue]; diff --git a/samples/openapi3/client/petstore/dart-dio/oneof_polymorphism_and_inheritance/lib/src/model/fruit.dart b/samples/openapi3/client/petstore/dart-dio/oneof_polymorphism_and_inheritance/lib/src/model/fruit.dart index 48651d978c23..132fb657567d 100644 --- a/samples/openapi3/client/petstore/dart-dio/oneof_polymorphism_and_inheritance/lib/src/model/fruit.dart +++ b/samples/openapi3/client/petstore/dart-dio/oneof_polymorphism_and_inheritance/lib/src/model/fruit.dart @@ -68,6 +68,8 @@ extension FruitBuilderDiscriminatorExt on FruitBuilder { } } + + class _$FruitSerializer implements PrimitiveSerializer { @override final Iterable types = const [Fruit, _$Fruit]; diff --git a/samples/openapi3/client/petstore/dart-dio/oneof_polymorphism_and_inheritance/lib/src/model/pizza.dart b/samples/openapi3/client/petstore/dart-dio/oneof_polymorphism_and_inheritance/lib/src/model/pizza.dart index 238e2ae15924..7b9a0362743d 100644 --- a/samples/openapi3/client/petstore/dart-dio/oneof_polymorphism_and_inheritance/lib/src/model/pizza.dart +++ b/samples/openapi3/client/petstore/dart-dio/oneof_polymorphism_and_inheritance/lib/src/model/pizza.dart @@ -51,6 +51,8 @@ extension PizzaBuilderDiscriminatorExt on PizzaBuilder { } } + + class _$PizzaSerializer implements PrimitiveSerializer { @override final Iterable types = const [Pizza]; diff --git a/samples/openapi3/client/petstore/dart-dio/oneof_primitive/lib/src/api_util.dart b/samples/openapi3/client/petstore/dart-dio/oneof_primitive/lib/src/api_util.dart index ed3bb12f25b8..ba125b61525b 100644 --- a/samples/openapi3/client/petstore/dart-dio/oneof_primitive/lib/src/api_util.dart +++ b/samples/openapi3/client/petstore/dart-dio/oneof_primitive/lib/src/api_util.dart @@ -9,34 +9,21 @@ import 'package:built_collection/built_collection.dart'; import 'package:built_value/serializer.dart'; import 'package:dio/dio.dart'; -/// Format the given form parameter object into something that Dio can handle. -/// Returns primitive or String. -/// Returns List/Map if the value is BuildList/BuiltMap. -dynamic encodeFormParameter(Serializers serializers, dynamic value, FullType type) { - if (value == null) { - return ''; - } - if (value is String || value is num || value is bool) { - return value; - } - final serialized = serializers.serialize( - value as Object, - specifiedType: type, - ); - if (serialized is String) { - return serialized; - } - if (value is BuiltList || value is BuiltSet || value is BuiltMap) { - return serialized; - } - return json.encode(serialized); -} - -dynamic encodeQueryParameter( +/// Format normal parameters for query/form bodies. +/// Returns primitive, String, or `ListParam` for collections when `asString` +/// is false. +/// When `omitNullCollectionItems` is true, undefined collection members +/// are removed before query serialization. +dynamic encodeParameter( Serializers serializers, dynamic value, FullType type, -) { + { + ListFormat format = ListFormat.multi, + bool asString = false, + bool forMultipart = false, + bool omitNullCollectionItems = false, +}) { if (value == null) { return ''; } @@ -44,7 +31,6 @@ dynamic encodeQueryParameter( return value; } if (value is Uint8List) { - // Currently not sure how to serialize this return value; } final serialized = serializers.serialize( @@ -52,26 +38,133 @@ dynamic encodeQueryParameter( specifiedType: type, ); if (serialized == null) { - return ''; + return asString ? '' : null; + } + // Match the collection independently of T. Dart's generic type checks are + // reified, so BuiltList does not satisfy BuiltList. + if (value is BuiltList || value is BuiltSet) { + final values = List.of((serialized as Iterable).cast()); + if (omitNullCollectionItems) { + values.removeWhere((item) => item == null); + } + if (asString && format != ListFormat.multi && format != ListFormat.multiCompatible) { + return _joinCollectionValues(values, format); + } + if (forMultipart) { + return serialized; + } + return ListParam(values, format); } - if (serialized is String) { - return serialized; + // Model form fields are JSON text; explicit maps keep their form encoding. + if (asString && value is! BuiltMap && serialized is! String) { + return jsonEncode(serialized); } return serialized; } -ListParam encodeCollectionQueryParameter( +String encodePathParameter( Serializers serializers, dynamic value, FullType type, { ListFormat format = ListFormat.multi, }) { + if (value == null) { + return ''; + } final serialized = serializers.serialize( value as Object, specifiedType: type, ); - if (value is BuiltList || value is BuiltSet) { - return ListParam(List.of((serialized as Iterable).cast()), format); + if (serialized == null) { + return ''; + } + if (serialized is String || serialized is num || serialized is bool) { + return Uri.encodeComponent(serialized.toString()); } - throw ArgumentError('Invalid value passed to encodeCollectionQueryParameter'); + if (serialized is Iterable) { + final values = serialized.map((item) => _encodeBuiltValuePathValue(serializers, item, format)).toList(); + return _joinCollectionValues(values, format); + } + if (serialized is Map) { + final pairs = []; + serialized.forEach((k, v) { + final serializedKey = _encodeBuiltValuePathValue(serializers, k, format); + final serializedValue = _encodeBuiltValuePathValue(serializers, v, format); + pairs.add('$serializedKey,$serializedValue'); + }); + return pairs.join(','); + } + return Uri.encodeComponent(serialized.toString()); +} + +String _encodeBuiltValuePathValue( + Serializers serializers, + dynamic value, + ListFormat format, +) { + if (value == null || value is String || value is num || value is bool) { + return _encodePathValue(value, format); + } + if (value is Iterable) { + final values = value.map((item) => _encodeBuiltValuePathValue(serializers, item, format)).toList(); + return _joinCollectionValues(values, format); + } + if (value is Map) { + final pairs = []; + value.forEach((k, v) { + final serializedKey = _encodeBuiltValuePathValue(serializers, k, format); + final serializedValue = _encodeBuiltValuePathValue(serializers, v, format); + pairs.add('$serializedKey,$serializedValue'); + }); + return pairs.join(','); + } + final serialized = serializers.serialize(value as Object); + return _encodeBuiltValuePathValue(serializers, serialized, format); +} + +String _encodePathValue(dynamic value, ListFormat format) { + if (value == null) { + return ''; + } + if (value is String || value is num || value is bool || value is Enum) { + return Uri.encodeComponent(value.toString()); + } + if (value is List) { + final values = value.map((item) => _encodePathValue(item, format)).toList(); + return _joinCollectionValues(values, format); + } + if (value is Map) { + final pairs = []; + value.forEach((k, v) { + final serializedKey = _encodePathValue(k, format); + final serializedValue = _encodePathValue(v, format); + pairs.add('$serializedKey,$serializedValue'); + }); + return pairs.join(','); + } + return Uri.encodeComponent(value.toString()); +} + +String _joinCollectionValues(List values, ListFormat format) { + switch (format) { + case ListFormat.csv: + return values.join(','); + case ListFormat.ssv: + return values.join(' '); + case ListFormat.tsv: + return values.join('\t'); + case ListFormat.pipes: + return values.join('|'); + case ListFormat.multi: + return values.join(','); + case ListFormat.multiCompatible: + return values.join(','); + } +} + +void removeNullParametersExcept( + Map parameters, + Set requiredParameters, +) { + parameters.removeWhere((key, value) => value == null && !requiredParameters.contains(key)); } diff --git a/samples/openapi3/client/petstore/dart-dio/petstore-timemachine/lib/src/api/pet_api.dart b/samples/openapi3/client/petstore/dart-dio/petstore-timemachine/lib/src/api/pet_api.dart index 6161c2f7eb2d..0451f59f5587 100644 --- a/samples/openapi3/client/petstore/dart-dio/petstore-timemachine/lib/src/api/pet_api.dart +++ b/samples/openapi3/client/petstore/dart-dio/petstore-timemachine/lib/src/api/pet_api.dart @@ -146,7 +146,7 @@ class PetApi { ProgressCallback? onSendProgress, ProgressCallback? onReceiveProgress, }) async { - final _path = r'/pet/{petId}'.replaceAll('{' r'petId' '}', Uri.encodeComponent(encodeQueryParameter(_serializers, petId, const FullType(int)).toString()),); + final _path = r'/pet/{petId}'.replaceAll('{' r'petId' '}', encodePathParameter(_serializers, petId, const FullType(int)),); final _options = Options( method: r'DELETE', headers: { @@ -218,7 +218,7 @@ class PetApi { ); final _queryParameters = { - r'status': encodeCollectionQueryParameter(_serializers, status, const FullType(BuiltList, [FullType(String)]), format: ListFormat.csv,), + r'status': encodeParameter(_serializers, status, const FullType(BuiltList, [FullType(String)]), omitNullCollectionItems: true, format: ListFormat.csv), }; final _response = await _dio.request( @@ -304,7 +304,7 @@ class PetApi { ); final _queryParameters = { - r'tags': encodeCollectionQueryParameter(_serializers, tags, const FullType(BuiltList, [FullType(String)]), format: ListFormat.csv,), + r'tags': encodeParameter(_serializers, tags, const FullType(BuiltList, [FullType(String)]), omitNullCollectionItems: true, format: ListFormat.csv), }; final _response = await _dio.request( @@ -370,7 +370,7 @@ class PetApi { ProgressCallback? onSendProgress, ProgressCallback? onReceiveProgress, }) async { - final _path = r'/pet/{petId}'.replaceAll('{' r'petId' '}', Uri.encodeComponent(encodeQueryParameter(_serializers, petId, const FullType(int)).toString()),); + final _path = r'/pet/{petId}'.replaceAll('{' r'petId' '}', encodePathParameter(_serializers, petId, const FullType(int)),); final _options = Options( method: r'GET', headers: { @@ -558,7 +558,7 @@ class PetApi { ProgressCallback? onSendProgress, ProgressCallback? onReceiveProgress, }) async { - final _path = r'/pet/{petId}'.replaceAll('{' r'petId' '}', Uri.encodeComponent(encodeQueryParameter(_serializers, petId, const FullType(int)).toString()),); + final _path = r'/pet/{petId}'.replaceAll('{' r'petId' '}', encodePathParameter(_serializers, petId, const FullType(int)),); final _options = Options( method: r'POST', headers: { @@ -581,9 +581,15 @@ class PetApi { try { _bodyData = { - if (name != null) r'name': encodeQueryParameter(_serializers, name, const FullType(String)), - if (status != null) r'status': encodeQueryParameter(_serializers, status, const FullType(String)), + if (name != null) r'name': encodeParameter(_serializers, name, const FullType(String), asString: true), + if (status != null) r'status': encodeParameter(_serializers, status, const FullType(String), asString: true), }; + removeNullParametersExcept( + _bodyData, + { + + }, + ); } catch(error, stackTrace) { throw DioException( @@ -636,7 +642,7 @@ class PetApi { ProgressCallback? onSendProgress, ProgressCallback? onReceiveProgress, }) async { - final _path = r'/pet/{petId}/uploadImage'.replaceAll('{' r'petId' '}', Uri.encodeComponent(encodeQueryParameter(_serializers, petId, const FullType(int)).toString()),); + final _path = r'/pet/{petId}/uploadImage'.replaceAll('{' r'petId' '}', encodePathParameter(_serializers, petId, const FullType(int)),); final _options = Options( method: r'POST', headers: { @@ -659,7 +665,7 @@ class PetApi { try { _bodyData = FormData.fromMap({ - if (additionalMetadata != null) r'additionalMetadata': encodeFormParameter(_serializers, additionalMetadata, const FullType(String)), + if (additionalMetadata != null) r'additionalMetadata': encodeParameter(_serializers, additionalMetadata, const FullType(String), forMultipart: true), if (file != null) r'file': file, }); diff --git a/samples/openapi3/client/petstore/dart-dio/petstore-timemachine/lib/src/api/store_api.dart b/samples/openapi3/client/petstore/dart-dio/petstore-timemachine/lib/src/api/store_api.dart index ebd11aef63e3..a205898ebc9d 100644 --- a/samples/openapi3/client/petstore/dart-dio/petstore-timemachine/lib/src/api/store_api.dart +++ b/samples/openapi3/client/petstore/dart-dio/petstore-timemachine/lib/src/api/store_api.dart @@ -43,7 +43,7 @@ class StoreApi { ProgressCallback? onSendProgress, ProgressCallback? onReceiveProgress, }) async { - final _path = r'/store/order/{orderId}'.replaceAll('{' r'orderId' '}', Uri.encodeComponent(encodeQueryParameter(_serializers, orderId, const FullType(String)).toString()),); + final _path = r'/store/order/{orderId}'.replaceAll('{' r'orderId' '}', encodePathParameter(_serializers, orderId, const FullType(String)),); final _options = Options( method: r'DELETE', headers: { @@ -170,7 +170,7 @@ class StoreApi { ProgressCallback? onSendProgress, ProgressCallback? onReceiveProgress, }) async { - final _path = r'/store/order/{orderId}'.replaceAll('{' r'orderId' '}', Uri.encodeComponent(encodeQueryParameter(_serializers, orderId, const FullType(int)).toString()),); + final _path = r'/store/order/{orderId}'.replaceAll('{' r'orderId' '}', encodePathParameter(_serializers, orderId, const FullType(int)),); final _options = Options( method: r'GET', headers: { diff --git a/samples/openapi3/client/petstore/dart-dio/petstore-timemachine/lib/src/api/user_api.dart b/samples/openapi3/client/petstore/dart-dio/petstore-timemachine/lib/src/api/user_api.dart index 7555893e7eb5..6fbf67c7a83d 100644 --- a/samples/openapi3/client/petstore/dart-dio/petstore-timemachine/lib/src/api/user_api.dart +++ b/samples/openapi3/client/petstore/dart-dio/petstore-timemachine/lib/src/api/user_api.dart @@ -266,7 +266,7 @@ class UserApi { ProgressCallback? onSendProgress, ProgressCallback? onReceiveProgress, }) async { - final _path = r'/user/{username}'.replaceAll('{' r'username' '}', Uri.encodeComponent(encodeQueryParameter(_serializers, username, const FullType(String)).toString()),); + final _path = r'/user/{username}'.replaceAll('{' r'username' '}', encodePathParameter(_serializers, username, const FullType(String)),); final _options = Options( method: r'DELETE', headers: { @@ -320,7 +320,7 @@ class UserApi { ProgressCallback? onSendProgress, ProgressCallback? onReceiveProgress, }) async { - final _path = r'/user/{username}'.replaceAll('{' r'username' '}', Uri.encodeComponent(encodeQueryParameter(_serializers, username, const FullType(String)).toString()),); + final _path = r'/user/{username}'.replaceAll('{' r'username' '}', encodePathParameter(_serializers, username, const FullType(String)),); final _options = Options( method: r'GET', headers: { @@ -411,8 +411,8 @@ class UserApi { ); final _queryParameters = { - r'username': encodeQueryParameter(_serializers, username, const FullType(String)), - r'password': encodeQueryParameter(_serializers, password, const FullType(String)), + r'username': encodeParameter(_serializers, username, const FullType(String)), + r'password': encodeParameter(_serializers, password, const FullType(String)), }; final _response = await _dio.request( @@ -529,7 +529,7 @@ class UserApi { ProgressCallback? onSendProgress, ProgressCallback? onReceiveProgress, }) async { - final _path = r'/user/{username}'.replaceAll('{' r'username' '}', Uri.encodeComponent(encodeQueryParameter(_serializers, username, const FullType(String)).toString()),); + final _path = r'/user/{username}'.replaceAll('{' r'username' '}', encodePathParameter(_serializers, username, const FullType(String)),); final _options = Options( method: r'PUT', headers: { diff --git a/samples/openapi3/client/petstore/dart-dio/petstore-timemachine/lib/src/api_util.dart b/samples/openapi3/client/petstore/dart-dio/petstore-timemachine/lib/src/api_util.dart index ed3bb12f25b8..ba125b61525b 100644 --- a/samples/openapi3/client/petstore/dart-dio/petstore-timemachine/lib/src/api_util.dart +++ b/samples/openapi3/client/petstore/dart-dio/petstore-timemachine/lib/src/api_util.dart @@ -9,34 +9,21 @@ import 'package:built_collection/built_collection.dart'; import 'package:built_value/serializer.dart'; import 'package:dio/dio.dart'; -/// Format the given form parameter object into something that Dio can handle. -/// Returns primitive or String. -/// Returns List/Map if the value is BuildList/BuiltMap. -dynamic encodeFormParameter(Serializers serializers, dynamic value, FullType type) { - if (value == null) { - return ''; - } - if (value is String || value is num || value is bool) { - return value; - } - final serialized = serializers.serialize( - value as Object, - specifiedType: type, - ); - if (serialized is String) { - return serialized; - } - if (value is BuiltList || value is BuiltSet || value is BuiltMap) { - return serialized; - } - return json.encode(serialized); -} - -dynamic encodeQueryParameter( +/// Format normal parameters for query/form bodies. +/// Returns primitive, String, or `ListParam` for collections when `asString` +/// is false. +/// When `omitNullCollectionItems` is true, undefined collection members +/// are removed before query serialization. +dynamic encodeParameter( Serializers serializers, dynamic value, FullType type, -) { + { + ListFormat format = ListFormat.multi, + bool asString = false, + bool forMultipart = false, + bool omitNullCollectionItems = false, +}) { if (value == null) { return ''; } @@ -44,7 +31,6 @@ dynamic encodeQueryParameter( return value; } if (value is Uint8List) { - // Currently not sure how to serialize this return value; } final serialized = serializers.serialize( @@ -52,26 +38,133 @@ dynamic encodeQueryParameter( specifiedType: type, ); if (serialized == null) { - return ''; + return asString ? '' : null; + } + // Match the collection independently of T. Dart's generic type checks are + // reified, so BuiltList does not satisfy BuiltList. + if (value is BuiltList || value is BuiltSet) { + final values = List.of((serialized as Iterable).cast()); + if (omitNullCollectionItems) { + values.removeWhere((item) => item == null); + } + if (asString && format != ListFormat.multi && format != ListFormat.multiCompatible) { + return _joinCollectionValues(values, format); + } + if (forMultipart) { + return serialized; + } + return ListParam(values, format); } - if (serialized is String) { - return serialized; + // Model form fields are JSON text; explicit maps keep their form encoding. + if (asString && value is! BuiltMap && serialized is! String) { + return jsonEncode(serialized); } return serialized; } -ListParam encodeCollectionQueryParameter( +String encodePathParameter( Serializers serializers, dynamic value, FullType type, { ListFormat format = ListFormat.multi, }) { + if (value == null) { + return ''; + } final serialized = serializers.serialize( value as Object, specifiedType: type, ); - if (value is BuiltList || value is BuiltSet) { - return ListParam(List.of((serialized as Iterable).cast()), format); + if (serialized == null) { + return ''; + } + if (serialized is String || serialized is num || serialized is bool) { + return Uri.encodeComponent(serialized.toString()); } - throw ArgumentError('Invalid value passed to encodeCollectionQueryParameter'); + if (serialized is Iterable) { + final values = serialized.map((item) => _encodeBuiltValuePathValue(serializers, item, format)).toList(); + return _joinCollectionValues(values, format); + } + if (serialized is Map) { + final pairs = []; + serialized.forEach((k, v) { + final serializedKey = _encodeBuiltValuePathValue(serializers, k, format); + final serializedValue = _encodeBuiltValuePathValue(serializers, v, format); + pairs.add('$serializedKey,$serializedValue'); + }); + return pairs.join(','); + } + return Uri.encodeComponent(serialized.toString()); +} + +String _encodeBuiltValuePathValue( + Serializers serializers, + dynamic value, + ListFormat format, +) { + if (value == null || value is String || value is num || value is bool) { + return _encodePathValue(value, format); + } + if (value is Iterable) { + final values = value.map((item) => _encodeBuiltValuePathValue(serializers, item, format)).toList(); + return _joinCollectionValues(values, format); + } + if (value is Map) { + final pairs = []; + value.forEach((k, v) { + final serializedKey = _encodeBuiltValuePathValue(serializers, k, format); + final serializedValue = _encodeBuiltValuePathValue(serializers, v, format); + pairs.add('$serializedKey,$serializedValue'); + }); + return pairs.join(','); + } + final serialized = serializers.serialize(value as Object); + return _encodeBuiltValuePathValue(serializers, serialized, format); +} + +String _encodePathValue(dynamic value, ListFormat format) { + if (value == null) { + return ''; + } + if (value is String || value is num || value is bool || value is Enum) { + return Uri.encodeComponent(value.toString()); + } + if (value is List) { + final values = value.map((item) => _encodePathValue(item, format)).toList(); + return _joinCollectionValues(values, format); + } + if (value is Map) { + final pairs = []; + value.forEach((k, v) { + final serializedKey = _encodePathValue(k, format); + final serializedValue = _encodePathValue(v, format); + pairs.add('$serializedKey,$serializedValue'); + }); + return pairs.join(','); + } + return Uri.encodeComponent(value.toString()); +} + +String _joinCollectionValues(List values, ListFormat format) { + switch (format) { + case ListFormat.csv: + return values.join(','); + case ListFormat.ssv: + return values.join(' '); + case ListFormat.tsv: + return values.join('\t'); + case ListFormat.pipes: + return values.join('|'); + case ListFormat.multi: + return values.join(','); + case ListFormat.multiCompatible: + return values.join(','); + } +} + +void removeNullParametersExcept( + Map parameters, + Set requiredParameters, +) { + parameters.removeWhere((key, value) => value == null && !requiredParameters.contains(key)); } diff --git a/samples/openapi3/client/petstore/dart-dio/petstore_client_lib_fake-json_serializable/.openapi-generator/FILES b/samples/openapi3/client/petstore/dart-dio/petstore_client_lib_fake-json_serializable/.openapi-generator/FILES index 6c5156b0df8d..0199ee1c28f4 100644 --- a/samples/openapi3/client/petstore/dart-dio/petstore_client_lib_fake-json_serializable/.openapi-generator/FILES +++ b/samples/openapi3/client/petstore/dart-dio/petstore_client_lib_fake-json_serializable/.openapi-generator/FILES @@ -75,6 +75,7 @@ lib/src/api/fake_classname_tags123_api.dart lib/src/api/pet_api.dart lib/src/api/store_api.dart lib/src/api/user_api.dart +lib/src/api_util.dart lib/src/auth/api_key_auth.dart lib/src/auth/auth.dart lib/src/auth/basic_auth.dart diff --git a/samples/openapi3/client/petstore/dart-dio/petstore_client_lib_fake-json_serializable/lib/src/api/fake_api.dart b/samples/openapi3/client/petstore/dart-dio/petstore_client_lib_fake-json_serializable/lib/src/api/fake_api.dart index 370401906dcd..7eddf9a83797 100644 --- a/samples/openapi3/client/petstore/dart-dio/petstore_client_lib_fake-json_serializable/lib/src/api/fake_api.dart +++ b/samples/openapi3/client/petstore/dart-dio/petstore_client_lib_fake-json_serializable/lib/src/api/fake_api.dart @@ -9,6 +9,7 @@ import 'dart:convert'; import 'package:openapi/src/deserialize.dart'; import 'package:dio/dio.dart'; +import 'package:openapi/src/api_util.dart'; import 'package:openapi/src/model/child_with_nullable.dart'; import 'package:openapi/src/model/fake_big_decimal_map200_response.dart'; import 'package:openapi/src/model/file_schema_test_class.dart'; @@ -1227,6 +1228,12 @@ _responseData = rawData == null ? null : deserialize(r if (password != null) r'password': password, if (callback != null) r'callback': callback, }; + removeNullParametersExcept( + _bodyData, + { + + }, + ); } catch(error, stackTrace) { throw DioException( @@ -1322,6 +1329,12 @@ _responseData = rawData == null ? null : deserialize(r if (enumFormStringArray != null) r'enum_form_string_array': enumFormStringArray, if (enumFormString != null) r'enum_form_string': enumFormString, }; + removeNullParametersExcept( + _bodyData, + { + + }, + ); } catch(error, stackTrace) { throw DioException( @@ -1600,6 +1613,12 @@ _responseData = rawData == null ? null : deserialize(r r'param': param, r'param2': param2, }; + removeNullParametersExcept( + _bodyData, + { + + }, + ); } catch(error, stackTrace) { throw DioException( diff --git a/samples/openapi3/client/petstore/dart-dio/petstore_client_lib_fake-json_serializable/lib/src/api/pet_api.dart b/samples/openapi3/client/petstore/dart-dio/petstore_client_lib_fake-json_serializable/lib/src/api/pet_api.dart index 56b029ed8ca0..40a20e1b4405 100644 --- a/samples/openapi3/client/petstore/dart-dio/petstore_client_lib_fake-json_serializable/lib/src/api/pet_api.dart +++ b/samples/openapi3/client/petstore/dart-dio/petstore_client_lib_fake-json_serializable/lib/src/api/pet_api.dart @@ -9,6 +9,7 @@ import 'dart:convert'; import 'package:openapi/src/deserialize.dart'; import 'package:dio/dio.dart'; +import 'package:openapi/src/api_util.dart'; import 'package:openapi/src/model/api_response.dart'; import 'package:openapi/src/model/pet.dart'; @@ -114,7 +115,7 @@ class PetApi { ProgressCallback? onSendProgress, ProgressCallback? onReceiveProgress, }) async { - final _path = r'/pet/{petId}'.replaceAll('{' r'petId' '}', Uri.encodeComponent(petId.toString()),); + final _path = r'/pet/{petId}'.replaceAll('{' r'petId' '}', encodePathParameter(petId),); final _options = Options( method: r'DELETE', headers: { @@ -332,7 +333,7 @@ _responseData = rawData == null ? null : deserialize, Pet>(rawData, 'Se ProgressCallback? onSendProgress, ProgressCallback? onReceiveProgress, }) async { - final _path = r'/pet/{petId}'.replaceAll('{' r'petId' '}', Uri.encodeComponent(petId.toString()),); + final _path = r'/pet/{petId}'.replaceAll('{' r'petId' '}', encodePathParameter(petId),); final _options = Options( method: r'GET', headers: { @@ -486,7 +487,7 @@ _responseData = rawData == null ? null : deserialize(rawData, 'Pet', g ProgressCallback? onSendProgress, ProgressCallback? onReceiveProgress, }) async { - final _path = r'/pet/{petId}'.replaceAll('{' r'petId' '}', Uri.encodeComponent(petId.toString()),); + final _path = r'/pet/{petId}'.replaceAll('{' r'petId' '}', encodePathParameter(petId),); final _options = Options( method: r'POST', headers: { @@ -512,6 +513,12 @@ _responseData = rawData == null ? null : deserialize(rawData, 'Pet', g if (name != null) r'name': name, if (status != null) r'status': status, }; + removeNullParametersExcept( + _bodyData, + { + + }, + ); } catch(error, stackTrace) { throw DioException( @@ -564,7 +571,7 @@ _responseData = rawData == null ? null : deserialize(rawData, 'Pet', g ProgressCallback? onSendProgress, ProgressCallback? onReceiveProgress, }) async { - final _path = r'/pet/{petId}/uploadImage'.replaceAll('{' r'petId' '}', Uri.encodeComponent(petId.toString()),); + final _path = r'/pet/{petId}/uploadImage'.replaceAll('{' r'petId' '}', encodePathParameter(petId),); final _options = Options( method: r'POST', headers: { @@ -667,7 +674,7 @@ _responseData = rawData == null ? null : deserialize(r ProgressCallback? onSendProgress, ProgressCallback? onReceiveProgress, }) async { - final _path = r'/fake/{petId}/uploadImageWithRequiredFile'.replaceAll('{' r'petId' '}', Uri.encodeComponent(petId.toString()),); + final _path = r'/fake/{petId}/uploadImageWithRequiredFile'.replaceAll('{' r'petId' '}', encodePathParameter(petId),); final _options = Options( method: r'POST', headers: { diff --git a/samples/openapi3/client/petstore/dart-dio/petstore_client_lib_fake-json_serializable/lib/src/api/store_api.dart b/samples/openapi3/client/petstore/dart-dio/petstore_client_lib_fake-json_serializable/lib/src/api/store_api.dart index 4ffc14056b5a..0cb7e174409a 100644 --- a/samples/openapi3/client/petstore/dart-dio/petstore_client_lib_fake-json_serializable/lib/src/api/store_api.dart +++ b/samples/openapi3/client/petstore/dart-dio/petstore_client_lib_fake-json_serializable/lib/src/api/store_api.dart @@ -9,6 +9,7 @@ import 'dart:convert'; import 'package:openapi/src/deserialize.dart'; import 'package:dio/dio.dart'; +import 'package:openapi/src/api_util.dart'; import 'package:openapi/src/model/order.dart'; class StoreApi { @@ -40,7 +41,7 @@ class StoreApi { ProgressCallback? onSendProgress, ProgressCallback? onReceiveProgress, }) async { - final _path = r'/store/order/{order_id}'.replaceAll('{' r'order_id' '}', Uri.encodeComponent(orderId.toString()),); + final _path = r'/store/order/{order_id}'.replaceAll('{' r'order_id' '}', encodePathParameter(orderId),); final _options = Options( method: r'DELETE', headers: { @@ -164,7 +165,7 @@ _responseData = rawData == null ? null : deserialize, int>(rawD ProgressCallback? onSendProgress, ProgressCallback? onReceiveProgress, }) async { - final _path = r'/store/order/{order_id}'.replaceAll('{' r'order_id' '}', Uri.encodeComponent(orderId.toString()),); + final _path = r'/store/order/{order_id}'.replaceAll('{' r'order_id' '}', encodePathParameter(orderId),); final _options = Options( method: r'GET', headers: { diff --git a/samples/openapi3/client/petstore/dart-dio/petstore_client_lib_fake-json_serializable/lib/src/api/user_api.dart b/samples/openapi3/client/petstore/dart-dio/petstore_client_lib_fake-json_serializable/lib/src/api/user_api.dart index 166786eed585..fc3608a02c47 100644 --- a/samples/openapi3/client/petstore/dart-dio/petstore_client_lib_fake-json_serializable/lib/src/api/user_api.dart +++ b/samples/openapi3/client/petstore/dart-dio/petstore_client_lib_fake-json_serializable/lib/src/api/user_api.dart @@ -9,6 +9,7 @@ import 'dart:convert'; import 'package:openapi/src/deserialize.dart'; import 'package:dio/dio.dart'; +import 'package:openapi/src/api_util.dart'; import 'package:openapi/src/model/user.dart'; class UserApi { @@ -238,7 +239,7 @@ class UserApi { ProgressCallback? onSendProgress, ProgressCallback? onReceiveProgress, }) async { - final _path = r'/user/{username}'.replaceAll('{' r'username' '}', Uri.encodeComponent(username.toString()),); + final _path = r'/user/{username}'.replaceAll('{' r'username' '}', encodePathParameter(username),); final _options = Options( method: r'DELETE', headers: { @@ -285,7 +286,7 @@ class UserApi { ProgressCallback? onSendProgress, ProgressCallback? onReceiveProgress, }) async { - final _path = r'/user/{username}'.replaceAll('{' r'username' '}', Uri.encodeComponent(username.toString()),); + final _path = r'/user/{username}'.replaceAll('{' r'username' '}', encodePathParameter(username),); final _options = Options( method: r'GET', headers: { @@ -484,7 +485,7 @@ _responseData = rawData == null ? null : deserialize(rawData, 'S ProgressCallback? onSendProgress, ProgressCallback? onReceiveProgress, }) async { - final _path = r'/user/{username}'.replaceAll('{' r'username' '}', Uri.encodeComponent(username.toString()),); + final _path = r'/user/{username}'.replaceAll('{' r'username' '}', encodePathParameter(username),); final _options = Options( method: r'PUT', headers: { diff --git a/samples/openapi3/client/petstore/dart-dio/petstore_client_lib_fake-json_serializable/lib/src/api_util.dart b/samples/openapi3/client/petstore/dart-dio/petstore_client_lib_fake-json_serializable/lib/src/api_util.dart new file mode 100644 index 000000000000..f903c6a9b06d --- /dev/null +++ b/samples/openapi3/client/petstore/dart-dio/petstore_client_lib_fake-json_serializable/lib/src/api_util.dart @@ -0,0 +1,62 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// + +import 'package:dio/dio.dart'; + +String encodePathParameter( + dynamic value, { + ListFormat format = ListFormat.multi, +}) { + if (value == null) { + return ''; + } + return _encodePathValue(value, format); +} + +String _encodePathValue(dynamic value, ListFormat format) { + if (value == null) { + return ''; + } + if (value is String || value is num || value is bool || value is Enum) { + return Uri.encodeComponent(value.toString()); + } + if (value is List) { + final values = value.map((item) => _encodePathValue(item, format)).toList(); + return _joinCollectionValues(values, format); + } + if (value is Map) { + final pairs = []; + value.forEach((k, v) { + final serializedKey = _encodePathValue(k, format); + final serializedValue = _encodePathValue(v, format); + pairs.add('$serializedKey,$serializedValue'); + }); + return pairs.join(','); + } + return Uri.encodeComponent(value.toString()); +} + +String _joinCollectionValues(List values, ListFormat format) { + switch (format) { + case ListFormat.csv: + return values.join(','); + case ListFormat.ssv: + return values.join(' '); + case ListFormat.tsv: + return values.join('\t'); + case ListFormat.pipes: + return values.join('|'); + case ListFormat.multi: + return values.join(','); + case ListFormat.multiCompatible: + return values.join(','); + } +} + +void removeNullParametersExcept( + Map parameters, + Set requiredParameters, +) { + parameters.removeWhere((key, value) => value == null && !requiredParameters.contains(key)); +} diff --git a/samples/openapi3/client/petstore/dart-dio/petstore_client_lib_fake-json_serializable/test/path_parameter_encoding_test.dart b/samples/openapi3/client/petstore/dart-dio/petstore_client_lib_fake-json_serializable/test/path_parameter_encoding_test.dart new file mode 100644 index 000000000000..e2d512b81e6d --- /dev/null +++ b/samples/openapi3/client/petstore/dart-dio/petstore_client_lib_fake-json_serializable/test/path_parameter_encoding_test.dart @@ -0,0 +1,26 @@ +import 'package:dio/dio.dart'; +import 'package:openapi/src/api_util.dart'; +import 'package:openapi/src/model/outer_enum.dart'; +import 'package:test/test.dart'; + +void main() { + group('path parameter enum encoding', () { + test('uses the wire value when it differs from the Dart enum name', () { + const value = OuterEnum.unknownDefaultOpenApi; + + expect(value.name, 'unknownDefaultOpenApi'); + expect(value.toString(), 'unknown_default_open_api'); + expect(encodePathParameter(value), 'unknown_default_open_api'); + }); + + test('uses wire values for enum collection elements', () { + expect( + encodePathParameter(const [ + OuterEnum.unknownDefaultOpenApi, + OuterEnum.placed, + ], format: ListFormat.csv), + 'unknown_default_open_api,placed', + ); + }); + }); +} diff --git a/samples/openapi3/client/petstore/dart-dio/petstore_client_lib_fake/lib/src/api/fake_api.dart b/samples/openapi3/client/petstore/dart-dio/petstore_client_lib_fake/lib/src/api/fake_api.dart index 2c26b06536d9..1f0c73e9fff8 100644 --- a/samples/openapi3/client/petstore/dart-dio/petstore_client_lib_fake/lib/src/api/fake_api.dart +++ b/samples/openapi3/client/petstore/dart-dio/petstore_client_lib_fake/lib/src/api/fake_api.dart @@ -302,7 +302,7 @@ class FakeApi { ); final _queryParameters = { - if (query1 != null) r'query_1': encodeQueryParameter(_serializers, query1, const FullType(String)), + if (query1 != null) r'query_1': encodeParameter(_serializers, query1, const FullType(String)), }; dynamic _bodyData; @@ -1040,7 +1040,7 @@ class FakeApi { ); final _queryParameters = { - r'query': encodeQueryParameter(_serializers, query, const FullType(String)), + r'query': encodeParameter(_serializers, query, const FullType(String)), }; dynamic _bodyData; @@ -1243,21 +1243,27 @@ class FakeApi { try { _bodyData = { - if (integer != null) r'integer': encodeQueryParameter(_serializers, integer, const FullType(int)), - if (int32 != null) r'int32': encodeQueryParameter(_serializers, int32, const FullType(int)), - if (int64 != null) r'int64': encodeQueryParameter(_serializers, int64, const FullType(int)), - r'number': encodeQueryParameter(_serializers, number, const FullType(num)), - if (float != null) r'float': encodeQueryParameter(_serializers, float, const FullType(double)), - r'double': encodeQueryParameter(_serializers, double_, const FullType(double)), - if (string != null) r'string': encodeQueryParameter(_serializers, string, const FullType(String)), - r'pattern_without_delimiter': encodeQueryParameter(_serializers, patternWithoutDelimiter, const FullType(String)), - r'byte': encodeQueryParameter(_serializers, byte, const FullType(String)), - if (binary != null) r'binary': encodeQueryParameter(_serializers, binary, const FullType(Uint8List)), - if (date != null) r'date': encodeQueryParameter(_serializers, date, const FullType(Date)), - if (dateTime != null) r'dateTime': encodeQueryParameter(_serializers, dateTime, const FullType(DateTime)), - if (password != null) r'password': encodeQueryParameter(_serializers, password, const FullType(String)), - if (callback != null) r'callback': encodeQueryParameter(_serializers, callback, const FullType(String)), + if (integer != null) r'integer': encodeParameter(_serializers, integer, const FullType(int), asString: true), + if (int32 != null) r'int32': encodeParameter(_serializers, int32, const FullType(int), asString: true), + if (int64 != null) r'int64': encodeParameter(_serializers, int64, const FullType(int), asString: true), + r'number': encodeParameter(_serializers, number, const FullType(num), asString: true), + if (float != null) r'float': encodeParameter(_serializers, float, const FullType(double), asString: true), + r'double': encodeParameter(_serializers, double_, const FullType(double), asString: true), + if (string != null) r'string': encodeParameter(_serializers, string, const FullType(String), asString: true), + r'pattern_without_delimiter': encodeParameter(_serializers, patternWithoutDelimiter, const FullType(String), asString: true), + r'byte': encodeParameter(_serializers, byte, const FullType(String), asString: true), + if (binary != null) r'binary': encodeParameter(_serializers, binary, const FullType(Uint8List), asString: true), + if (date != null) r'date': encodeParameter(_serializers, date, const FullType(Date), asString: true), + if (dateTime != null) r'dateTime': encodeParameter(_serializers, dateTime, const FullType(DateTime), asString: true), + if (password != null) r'password': encodeParameter(_serializers, password, const FullType(String), asString: true), + if (callback != null) r'callback': encodeParameter(_serializers, callback, const FullType(String), asString: true), }; + removeNullParametersExcept( + _bodyData, + { + + }, + ); } catch(error, stackTrace) { throw DioException( @@ -1339,20 +1345,26 @@ class FakeApi { ); final _queryParameters = { - if (enumQueryStringArray != null) r'enum_query_string_array': encodeCollectionQueryParameter(_serializers, enumQueryStringArray, const FullType(BuiltList, [FullType(String)]), format: ListFormat.multi,), - if (enumQueryString != null) r'enum_query_string': encodeQueryParameter(_serializers, enumQueryString, const FullType(String)), - if (enumQueryInteger != null) r'enum_query_integer': encodeQueryParameter(_serializers, enumQueryInteger, const FullType(int)), - if (enumQueryDouble != null) r'enum_query_double': encodeQueryParameter(_serializers, enumQueryDouble, const FullType(double)), - if (enumQueryModelArray != null) r'enum_query_model_array': encodeCollectionQueryParameter(_serializers, enumQueryModelArray, const FullType(BuiltList, [FullType(ModelEnumClass)]), format: ListFormat.multi,), + if (enumQueryStringArray != null) r'enum_query_string_array': encodeParameter(_serializers, enumQueryStringArray, const FullType(BuiltList, [FullType(String)]), omitNullCollectionItems: true, format: ListFormat.multi), + if (enumQueryString != null) r'enum_query_string': encodeParameter(_serializers, enumQueryString, const FullType(String)), + if (enumQueryInteger != null) r'enum_query_integer': encodeParameter(_serializers, enumQueryInteger, const FullType(int)), + if (enumQueryDouble != null) r'enum_query_double': encodeParameter(_serializers, enumQueryDouble, const FullType(double)), + if (enumQueryModelArray != null) r'enum_query_model_array': encodeParameter(_serializers, enumQueryModelArray, const FullType(BuiltList, [FullType(ModelEnumClass)]), omitNullCollectionItems: true, format: ListFormat.multi), }; dynamic _bodyData; try { _bodyData = { - if (enumFormStringArray != null) r'enum_form_string_array': encodeCollectionQueryParameter(_serializers, enumFormStringArray, const FullType(BuiltList, [FullType(String)]), format: ListFormat.csv,), - if (enumFormString != null) r'enum_form_string': encodeQueryParameter(_serializers, enumFormString, const FullType(String)), + if (enumFormStringArray != null) r'enum_form_string_array': encodeParameter(_serializers, enumFormStringArray, const FullType(BuiltList, [FullType(String)]), asString: true, format: ListFormat.csv), + if (enumFormString != null) r'enum_form_string': encodeParameter(_serializers, enumFormString, const FullType(String), asString: true), }; + removeNullParametersExcept( + _bodyData, + { + + }, + ); } catch(error, stackTrace) { throw DioException( @@ -1435,10 +1447,10 @@ class FakeApi { ); final _queryParameters = { - r'required_string_group': encodeQueryParameter(_serializers, requiredStringGroup, const FullType(int)), - r'required_int64_group': encodeQueryParameter(_serializers, requiredInt64Group, const FullType(int)), - if (stringGroup != null) r'string_group': encodeQueryParameter(_serializers, stringGroup, const FullType(int)), - if (int64Group != null) r'int64_group': encodeQueryParameter(_serializers, int64Group, const FullType(int)), + r'required_string_group': encodeParameter(_serializers, requiredStringGroup, const FullType(int)), + r'required_int64_group': encodeParameter(_serializers, requiredInt64Group, const FullType(int)), + if (stringGroup != null) r'string_group': encodeParameter(_serializers, stringGroup, const FullType(int)), + if (int64Group != null) r'int64_group': encodeParameter(_serializers, int64Group, const FullType(int)), }; final _response = await _dio.request( @@ -1630,9 +1642,15 @@ class FakeApi { try { _bodyData = { - r'param': encodeQueryParameter(_serializers, param, const FullType(String)), - r'param2': encodeQueryParameter(_serializers, param2, const FullType(String)), + r'param': encodeParameter(_serializers, param, const FullType(String), asString: true), + r'param2': encodeParameter(_serializers, param2, const FullType(String), asString: true), }; + removeNullParametersExcept( + _bodyData, + { + + }, + ); } catch(error, stackTrace) { throw DioException( @@ -1774,13 +1792,13 @@ class FakeApi { ); final _queryParameters = { - r'pipe': encodeCollectionQueryParameter(_serializers, pipe, const FullType(BuiltList, [FullType(String)]), format: ListFormat.pipes,), - r'ioutil': encodeCollectionQueryParameter(_serializers, ioutil, const FullType(BuiltList, [FullType(String)]), format: ListFormat.csv,), - r'http': encodeCollectionQueryParameter(_serializers, http, const FullType(BuiltList, [FullType(String)]), format: ListFormat.ssv,), - r'url': encodeCollectionQueryParameter(_serializers, url, const FullType(BuiltList, [FullType(String)]), format: ListFormat.csv,), - r'context': encodeCollectionQueryParameter(_serializers, context, const FullType(BuiltList, [FullType(String)]), format: ListFormat.multi,), - if (language != null) r'language': encodeQueryParameter(_serializers, language, const FullType(BuiltMap, [FullType(String), FullType(String)]), ), - r'allowEmpty': encodeQueryParameter(_serializers, allowEmpty, const FullType(String)), + r'pipe': encodeParameter(_serializers, pipe, const FullType(BuiltList, [FullType(String)]), omitNullCollectionItems: true, format: ListFormat.pipes), + r'ioutil': encodeParameter(_serializers, ioutil, const FullType(BuiltList, [FullType(String)]), omitNullCollectionItems: true, format: ListFormat.csv), + r'http': encodeParameter(_serializers, http, const FullType(BuiltList, [FullType(String)]), omitNullCollectionItems: true, format: ListFormat.ssv), + r'url': encodeParameter(_serializers, url, const FullType(BuiltList, [FullType(String)]), omitNullCollectionItems: true, format: ListFormat.csv), + r'context': encodeParameter(_serializers, context, const FullType(BuiltList, [FullType(String)]), omitNullCollectionItems: true, format: ListFormat.multi), + if (language != null) r'language': encodeParameter(_serializers, language, const FullType(BuiltMap, [FullType(String), FullType(String)])), + r'allowEmpty': encodeParameter(_serializers, allowEmpty, const FullType(String)), }; final _response = await _dio.request( diff --git a/samples/openapi3/client/petstore/dart-dio/petstore_client_lib_fake/lib/src/api/pet_api.dart b/samples/openapi3/client/petstore/dart-dio/petstore_client_lib_fake/lib/src/api/pet_api.dart index 379f664abef8..91e20f040631 100644 --- a/samples/openapi3/client/petstore/dart-dio/petstore_client_lib_fake/lib/src/api/pet_api.dart +++ b/samples/openapi3/client/petstore/dart-dio/petstore_client_lib_fake/lib/src/api/pet_api.dart @@ -118,7 +118,7 @@ class PetApi { ProgressCallback? onSendProgress, ProgressCallback? onReceiveProgress, }) async { - final _path = r'/pet/{petId}'.replaceAll('{' r'petId' '}', Uri.encodeComponent(encodeQueryParameter(_serializers, petId, const FullType(int)).toString()),); + final _path = r'/pet/{petId}'.replaceAll('{' r'petId' '}', encodePathParameter(_serializers, petId, const FullType(int)),); final _options = Options( method: r'DELETE', headers: { @@ -190,7 +190,7 @@ class PetApi { ); final _queryParameters = { - r'status': encodeCollectionQueryParameter(_serializers, status, const FullType(BuiltList, [FullType(String)]), format: ListFormat.csv,), + r'status': encodeParameter(_serializers, status, const FullType(BuiltList, [FullType(String)]), omitNullCollectionItems: true, format: ListFormat.csv), }; final _response = await _dio.request( @@ -276,7 +276,7 @@ class PetApi { ); final _queryParameters = { - r'tags': encodeCollectionQueryParameter(_serializers, tags, const FullType(BuiltSet, [FullType(String)]), format: ListFormat.csv,), + r'tags': encodeParameter(_serializers, tags, const FullType(BuiltSet, [FullType(String)]), omitNullCollectionItems: true, format: ListFormat.csv), }; final _response = await _dio.request( @@ -342,7 +342,7 @@ class PetApi { ProgressCallback? onSendProgress, ProgressCallback? onReceiveProgress, }) async { - final _path = r'/pet/{petId}'.replaceAll('{' r'petId' '}', Uri.encodeComponent(encodeQueryParameter(_serializers, petId, const FullType(int)).toString()),); + final _path = r'/pet/{petId}'.replaceAll('{' r'petId' '}', encodePathParameter(_serializers, petId, const FullType(int)),); final _options = Options( method: r'GET', headers: { @@ -500,7 +500,7 @@ class PetApi { ProgressCallback? onSendProgress, ProgressCallback? onReceiveProgress, }) async { - final _path = r'/pet/{petId}'.replaceAll('{' r'petId' '}', Uri.encodeComponent(encodeQueryParameter(_serializers, petId, const FullType(int)).toString()),); + final _path = r'/pet/{petId}'.replaceAll('{' r'petId' '}', encodePathParameter(_serializers, petId, const FullType(int)),); final _options = Options( method: r'POST', headers: { @@ -523,9 +523,15 @@ class PetApi { try { _bodyData = { - if (name != null) r'name': encodeQueryParameter(_serializers, name, const FullType(String)), - if (status != null) r'status': encodeQueryParameter(_serializers, status, const FullType(String)), + if (name != null) r'name': encodeParameter(_serializers, name, const FullType(String), asString: true), + if (status != null) r'status': encodeParameter(_serializers, status, const FullType(String), asString: true), }; + removeNullParametersExcept( + _bodyData, + { + + }, + ); } catch(error, stackTrace) { throw DioException( @@ -578,7 +584,7 @@ class PetApi { ProgressCallback? onSendProgress, ProgressCallback? onReceiveProgress, }) async { - final _path = r'/pet/{petId}/uploadImage'.replaceAll('{' r'petId' '}', Uri.encodeComponent(encodeQueryParameter(_serializers, petId, const FullType(int)).toString()),); + final _path = r'/pet/{petId}/uploadImage'.replaceAll('{' r'petId' '}', encodePathParameter(_serializers, petId, const FullType(int)),); final _options = Options( method: r'POST', headers: { @@ -601,7 +607,7 @@ class PetApi { try { _bodyData = FormData.fromMap({ - if (additionalMetadata != null) r'additionalMetadata': encodeFormParameter(_serializers, additionalMetadata, const FullType(String)), + if (additionalMetadata != null) r'additionalMetadata': encodeParameter(_serializers, additionalMetadata, const FullType(String), forMultipart: true), if (file != null) r'file': file, }); @@ -684,7 +690,7 @@ class PetApi { ProgressCallback? onSendProgress, ProgressCallback? onReceiveProgress, }) async { - final _path = r'/fake/{petId}/uploadImageWithRequiredFile'.replaceAll('{' r'petId' '}', Uri.encodeComponent(encodeQueryParameter(_serializers, petId, const FullType(int)).toString()),); + final _path = r'/fake/{petId}/uploadImageWithRequiredFile'.replaceAll('{' r'petId' '}', encodePathParameter(_serializers, petId, const FullType(int)),); final _options = Options( method: r'POST', headers: { @@ -707,7 +713,7 @@ class PetApi { try { _bodyData = FormData.fromMap({ - if (additionalMetadata != null) r'additionalMetadata': encodeFormParameter(_serializers, additionalMetadata, const FullType(String)), + if (additionalMetadata != null) r'additionalMetadata': encodeParameter(_serializers, additionalMetadata, const FullType(String), forMultipart: true), r'requiredFile': requiredFile, }); diff --git a/samples/openapi3/client/petstore/dart-dio/petstore_client_lib_fake/lib/src/api/store_api.dart b/samples/openapi3/client/petstore/dart-dio/petstore_client_lib_fake/lib/src/api/store_api.dart index 3c3e6ccb8f8d..03b3f7ca9513 100644 --- a/samples/openapi3/client/petstore/dart-dio/petstore_client_lib_fake/lib/src/api/store_api.dart +++ b/samples/openapi3/client/petstore/dart-dio/petstore_client_lib_fake/lib/src/api/store_api.dart @@ -43,7 +43,7 @@ class StoreApi { ProgressCallback? onSendProgress, ProgressCallback? onReceiveProgress, }) async { - final _path = r'/store/order/{order_id}'.replaceAll('{' r'order_id' '}', Uri.encodeComponent(encodeQueryParameter(_serializers, orderId, const FullType(String)).toString()),); + final _path = r'/store/order/{order_id}'.replaceAll('{' r'order_id' '}', encodePathParameter(_serializers, orderId, const FullType(String)),); final _options = Options( method: r'DELETE', headers: { @@ -170,7 +170,7 @@ class StoreApi { ProgressCallback? onSendProgress, ProgressCallback? onReceiveProgress, }) async { - final _path = r'/store/order/{order_id}'.replaceAll('{' r'order_id' '}', Uri.encodeComponent(encodeQueryParameter(_serializers, orderId, const FullType(int)).toString()),); + final _path = r'/store/order/{order_id}'.replaceAll('{' r'order_id' '}', encodePathParameter(_serializers, orderId, const FullType(int)),); final _options = Options( method: r'GET', headers: { diff --git a/samples/openapi3/client/petstore/dart-dio/petstore_client_lib_fake/lib/src/api/user_api.dart b/samples/openapi3/client/petstore/dart-dio/petstore_client_lib_fake/lib/src/api/user_api.dart index f7b22e1a2bf2..605eb1fb3e78 100644 --- a/samples/openapi3/client/petstore/dart-dio/petstore_client_lib_fake/lib/src/api/user_api.dart +++ b/samples/openapi3/client/petstore/dart-dio/petstore_client_lib_fake/lib/src/api/user_api.dart @@ -244,7 +244,7 @@ class UserApi { ProgressCallback? onSendProgress, ProgressCallback? onReceiveProgress, }) async { - final _path = r'/user/{username}'.replaceAll('{' r'username' '}', Uri.encodeComponent(encodeQueryParameter(_serializers, username, const FullType(String)).toString()),); + final _path = r'/user/{username}'.replaceAll('{' r'username' '}', encodePathParameter(_serializers, username, const FullType(String)),); final _options = Options( method: r'DELETE', headers: { @@ -291,7 +291,7 @@ class UserApi { ProgressCallback? onSendProgress, ProgressCallback? onReceiveProgress, }) async { - final _path = r'/user/{username}'.replaceAll('{' r'username' '}', Uri.encodeComponent(encodeQueryParameter(_serializers, username, const FullType(String)).toString()),); + final _path = r'/user/{username}'.replaceAll('{' r'username' '}', encodePathParameter(_serializers, username, const FullType(String)),); final _options = Options( method: r'GET', headers: { @@ -382,8 +382,8 @@ class UserApi { ); final _queryParameters = { - r'username': encodeQueryParameter(_serializers, username, const FullType(String)), - r'password': encodeQueryParameter(_serializers, password, const FullType(String)), + r'username': encodeParameter(_serializers, username, const FullType(String)), + r'password': encodeParameter(_serializers, password, const FullType(String)), }; final _response = await _dio.request( @@ -493,7 +493,7 @@ class UserApi { ProgressCallback? onSendProgress, ProgressCallback? onReceiveProgress, }) async { - final _path = r'/user/{username}'.replaceAll('{' r'username' '}', Uri.encodeComponent(encodeQueryParameter(_serializers, username, const FullType(String)).toString()),); + final _path = r'/user/{username}'.replaceAll('{' r'username' '}', encodePathParameter(_serializers, username, const FullType(String)),); final _options = Options( method: r'PUT', headers: { diff --git a/samples/openapi3/client/petstore/dart-dio/petstore_client_lib_fake/lib/src/api_util.dart b/samples/openapi3/client/petstore/dart-dio/petstore_client_lib_fake/lib/src/api_util.dart index ed3bb12f25b8..ba125b61525b 100644 --- a/samples/openapi3/client/petstore/dart-dio/petstore_client_lib_fake/lib/src/api_util.dart +++ b/samples/openapi3/client/petstore/dart-dio/petstore_client_lib_fake/lib/src/api_util.dart @@ -9,34 +9,21 @@ import 'package:built_collection/built_collection.dart'; import 'package:built_value/serializer.dart'; import 'package:dio/dio.dart'; -/// Format the given form parameter object into something that Dio can handle. -/// Returns primitive or String. -/// Returns List/Map if the value is BuildList/BuiltMap. -dynamic encodeFormParameter(Serializers serializers, dynamic value, FullType type) { - if (value == null) { - return ''; - } - if (value is String || value is num || value is bool) { - return value; - } - final serialized = serializers.serialize( - value as Object, - specifiedType: type, - ); - if (serialized is String) { - return serialized; - } - if (value is BuiltList || value is BuiltSet || value is BuiltMap) { - return serialized; - } - return json.encode(serialized); -} - -dynamic encodeQueryParameter( +/// Format normal parameters for query/form bodies. +/// Returns primitive, String, or `ListParam` for collections when `asString` +/// is false. +/// When `omitNullCollectionItems` is true, undefined collection members +/// are removed before query serialization. +dynamic encodeParameter( Serializers serializers, dynamic value, FullType type, -) { + { + ListFormat format = ListFormat.multi, + bool asString = false, + bool forMultipart = false, + bool omitNullCollectionItems = false, +}) { if (value == null) { return ''; } @@ -44,7 +31,6 @@ dynamic encodeQueryParameter( return value; } if (value is Uint8List) { - // Currently not sure how to serialize this return value; } final serialized = serializers.serialize( @@ -52,26 +38,133 @@ dynamic encodeQueryParameter( specifiedType: type, ); if (serialized == null) { - return ''; + return asString ? '' : null; + } + // Match the collection independently of T. Dart's generic type checks are + // reified, so BuiltList does not satisfy BuiltList. + if (value is BuiltList || value is BuiltSet) { + final values = List.of((serialized as Iterable).cast()); + if (omitNullCollectionItems) { + values.removeWhere((item) => item == null); + } + if (asString && format != ListFormat.multi && format != ListFormat.multiCompatible) { + return _joinCollectionValues(values, format); + } + if (forMultipart) { + return serialized; + } + return ListParam(values, format); } - if (serialized is String) { - return serialized; + // Model form fields are JSON text; explicit maps keep their form encoding. + if (asString && value is! BuiltMap && serialized is! String) { + return jsonEncode(serialized); } return serialized; } -ListParam encodeCollectionQueryParameter( +String encodePathParameter( Serializers serializers, dynamic value, FullType type, { ListFormat format = ListFormat.multi, }) { + if (value == null) { + return ''; + } final serialized = serializers.serialize( value as Object, specifiedType: type, ); - if (value is BuiltList || value is BuiltSet) { - return ListParam(List.of((serialized as Iterable).cast()), format); + if (serialized == null) { + return ''; + } + if (serialized is String || serialized is num || serialized is bool) { + return Uri.encodeComponent(serialized.toString()); } - throw ArgumentError('Invalid value passed to encodeCollectionQueryParameter'); + if (serialized is Iterable) { + final values = serialized.map((item) => _encodeBuiltValuePathValue(serializers, item, format)).toList(); + return _joinCollectionValues(values, format); + } + if (serialized is Map) { + final pairs = []; + serialized.forEach((k, v) { + final serializedKey = _encodeBuiltValuePathValue(serializers, k, format); + final serializedValue = _encodeBuiltValuePathValue(serializers, v, format); + pairs.add('$serializedKey,$serializedValue'); + }); + return pairs.join(','); + } + return Uri.encodeComponent(serialized.toString()); +} + +String _encodeBuiltValuePathValue( + Serializers serializers, + dynamic value, + ListFormat format, +) { + if (value == null || value is String || value is num || value is bool) { + return _encodePathValue(value, format); + } + if (value is Iterable) { + final values = value.map((item) => _encodeBuiltValuePathValue(serializers, item, format)).toList(); + return _joinCollectionValues(values, format); + } + if (value is Map) { + final pairs = []; + value.forEach((k, v) { + final serializedKey = _encodeBuiltValuePathValue(serializers, k, format); + final serializedValue = _encodeBuiltValuePathValue(serializers, v, format); + pairs.add('$serializedKey,$serializedValue'); + }); + return pairs.join(','); + } + final serialized = serializers.serialize(value as Object); + return _encodeBuiltValuePathValue(serializers, serialized, format); +} + +String _encodePathValue(dynamic value, ListFormat format) { + if (value == null) { + return ''; + } + if (value is String || value is num || value is bool || value is Enum) { + return Uri.encodeComponent(value.toString()); + } + if (value is List) { + final values = value.map((item) => _encodePathValue(item, format)).toList(); + return _joinCollectionValues(values, format); + } + if (value is Map) { + final pairs = []; + value.forEach((k, v) { + final serializedKey = _encodePathValue(k, format); + final serializedValue = _encodePathValue(v, format); + pairs.add('$serializedKey,$serializedValue'); + }); + return pairs.join(','); + } + return Uri.encodeComponent(value.toString()); +} + +String _joinCollectionValues(List values, ListFormat format) { + switch (format) { + case ListFormat.csv: + return values.join(','); + case ListFormat.ssv: + return values.join(' '); + case ListFormat.tsv: + return values.join('\t'); + case ListFormat.pipes: + return values.join('|'); + case ListFormat.multi: + return values.join(','); + case ListFormat.multiCompatible: + return values.join(','); + } +} + +void removeNullParametersExcept( + Map parameters, + Set requiredParameters, +) { + parameters.removeWhere((key, value) => value == null && !requiredParameters.contains(key)); } diff --git a/samples/openapi3/client/petstore/dart-dio/petstore_client_lib_fake/lib/src/model/animal.dart b/samples/openapi3/client/petstore/dart-dio/petstore_client_lib_fake/lib/src/model/animal.dart index 0eb3c9fe4fc2..d1c2dbe11b40 100644 --- a/samples/openapi3/client/petstore/dart-dio/petstore_client_lib_fake/lib/src/model/animal.dart +++ b/samples/openapi3/client/petstore/dart-dio/petstore_client_lib_fake/lib/src/model/animal.dart @@ -57,6 +57,8 @@ extension AnimalBuilderDiscriminatorExt on AnimalBuilder { } } + + class _$AnimalSerializer implements PrimitiveSerializer { @override final Iterable types = const [Animal]; diff --git a/samples/openapi3/client/petstore/dart-dio/petstore_client_lib_fake/lib/src/model/parent_with_nullable.dart b/samples/openapi3/client/petstore/dart-dio/petstore_client_lib_fake/lib/src/model/parent_with_nullable.dart index 92dd01f19752..373201d259aa 100644 --- a/samples/openapi3/client/petstore/dart-dio/petstore_client_lib_fake/lib/src/model/parent_with_nullable.dart +++ b/samples/openapi3/client/petstore/dart-dio/petstore_client_lib_fake/lib/src/model/parent_with_nullable.dart @@ -49,6 +49,8 @@ extension ParentWithNullableBuilderDiscriminatorExt on ParentWithNullableBuilder } } + + class _$ParentWithNullableSerializer implements PrimitiveSerializer { @override final Iterable types = const [ParentWithNullable]; diff --git a/samples/openapi3/client/petstore/dart-dio/petstore_client_lib_fake/lib/src/serializers.dart b/samples/openapi3/client/petstore/dart-dio/petstore_client_lib_fake/lib/src/serializers.dart index 33ce139b91df..35dc14bb5535 100644 --- a/samples/openapi3/client/petstore/dart-dio/petstore_client_lib_fake/lib/src/serializers.dart +++ b/samples/openapi3/client/petstore/dart-dio/petstore_client_lib_fake/lib/src/serializers.dart @@ -204,7 +204,7 @@ Serializers serializers = (_$serializers.toBuilder() ) ..addBuilderFactory( const FullType(BuiltList, [FullType.nullable(JsonObject)]), - () => ListBuilder(), + () => ListBuilder(), ) ..addBuilderFactory( const FullType(BuiltList, [FullType(Tag)]), diff --git a/samples/openapi3/client/petstore/dart-dio/petstore_client_lib_fake/test/model_request_encoding_test.dart b/samples/openapi3/client/petstore/dart-dio/petstore_client_lib_fake/test/model_request_encoding_test.dart new file mode 100644 index 000000000000..d48fc51b3526 --- /dev/null +++ b/samples/openapi3/client/petstore/dart-dio/petstore_client_lib_fake/test/model_request_encoding_test.dart @@ -0,0 +1,293 @@ +import 'dart:convert'; + +import 'package:built_collection/built_collection.dart'; +import 'package:built_value/serializer.dart'; +import 'package:dio/dio.dart'; +import 'package:openapi/src/api_util.dart'; +import 'package:openapi/src/model/category.dart'; +import 'package:openapi/src/serializers.dart'; +import 'package:test/test.dart'; + +void main() { + group('model request encoding', () { + final model = Category((b) => b..name = 'Milo & friends'); + const modelType = FullType(Category); + + Future transform(Object? pet, String contentType) { + return SyncTransformer().transformRequest(RequestOptions( + path: '/pets', + method: 'POST', + contentType: contentType, + data: {'pet': pet}, + )); + } + + test('JSON request preserves the model as an object and round-trips', + () async { + // Matches the generated JSON-body branch: serialize first, then let + // Dio encode the complete body once. + final serialized = standardSerializers.serialize( + model, + specifiedType: modelType, + ); + final body = await transform(serialized, Headers.jsonContentType); + final decoded = jsonDecode(body) as Map; + + expect(decoded['pet'], isA>()); + expect(decoded['pet'], {'name': 'Milo & friends'}); + expect( + standardSerializers.deserialize(decoded['pet'], + specifiedType: modelType), + model, + ); + }); + + test('default URL-encoded model field is JSON and round-trips', () async { + // OpenAPI 3.0.4 section 4.7.15.2.1: when no style/explode/allowReserved + // is explicitly set, an object field uses application/json encoding. + // Matches the generated non-container form-parameter call site. + final encoded = encodeParameter( + standardSerializers, + model, + modelType, + asString: true, + ); + final body = await transform(encoded, Headers.formUrlEncodedContentType); + final fields = Uri.splitQueryString(body); + + expect(fields.keys, ['pet']); + final decodedPet = jsonDecode(fields['pet']!); + expect(decodedPet, {'name': 'Milo & friends'}); + expect( + standardSerializers.deserialize(decodedPet, specifiedType: modelType), + model, + ); + }); + + test('query and multipart model values remain serialized objects', () { + for (final forMultipart in [false, true]) { + final encoded = encodeParameter( + standardSerializers, + model, + modelType, + forMultipart: forMultipart, + ); + expect(encoded, {'name': 'Milo & friends'}); + } + }); + + test('serialized strings are not JSON-quoted in form fields', () { + final date = DateTime.utc(2026, 10, 12); + expect( + encodeParameter( + standardSerializers, + date, + const FullType(DateTime), + asString: true, + ), + '2026-10-12T00:00:00.000Z', + ); + }); + + test('null parameters are encoded as empty values', () { + expect( + encodeParameter( + standardSerializers, + null, + const FullType(String), + ), + '', + ); + }); + + test('explicit maps and list formats retain their existing behavior', () { + final serializers = (standardSerializers.toBuilder() + ..addBuilderFactory( + const FullType(BuiltList, [FullType(String)]), + () => ListBuilder(), + ) + ..addBuilderFactory( + const FullType(BuiltMap, [FullType(String), FullType(String)]), + () => MapBuilder(), + )) + .build(); + final map = BuiltMap({'name': 'Milo'}); + expect( + encodeParameter( + serializers, + map, + const FullType(BuiltMap, [FullType(String), FullType(String)]), + asString: true, + ), + {'name': 'Milo'}, + ); + + final list = BuiltList(['Milo', 'friends']); + for (final format in [ListFormat.multi, ListFormat.csv]) { + final encoded = encodeParameter( + serializers, + list, + const FullType(BuiltList, [FullType(String)]), + asString: true, + format: format, + ); + if (format == ListFormat.csv) { + expect(encoded, 'Milo,friends'); + } else { + expect(encoded, isA>()); + final param = encoded as ListParam; + expect(param.value, ['Milo', 'friends']); + expect(param.format, ListFormat.multi); + } + } + }); + + test('nullable-item query lists preserve standard collection formats', () { + const type = FullType(BuiltList, [FullType.nullable(String)]); + final serializers = + (standardSerializers.toBuilder() + ..addBuilderFactory(type, () => ListBuilder())) + .build(); + final values = BuiltList(['blue', null, 'brown']); + final expected = { + ListFormat.multi: 'color=blue&color=brown', + ListFormat.csv: 'color=blue,brown', + ListFormat.ssv: 'color=blue%20brown', + ListFormat.pipes: 'color=blue|brown', + }; + + for (final entry in expected.entries) { + final encoded = encodeParameter( + serializers, + values, + type, + format: entry.key, + omitNullCollectionItems: true, + ); + + expect(encoded, isA>()); + expect( + Transformer.urlEncodeQueryMap({'color': encoded}), + entry.value, + ); + } + }); + + test('query null filtering handles edge positions and all-null lists', () { + const type = FullType(BuiltList, [FullType.nullable(String)]); + final serializers = + (standardSerializers.toBuilder() + ..addBuilderFactory(type, () => ListBuilder())) + .build(); + final cases = , String>{ + BuiltList([null, 'blue', null, 'brown', null]): + 'color=blue&color=brown', + BuiltList([null, null]): '', + BuiltList(['', null, 'brown']): 'color=&color=brown', + }; + + for (final entry in cases.entries) { + final encoded = encodeParameter( + serializers, + entry.key, + type, + omitNullCollectionItems: true, + ); + + expect( + Transformer.urlEncodeQueryMap({'color': encoded}), + entry.value, + ); + } + }); + + test('nullable query items encode reserved data before delimiters', () { + const type = FullType(BuiltList, [FullType.nullable(String)]); + final serializers = + (standardSerializers.toBuilder() + ..addBuilderFactory(type, () => ListBuilder())) + .build(); + final values = BuiltList([ + 'blue,green', + null, + 'black & white', + ]); + final expected = { + ListFormat.multi: 'color=blue%2Cgreen&color=black+%26+white', + ListFormat.csv: 'color=blue%2Cgreen,black+%26+white', + ListFormat.ssv: 'color=blue%2Cgreen%20black+%26+white', + ListFormat.pipes: 'color=blue%2Cgreen|black+%26+white', + }; + + for (final entry in expected.entries) { + final encoded = encodeParameter( + serializers, + values, + type, + format: entry.key, + omitNullCollectionItems: true, + ); + + expect( + Transformer.urlEncodeQueryMap({'color': encoded}), + entry.value, + ); + } + }); + + test('query null filtering is opt-in and does not mutate its input', () { + const type = FullType(BuiltList, [FullType.nullable(String)]); + final serializers = + (standardSerializers.toBuilder() + ..addBuilderFactory(type, () => ListBuilder())) + .build(); + final values = BuiltList(['blue', null, 'brown']); + + final unfiltered = encodeParameter( + serializers, + values, + type, + ) as ListParam; + final filtered = encodeParameter( + serializers, + values, + type, + omitNullCollectionItems: true, + ) as ListParam; + + expect(unfiltered.value, ['blue', null, 'brown']); + expect(filtered.value, ['blue', 'brown']); + expect(values.toList(), ['blue', null, 'brown']); + }); + + test('explicit JSON form field needs a second decode to recover the model', + () async { + // Models a server contract explicitly requiring JSON text in `pet`. + // This conversion belongs at that form call site, not in JSON bodies. + final serialized = standardSerializers.serialize( + model, + specifiedType: modelType, + ); + final body = await transform( + jsonEncode(serialized), + Headers.formUrlEncodedContentType, + ); + final fields = Uri.splitQueryString(body); + + expect(fields.keys, ['pet']); + expect(fields['pet'], isA()); + final decodedPet = jsonDecode(fields['pet']!); + expect(decodedPet, {'name': 'Milo & friends'}); + expect( + standardSerializers.deserialize(decodedPet, specifiedType: modelType), + model, + ); + + // Using the same string inside a JSON body does not recover an object: + // the server would receive a string property instead. + final jsonBody = await transform(fields['pet'], Headers.jsonContentType); + final decodedJson = jsonDecode(jsonBody) as Map; + expect(decodedJson['pet'], isA()); + }); + }); +} diff --git a/samples/openapi3/client/petstore/dart-dio/petstore_client_lib_fake/test/path_parameter_encoding_test.dart b/samples/openapi3/client/petstore/dart-dio/petstore_client_lib_fake/test/path_parameter_encoding_test.dart new file mode 100644 index 000000000000..ef2ecfcc468a --- /dev/null +++ b/samples/openapi3/client/petstore/dart-dio/petstore_client_lib_fake/test/path_parameter_encoding_test.dart @@ -0,0 +1,47 @@ +import 'package:built_collection/built_collection.dart'; +import 'package:built_value/serializer.dart'; +import 'package:dio/dio.dart'; +import 'package:openapi/src/api_util.dart'; +import 'package:openapi/src/model/outer_enum.dart'; +import 'package:openapi/src/serializers.dart'; +import 'package:test/test.dart'; + +void main() { + group('path parameter enum encoding', () { + test('uses the serialized wire value instead of the EnumClass name', () { + const value = OuterEnum.unknownDefaultOpenApi; + + expect(value.name, 'unknownDefaultOpenApi'); + expect( + encodePathParameter( + standardSerializers, + value, + const FullType(OuterEnum), + ), + 'unknown_default_open_api', + ); + }); + + test('uses serialized wire values for enum collection elements', () { + final serializers = + (standardSerializers.toBuilder()..addBuilderFactory( + const FullType(BuiltList, [FullType(OuterEnum)]), + () => ListBuilder(), + )) + .build(); + + expect( + encodePathParameter( + serializers, + BuiltList(const [ + OuterEnum.unknownDefaultOpenApi, + OuterEnum.placed, + ]), + const FullType(BuiltList, [FullType(OuterEnum)]), + format: ListFormat.csv, + ), + 'unknown_default_open_api,placed', + ); + }); + }); +}