Skip to content

Commit a7187d1

Browse files
committed
Merge branch 'fix/dart-dio-built-value-additional-properties-factory' of https://github.com/Homegan/openapi-generator into Homegan-fix/dart-dio-built-value-additional-properties-factory
2 parents 9f7944d + c26dac7 commit a7187d1

6 files changed

Lines changed: 339 additions & 6 deletions

File tree

modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/DartDioClientCodegen.java

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -661,10 +661,134 @@ public void postProcessModelProperty(CodegenModel model, CodegenProperty propert
661661
items.getAdditionalProperties().dataType
662662
));
663663
}
664+
665+
// Recursively register builder factories for every nested
666+
// container reachable from this property. Without this step,
667+
// shapes like Map<String, List<X>> (e.g. an OpenAPI object
668+
// with `additionalProperties: { type: array, items: ...}`)
669+
// never get a `BuiltList<X>` factory registered, and
670+
// built_value throws "No builder factory for BuiltList<X>"
671+
// at deserialization time.
672+
registerNestedBuilderFactories(property);
664673
}
665674
}
666675
}
667676

677+
/**
678+
* Walk the CodegenProperty tree and register a built_value
679+
* BuilderFactory for every container node, including the top one.
680+
* Handles arbitrary nesting like {@code Map<String, List<X>>},
681+
* {@code List<Map<String, X>>}, {@code List<List<X>>}, etc.
682+
*/
683+
private void registerNestedBuilderFactories(CodegenProperty prop) {
684+
if (prop == null || !prop.isContainer || prop.items == null) {
685+
return;
686+
}
687+
// Recurse first so deeper containers are registered too.
688+
registerNestedBuilderFactories(prop.items);
689+
690+
if (prop.items.isContainer) {
691+
// Truly nested container (e.g. Map<String, List<X>>):
692+
// must use composite form because the simple constructor
693+
// cannot express the nested FullType.
694+
BuilderFactoryExpr expr = renderBuilderFactory(prop);
695+
if (expr != null) {
696+
addBuiltValueSerializer(BuiltValueSerializer.composite(
697+
expr.fullTypeArgs,
698+
expr.builderInstantiation));
699+
}
700+
} else {
701+
// Leaf container (e.g. List<X>, Map<String, X>): use the
702+
// same simple constructor the rest of the codegen uses so
703+
// the Set deduplicates correctly.
704+
addBuiltValueSerializer(new BuiltValueSerializer(
705+
prop.isArray,
706+
prop.getUniqueItems(),
707+
prop.isMap,
708+
prop.items.isNullable,
709+
prop.items.dataType));
710+
}
711+
}
712+
713+
/**
714+
* Render the FullType argument list and the matching Builder
715+
* instantiation for a container CodegenProperty.
716+
*
717+
* @return null if {@code prop} is not a container we can render.
718+
*/
719+
private BuilderFactoryExpr renderBuilderFactory(CodegenProperty prop) {
720+
if (prop == null || !prop.isContainer || prop.items == null) {
721+
return null;
722+
}
723+
String innerFullType = renderInnerFullType(prop.items);
724+
String innerDart = renderDartType(prop.items);
725+
726+
if (prop.isArray) {
727+
String collection = prop.getUniqueItems() ? "BuiltSet" : "BuiltList";
728+
String builder = prop.getUniqueItems() ? "SetBuilder" : "ListBuilder";
729+
return new BuilderFactoryExpr(
730+
collection + ", [FullType(" + innerFullType + ")]",
731+
builder + "<" + innerDart + ">");
732+
}
733+
if (prop.isMap) {
734+
return new BuilderFactoryExpr(
735+
"BuiltMap, [FullType(String), FullType(" + innerFullType + ")]",
736+
"MapBuilder<String, " + innerDart + ">");
737+
}
738+
return null;
739+
}
740+
741+
/**
742+
* What goes inside {@code FullType(...)} for this property:
743+
* a leaf type name like {@code "Foo"}, or a nested expression like
744+
* {@code "BuiltList, [FullType(Foo)]"}.
745+
*/
746+
private String renderInnerFullType(CodegenProperty prop) {
747+
if (prop == null) return "dynamic";
748+
if (!prop.isContainer || prop.items == null) {
749+
return prop.dataType;
750+
}
751+
String inner = renderInnerFullType(prop.items);
752+
if (prop.isArray) {
753+
String collection = prop.getUniqueItems() ? "BuiltSet" : "BuiltList";
754+
return collection + ", [FullType(" + inner + ")]";
755+
}
756+
if (prop.isMap) {
757+
return "BuiltMap, [FullType(String), FullType(" + inner + ")]";
758+
}
759+
return prop.dataType;
760+
}
761+
762+
/**
763+
* Render the Dart type literal (e.g. {@code BuiltMap<String, BuiltList<Foo>>})
764+
* used inside the {@code () => XBuilder<...>()} lambda.
765+
*/
766+
private String renderDartType(CodegenProperty prop) {
767+
if (prop == null) return "dynamic";
768+
if (!prop.isContainer || prop.items == null) {
769+
return prop.dataType;
770+
}
771+
String inner = renderDartType(prop.items);
772+
if (prop.isArray) {
773+
String collection = prop.getUniqueItems() ? "BuiltSet" : "BuiltList";
774+
return collection + "<" + inner + ">";
775+
}
776+
if (prop.isMap) {
777+
return "BuiltMap<String, " + inner + ">";
778+
}
779+
return prop.dataType;
780+
}
781+
782+
private static final class BuilderFactoryExpr {
783+
final String fullTypeArgs;
784+
final String builderInstantiation;
785+
786+
BuilderFactoryExpr(String fullTypeArgs, String builderInstantiation) {
787+
this.fullTypeArgs = fullTypeArgs;
788+
this.builderInstantiation = builderInstantiation;
789+
}
790+
}
791+
668792
@Override
669793
public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List<ModelMap> allModels) {
670794
super.postProcessOperationsWithModels(objs, allModels);
@@ -833,12 +957,45 @@ static class BuiltValueSerializer {
833957

834958
@Getter final String dataType;
835959

960+
// When non-null, the serializer is rendered verbatim from these
961+
// pre-computed strings instead of being dispatched through the
962+
// isArray/isMap branches in serializers.mustache. Used for
963+
// arbitrarily nested container types where dataType alone can't
964+
// express the FullType expression (e.g. Map<String, List<X>>).
965+
@Getter final String fullTypeArgs;
966+
967+
@Getter final String builderInstantiation;
968+
836969
private BuiltValueSerializer(boolean isArray, boolean uniqueItems, boolean isMap, boolean isNullable, String dataType) {
837970
this.isArray = isArray;
838971
this.uniqueItems = uniqueItems;
839972
this.isMap = isMap;
840973
this.isNullable = isNullable;
841974
this.dataType = dataType;
975+
this.fullTypeArgs = null;
976+
this.builderInstantiation = null;
977+
}
978+
979+
private BuiltValueSerializer(String fullTypeArgs, String builderInstantiation) {
980+
this.isArray = false;
981+
this.uniqueItems = false;
982+
this.isMap = false;
983+
this.isNullable = false;
984+
this.dataType = "";
985+
this.fullTypeArgs = fullTypeArgs;
986+
this.builderInstantiation = builderInstantiation;
987+
}
988+
989+
/**
990+
* Build a serializer for a nested-container BuilderFactory whose
991+
* type signature can't be expressed by the simple
992+
* (isArray, isMap, dataType) form. {@code fullTypeArgs} is the
993+
* argument list for {@code FullType(...)} (without the wrapping
994+
* call) and {@code builderInstantiation} is the inside of
995+
* {@code () => ...()}.
996+
*/
997+
public static BuiltValueSerializer composite(String fullTypeArgs, String builderInstantiation) {
998+
return new BuiltValueSerializer(fullTypeArgs, builderInstantiation);
842999
}
8431000

8441001
public boolean isArray() {
@@ -858,11 +1015,18 @@ public boolean equals(Object o) {
8581015
if (this == o) return true;
8591016
if (o == null || getClass() != o.getClass()) return false;
8601017
BuiltValueSerializer that = (BuiltValueSerializer) o;
1018+
if (fullTypeArgs != null || that.fullTypeArgs != null) {
1019+
return Objects.equals(fullTypeArgs, that.fullTypeArgs)
1020+
&& Objects.equals(builderInstantiation, that.builderInstantiation);
1021+
}
8611022
return isArray == that.isArray && uniqueItems == that.uniqueItems && isMap == that.isMap && isNullable == that.isNullable && dataType.equals(that.dataType);
8621023
}
8631024

8641025
@Override
8651026
public int hashCode() {
1027+
if (fullTypeArgs != null) {
1028+
return Objects.hash(fullTypeArgs, builderInstantiation);
1029+
}
8661030
return Objects.hash(isArray, uniqueItems, isMap, isNullable, dataType);
8671031
}
8681032
}

modules/openapi-generator/src/main/resources/dart/libraries/dio/serialization/built_value/serializers.mustache

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ part 'serializers.g.dart';
2323
])
2424
Serializers serializers = (_$serializers.toBuilder(){{#builtValueSerializers}}
2525
..addBuilderFactory(
26+
{{#fullTypeArgs}}
27+
const FullType({{{fullTypeArgs}}}),
28+
() => {{{builderInstantiation}}}(),
29+
{{/fullTypeArgs}}
30+
{{^fullTypeArgs}}
2631
{{#isArray}}
2732
const FullType(Built{{#uniqueItems}}Set{{/uniqueItems}}{{^uniqueItems}}List{{/uniqueItems}}, [FullType{{#isNullable}}.nullable{{/isNullable}}({{dataType}})]),
2833
() => {{#uniqueItems}}Set{{/uniqueItems}}{{^uniqueItems}}List{{/uniqueItems}}Builder<{{dataType}}>(),
@@ -31,6 +36,7 @@ Serializers serializers = (_$serializers.toBuilder(){{#builtValueSerializers}}
3136
const FullType(BuiltMap, [FullType(String), FullType{{#isNullable}}.nullable{{/isNullable}}({{dataType}})]),
3237
() => MapBuilder<String, {{dataType}}{{#isNullable}}?{{/isNullable}}>(),
3338
{{/isMap}}
39+
{{/fullTypeArgs}}
3440
){{/builtValueSerializers}}
3541
{{#models}}{{#model}}{{#vendorExtensions.x-is-parent}}..add({{classname}}.serializer)
3642
{{/vendorExtensions.x-is-parent}}{{/model}}{{/models}}..add(const OneOfSerializer())

modules/openapi-generator/src/test/java/org/openapitools/codegen/dart/dio/DartDioClientCodegenTest.java

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,4 +274,48 @@ public void verifyWebhookImports() throws IOException {
274274
Assert.assertFalse(apiContent.contains("&#x3D;"),
275275
"Webhook should not contain HTML entity encoding (bug #22586 symptom)");
276276
}
277+
278+
/**
279+
* Regression test for missing BuilderFactory entries on container
280+
* types reachable only via {@code additionalProperties}.
281+
*
282+
* Before the fix, a property like
283+
* {@code Map<String, List<Widget>>} (an object schema with
284+
* {@code additionalProperties: { type: array, items: ... }}) ended
285+
* up in the generated Dart class but no
286+
* {@code addBuilderFactory(BuiltList<Widget>, ...)} call was
287+
* emitted in {@code serializers.dart}. built_value then failed at
288+
* runtime with
289+
* {@code Bad state: No builder factory for BuiltList<Widget>}.
290+
*
291+
* The fix walks every model property's container tree and registers
292+
* a factory for each nested layer.
293+
*/
294+
@Test
295+
public void testNestedAdditionalPropertiesGetBuilderFactories() throws IOException {
296+
File output = Files.createTempDirectory("test").toFile();
297+
output.deleteOnExit();
298+
299+
final CodegenConfigurator configurator = new CodegenConfigurator()
300+
.setGeneratorName("dart-dio")
301+
.setInputSpec("src/test/resources/3_0/dart-dio/built_value_additional_properties_factory.yaml")
302+
.setOutputDir(output.getAbsolutePath().replace("\\", "/"));
303+
304+
ClientOptInput opts = configurator.toClientOptInput();
305+
Generator generator = new DefaultGenerator().opts(opts);
306+
List<File> files = generator.generate();
307+
files.forEach(File::deleteOnExit);
308+
309+
Path serializers = output.toPath().resolve("lib/src/serializers.dart");
310+
311+
// Inner container: List<Widget>.
312+
TestUtils.assertFileContains(serializers,
313+
"const FullType(BuiltList, [FullType(Widget)]),",
314+
"() => ListBuilder<Widget>(),");
315+
316+
// Outer container: Map<String, List<Widget>>.
317+
TestUtils.assertFileContains(serializers,
318+
"const FullType(BuiltMap, [FullType(String), FullType(BuiltList, [FullType(Widget)])]),",
319+
"() => MapBuilder<String, BuiltList<Widget>>(),");
320+
}
277321
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
openapi: "3.0.0"
2+
info:
3+
title: Built-value additionalProperties BuilderFactory test
4+
version: "1.0.0"
5+
paths:
6+
/catalog/{id}:
7+
get:
8+
operationId: getCatalog
9+
parameters:
10+
- name: id
11+
in: path
12+
required: true
13+
schema: { type: integer }
14+
responses:
15+
"200":
16+
description: OK
17+
content:
18+
application/json:
19+
schema:
20+
$ref: "#/components/schemas/Catalog"
21+
components:
22+
schemas:
23+
Widget:
24+
type: object
25+
required:
26+
- id
27+
- name
28+
properties:
29+
id:
30+
type: integer
31+
name:
32+
type: string
33+
# `additionalProperties: { type: array, items: ... }` is the canonical
34+
# "map of array of $ref" shape. Without the BuilderFactory walk,
35+
# deserialization throws
36+
# Bad state: No builder factory for BuiltList<Widget>
37+
WidgetsByCategory:
38+
type: object
39+
additionalProperties:
40+
type: array
41+
items:
42+
$ref: "#/components/schemas/Widget"
43+
Catalog:
44+
type: object
45+
required:
46+
- id
47+
properties:
48+
id:
49+
type: integer
50+
widgetsByCategory:
51+
$ref: "#/components/schemas/WidgetsByCategory"

samples/openapi3/client/petstore/dart-dio/petstore-timemachine/lib/src/serializers.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ Serializers serializers = (_$serializers.toBuilder()
4747
const FullType(BuiltList, [FullType(String)]),
4848
() => ListBuilder<String>(),
4949
)
50+
..addBuilderFactory(
51+
const FullType(BuiltList, [FullType(Tag)]),
52+
() => ListBuilder<Tag>(),
53+
)
5054
..add(const OneOfSerializer())
5155
..add(const AnyOfSerializer())
5256
..add(const OffsetDateSerializer())

0 commit comments

Comments
 (0)