Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -660,10 +660,134 @@ public void postProcessModelProperty(CodegenModel model, CodegenProperty propert
items.getAdditionalProperties().dataType
));
}

// Recursively register builder factories for every nested
// container reachable from this property. Without this step,
// shapes like Map<String, List<X>> (e.g. an OpenAPI object
// with `additionalProperties: { type: array, items: ...}`)
// never get a `BuiltList<X>` factory registered, and
// built_value throws "No builder factory for BuiltList<X>"
// at deserialization time.
registerNestedBuilderFactories(property);
}
}
}

/**
* Walk the CodegenProperty tree and register a built_value
* BuilderFactory for every container node, including the top one.
* Handles arbitrary nesting like {@code Map<String, List<X>>},
* {@code List<Map<String, X>>}, {@code List<List<X>>}, etc.
*/
private void registerNestedBuilderFactories(CodegenProperty prop) {
if (prop == null || !prop.isContainer || prop.items == null) {
return;
}
// Recurse first so deeper containers are registered too.
registerNestedBuilderFactories(prop.items);

if (prop.items.isContainer) {
// Truly nested container (e.g. Map<String, List<X>>):
// must use composite form because the simple constructor
// cannot express the nested FullType.
BuilderFactoryExpr expr = renderBuilderFactory(prop);
if (expr != null) {
addBuiltValueSerializer(BuiltValueSerializer.composite(
expr.fullTypeArgs,
expr.builderInstantiation));
}
} else {
// Leaf container (e.g. List<X>, Map<String, X>): use the
// same simple constructor the rest of the codegen uses so
// the Set deduplicates correctly.
addBuiltValueSerializer(new BuiltValueSerializer(
prop.isArray,
prop.getUniqueItems(),
prop.isMap,
prop.items.isNullable,
prop.items.dataType));
}
}

/**
* Render the FullType argument list and the matching Builder
* instantiation for a container CodegenProperty.
*
* @return null if {@code prop} is not a container we can render.
*/
private BuilderFactoryExpr renderBuilderFactory(CodegenProperty prop) {
if (prop == null || !prop.isContainer || prop.items == null) {
return null;
}
String innerFullType = renderInnerFullType(prop.items);
String innerDart = renderDartType(prop.items);

if (prop.isArray) {
String collection = prop.getUniqueItems() ? "BuiltSet" : "BuiltList";
String builder = prop.getUniqueItems() ? "SetBuilder" : "ListBuilder";
return new BuilderFactoryExpr(
collection + ", [FullType(" + innerFullType + ")]",
builder + "<" + innerDart + ">");
}
if (prop.isMap) {
return new BuilderFactoryExpr(
"BuiltMap, [FullType(String), FullType(" + innerFullType + ")]",
"MapBuilder<String, " + innerDart + ">");
}
return null;
}

/**
* What goes inside {@code FullType(...)} for this property:
* a leaf type name like {@code "Foo"}, or a nested expression like
* {@code "BuiltList, [FullType(Foo)]"}.
*/
private String renderInnerFullType(CodegenProperty prop) {
if (prop == null) return "dynamic";
if (!prop.isContainer || prop.items == null) {
return prop.dataType;
}
String inner = renderInnerFullType(prop.items);
if (prop.isArray) {
String collection = prop.getUniqueItems() ? "BuiltSet" : "BuiltList";
return collection + ", [FullType(" + inner + ")]";
}
if (prop.isMap) {
return "BuiltMap, [FullType(String), FullType(" + inner + ")]";
}
return prop.dataType;
}

/**
* Render the Dart type literal (e.g. {@code BuiltMap<String, BuiltList<Foo>>})
* used inside the {@code () => XBuilder<...>()} lambda.
*/
private String renderDartType(CodegenProperty prop) {
if (prop == null) return "dynamic";
if (!prop.isContainer || prop.items == null) {
return prop.dataType;
}
String inner = renderDartType(prop.items);
if (prop.isArray) {
String collection = prop.getUniqueItems() ? "BuiltSet" : "BuiltList";
return collection + "<" + inner + ">";
}
if (prop.isMap) {
return "BuiltMap<String, " + inner + ">";
}
return prop.dataType;
}

private static final class BuilderFactoryExpr {
final String fullTypeArgs;
final String builderInstantiation;

BuilderFactoryExpr(String fullTypeArgs, String builderInstantiation) {
this.fullTypeArgs = fullTypeArgs;
this.builderInstantiation = builderInstantiation;
}
}

@Override
public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List<ModelMap> allModels) {
super.postProcessOperationsWithModels(objs, allModels);
Expand Down Expand Up @@ -817,12 +941,45 @@ static class BuiltValueSerializer {

@Getter final String dataType;

// When non-null, the serializer is rendered verbatim from these
// pre-computed strings instead of being dispatched through the
// isArray/isMap branches in serializers.mustache. Used for
// arbitrarily nested container types where dataType alone can't
// express the FullType expression (e.g. Map<String, List<X>>).
@Getter final String fullTypeArgs;

@Getter final String builderInstantiation;

private BuiltValueSerializer(boolean isArray, boolean uniqueItems, boolean isMap, boolean isNullable, String dataType) {
this.isArray = isArray;
this.uniqueItems = uniqueItems;
this.isMap = isMap;
this.isNullable = isNullable;
this.dataType = dataType;
this.fullTypeArgs = null;
this.builderInstantiation = null;
}

private BuiltValueSerializer(String fullTypeArgs, String builderInstantiation) {
this.isArray = false;
this.uniqueItems = false;
this.isMap = false;
this.isNullable = false;
this.dataType = "";
this.fullTypeArgs = fullTypeArgs;
this.builderInstantiation = builderInstantiation;
}

/**
* Build a serializer for a nested-container BuilderFactory whose
* type signature can't be expressed by the simple
* (isArray, isMap, dataType) form. {@code fullTypeArgs} is the
* argument list for {@code FullType(...)} (without the wrapping
* call) and {@code builderInstantiation} is the inside of
* {@code () => ...()}.
*/
public static BuiltValueSerializer composite(String fullTypeArgs, String builderInstantiation) {
return new BuiltValueSerializer(fullTypeArgs, builderInstantiation);
}

public boolean isArray() {
Expand All @@ -842,11 +999,18 @@ public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
BuiltValueSerializer that = (BuiltValueSerializer) o;
if (fullTypeArgs != null || that.fullTypeArgs != null) {
return Objects.equals(fullTypeArgs, that.fullTypeArgs)
&& Objects.equals(builderInstantiation, that.builderInstantiation);
}
return isArray == that.isArray && uniqueItems == that.uniqueItems && isMap == that.isMap && isNullable == that.isNullable && dataType.equals(that.dataType);
}

@Override
public int hashCode() {
if (fullTypeArgs != null) {
return Objects.hash(fullTypeArgs, builderInstantiation);
}
return Objects.hash(isArray, uniqueItems, isMap, isNullable, dataType);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ part 'serializers.g.dart';
])
Serializers serializers = (_$serializers.toBuilder(){{#builtValueSerializers}}
..addBuilderFactory(
{{#fullTypeArgs}}
const FullType({{{fullTypeArgs}}}),
() => {{{builderInstantiation}}}(),
{{/fullTypeArgs}}
{{^fullTypeArgs}}
{{#isArray}}
const FullType(Built{{#uniqueItems}}Set{{/uniqueItems}}{{^uniqueItems}}List{{/uniqueItems}}, [FullType{{#isNullable}}.nullable{{/isNullable}}({{dataType}})]),
() => {{#uniqueItems}}Set{{/uniqueItems}}{{^uniqueItems}}List{{/uniqueItems}}Builder<{{dataType}}>(),
Expand All @@ -31,6 +36,7 @@ Serializers serializers = (_$serializers.toBuilder(){{#builtValueSerializers}}
const FullType(BuiltMap, [FullType(String), FullType{{#isNullable}}.nullable{{/isNullable}}({{dataType}})]),
() => MapBuilder<String, {{dataType}}{{#isNullable}}?{{/isNullable}}>(),
{{/isMap}}
{{/fullTypeArgs}}
){{/builtValueSerializers}}
{{#models}}{{#model}}{{#vendorExtensions.x-is-parent}}..add({{classname}}.serializer)
{{/vendorExtensions.x-is-parent}}{{/model}}{{/models}}..add(const OneOfSerializer())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,50 @@ public void testImportMappingsInSerializersAndBarrelFile() throws IOException {
"package:my_api/src/model/custom_address.dart");
}

/**
* Regression test for missing BuilderFactory entries on container
* types reachable only via {@code additionalProperties}.
*
* Before the fix, a property like
* {@code Map<String, List<Widget>>} (an object schema with
* {@code additionalProperties: { type: array, items: ... }}) ended
* up in the generated Dart class but no
* {@code addBuilderFactory(BuiltList<Widget>, ...)} call was
* emitted in {@code serializers.dart}. built_value then failed at
* runtime with
* {@code Bad state: No builder factory for BuiltList<Widget>}.
*
* The fix walks every model property's container tree and registers
* a factory for each nested layer.
*/
@Test
public void testNestedAdditionalPropertiesGetBuilderFactories() 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_additional_properties_factory.yaml")
.setOutputDir(output.getAbsolutePath().replace("\\", "/"));

ClientOptInput opts = configurator.toClientOptInput();
Generator generator = new DefaultGenerator().opts(opts);
List<File> files = generator.generate();
files.forEach(File::deleteOnExit);

Path serializers = output.toPath().resolve("lib/src/serializers.dart");

// Inner container: List<Widget>.
TestUtils.assertFileContains(serializers,
"const FullType(BuiltList, [FullType(Widget)]),",
"() => ListBuilder<Widget>(),");

// Outer container: Map<String, List<Widget>>.
TestUtils.assertFileContains(serializers,
"const FullType(BuiltMap, [FullType(String), FullType(BuiltList, [FullType(Widget)])]),",
"() => MapBuilder<String, BuiltList<Widget>>(),");
}

@Test
public void verifyDartDioGeneratorRuns() throws IOException {
File output = Files.createTempDirectory("test").toFile();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
openapi: "3.0.0"
info:
title: Built-value additionalProperties BuilderFactory test
version: "1.0.0"
paths:
/catalog/{id}:
get:
operationId: getCatalog
parameters:
- name: id
in: path
required: true
schema: { type: integer }
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: "#/components/schemas/Catalog"
components:
schemas:
Widget:
type: object
required:
- id
- name
properties:
id:
type: integer
name:
type: string
# `additionalProperties: { type: array, items: ... }` is the canonical
# "map of array of $ref" shape. Without the BuilderFactory walk,
# deserialization throws
# Bad state: No builder factory for BuiltList<Widget>
WidgetsByCategory:
type: object
additionalProperties:
type: array
items:
$ref: "#/components/schemas/Widget"
Catalog:
type: object
required:
- id
properties:
id:
type: integer
widgetsByCategory:
$ref: "#/components/schemas/WidgetsByCategory"
Loading