Skip to content

Commit 16902e5

Browse files
committed
Generate interfaces for interface mixins
1 parent ab7a64d commit 16902e5

18 files changed

Lines changed: 883 additions & 11 deletions

File tree

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package software.amazon.smithy.java.codegen.test;
7+
8+
import static org.junit.jupiter.api.Assertions.assertEquals;
9+
import static org.junit.jupiter.api.Assertions.assertFalse;
10+
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
11+
import static org.junit.jupiter.api.Assertions.assertTrue;
12+
13+
import java.util.List;
14+
import org.junit.jupiter.api.Test;
15+
import software.amazon.smithy.java.codegen.test.model.DetailedUser;
16+
import software.amazon.smithy.java.codegen.test.model.HasFullName;
17+
import software.amazon.smithy.java.codegen.test.model.HasName;
18+
import software.amazon.smithy.java.codegen.test.model.HasTags;
19+
import software.amazon.smithy.java.codegen.test.model.SimpleUser;
20+
import software.amazon.smithy.java.codegen.test.model.TaggedResource;
21+
import software.amazon.smithy.java.codegen.test.model.TaggedUser;
22+
import software.amazon.smithy.java.codegen.test.model.UserNotFound;
23+
24+
class InterfaceMixinTest {
25+
26+
@Test
27+
void singleInterfaceMixin() {
28+
HasName user = SimpleUser.builder().id(42).name("Bob").email("bob@test.com").build();
29+
assertEquals("Bob", user.getName());
30+
assertEquals(42, user.getId());
31+
}
32+
33+
@Test
34+
void chainedInterfaceMixinHierarchy() {
35+
HasFullName user = DetailedUser.builder().id(7).name("Carol").lastName("Jones").age(30).build();
36+
assertEquals("Jones", user.getLastName());
37+
assertEquals(7, user.getId());
38+
// HasFullName extends HasName, so DetailedUser is also a HasName
39+
assertInstanceOf(HasName.class, user);
40+
}
41+
42+
@Test
43+
void collectionMemberInterface() {
44+
HasTags resource = TaggedResource.builder().resourceId("r-1").tags(List.of("x")).build();
45+
assertEquals(List.of("x"), resource.getTags());
46+
assertTrue(resource.hasTags());
47+
}
48+
49+
@Test
50+
void collectionMemberInterfaceUnset() {
51+
HasTags resource = TaggedResource.builder().resourceId("r-1").build();
52+
assertFalse(resource.hasTags());
53+
}
54+
55+
@Test
56+
void multipleInterfaceMixins() {
57+
HasName user = TaggedUser.builder().id(5).name("Frank").tags(List.of("a", "b")).role("user").build();
58+
assertEquals("Frank", user.getName());
59+
assertEquals(5, user.getId());
60+
assertInstanceOf(HasTags.class, user);
61+
}
62+
63+
@Test
64+
void errorShapeWithInterfaceMixin() {
65+
HasName error = UserNotFound.builder().id(1).name("missing").detail("not found").build();
66+
assertEquals("missing", error.getName());
67+
assertEquals(1, error.getId());
68+
assertInstanceOf(Exception.class, error);
69+
}
70+
}

codegen/codegen-core/src/it/resources/META-INF/smithy/main.smithy

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use smithy.java.codegen.test.exceptions#ExceptionTests
77
use smithy.java.codegen.test.idempotencytoken#IdempotencyTokenRequired
88
use smithy.java.codegen.test.lists#ListTests
99
use smithy.java.codegen.test.maps#MapTests
10+
use smithy.java.codegen.test.mixins#MixinTests
1011
use smithy.java.codegen.test.naming#Naming
1112
use smithy.java.codegen.test.recursion#RecursionTests
1213
use smithy.java.codegen.test.structures#StructureTests
@@ -22,6 +23,7 @@ service TestService {
2223
ExceptionTests
2324
ListTests
2425
MapTests
26+
MixinTests
2527
StructureTests
2628
UnionTests
2729
RecursionTests

codegen/codegen-core/src/it/resources/META-INF/smithy/manifest

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ structures/union-members.smithy
4848
traits/traits.smithy
4949
unions/union-tests.smithy
5050
unions/unions-all-types.smithy
51+
mixins/interface-mixin-tests.smithy
5152
common.smithy
5253
authScheme.smithy
5354
main.smithy
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
$version: "2"
2+
3+
namespace smithy.java.codegen.test.mixins
4+
5+
/// Interface mixin with basic members
6+
@mixin(interface: true)
7+
structure HasName {
8+
name: String
9+
@required
10+
id: Integer
11+
}
12+
13+
/// Interface mixin extending another interface mixin
14+
@mixin(interface: true)
15+
structure HasFullName with [HasName] {
16+
lastName: String
17+
}
18+
19+
/// Concrete structure using a single interface mixin
20+
structure SimpleUser with [HasName] {
21+
email: String
22+
}
23+
24+
/// Concrete structure using a chained interface mixin hierarchy
25+
structure DetailedUser with [HasFullName] {
26+
age: Integer
27+
}
28+
29+
/// Interface mixin with a list member to test has*() override
30+
@mixin(interface: true)
31+
structure HasTags {
32+
tags: TagList
33+
}
34+
35+
list TagList {
36+
member: String
37+
}
38+
39+
/// Concrete structure using the list-bearing interface mixin
40+
structure TaggedResource with [HasTags] {
41+
resourceId: String
42+
}
43+
44+
/// Concrete structure using multiple interface mixins
45+
structure TaggedUser with [HasName, HasTags] {
46+
role: String
47+
}
48+
49+
/// Error shape using an interface mixin
50+
@error("client")
51+
structure UserNotFound with [HasName] {
52+
detail: String
53+
}
54+
55+
resource MixinTests {
56+
operations: [
57+
MixinTestOp
58+
]
59+
}
60+
61+
@http(method: "POST", uri: "/mixin-test")
62+
operation MixinTestOp {
63+
input := {
64+
simpleUser: SimpleUser
65+
detailedUser: DetailedUser
66+
taggedResource: TaggedResource
67+
taggedUser: TaggedUser
68+
}
69+
errors: [
70+
UserNotFound
71+
]
72+
}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package software.amazon.smithy.java.codegen.generators;
7+
8+
import java.util.ArrayList;
9+
import java.util.List;
10+
import java.util.function.Consumer;
11+
import software.amazon.smithy.codegen.core.Symbol;
12+
import software.amazon.smithy.codegen.core.SymbolProvider;
13+
import software.amazon.smithy.codegen.core.directed.ShapeDirective;
14+
import software.amazon.smithy.java.codegen.CodeGenerationContext;
15+
import software.amazon.smithy.java.codegen.CodegenUtils;
16+
import software.amazon.smithy.java.codegen.JavaCodegenSettings;
17+
import software.amazon.smithy.java.codegen.sections.ClassSection;
18+
import software.amazon.smithy.java.codegen.writer.JavaWriter;
19+
import software.amazon.smithy.model.Model;
20+
import software.amazon.smithy.model.shapes.MemberShape;
21+
import software.amazon.smithy.model.shapes.Shape;
22+
import software.amazon.smithy.model.shapes.ShapeId;
23+
import software.amazon.smithy.model.shapes.StructureShape;
24+
import software.amazon.smithy.model.traits.MixinTrait;
25+
import software.amazon.smithy.utils.SmithyInternalApi;
26+
27+
/**
28+
* Generates a Java interface for a Smithy mixin shape with {@code @mixin(interface = true)}.
29+
*
30+
* <p>The generated interface contains getter method signatures for the mixin's own members
31+
* (excluding members inherited from parent interface mixins). If the mixin extends other
32+
* interface mixins, the generated interface will extend those parent interfaces.
33+
*
34+
* <p>For collection members (List/Map), a {@code hasX()} method signature is also generated.
35+
*/
36+
@SmithyInternalApi
37+
public final class MixinInterfaceGenerator<
38+
T extends ShapeDirective<? extends Shape, CodeGenerationContext, JavaCodegenSettings>>
39+
implements Consumer<T> {
40+
41+
@Override
42+
public void accept(T directive) {
43+
var shape = directive.shape();
44+
var model = directive.model();
45+
var symbolProvider = directive.symbolProvider();
46+
47+
directive.context().writerDelegator().useShapeWriter(shape, writer -> {
48+
writer.pushState(new ClassSection(shape));
49+
50+
// Compute parent interface mixin symbols for extends clause
51+
List<Symbol> parentInterfaces = new ArrayList<>();
52+
for (ShapeId mixinId : shape.getMixins()) {
53+
Shape mixinShape = model.expectShape(mixinId);
54+
if (MixinTrait.isInterfaceMixin(mixinShape)) {
55+
parentInterfaces.add(symbolProvider.toSymbol(mixinShape));
56+
}
57+
}
58+
59+
var template =
60+
"""
61+
public interface ${shape:T}${?hasParents} extends ${#parents}${value:T}${^key.last}, ${/key.last}${/parents}${/hasParents} {
62+
${getters:C|}
63+
}
64+
""";
65+
writer.putContext("shape", directive.symbol());
66+
writer.putContext("hasParents", !parentInterfaces.isEmpty());
67+
writer.putContext("parents", parentInterfaces);
68+
writer.putContext("getters",
69+
new GetterSignatureGenerator(writer, shape, symbolProvider, model, parentInterfaces));
70+
writer.write(template);
71+
72+
writer.popState();
73+
});
74+
}
75+
76+
private record GetterSignatureGenerator(
77+
JavaWriter writer,
78+
Shape shape,
79+
SymbolProvider symbolProvider,
80+
Model model,
81+
List<Symbol> parentInterfaces) implements Runnable {
82+
@Override
83+
public void run() {
84+
for (MemberShape member : shape.members()) {
85+
if (isMemberFromParentInterface(member)) {
86+
continue;
87+
}
88+
writer.pushState();
89+
var target = model.expectShape(member.getTarget());
90+
writer.putContext("member", symbolProvider.toSymbol(member));
91+
writer.putContext("getterName", CodegenUtils.toGetterName(member, model));
92+
writer.putContext("isNullable", CodegenUtils.isNullableMember(model, member));
93+
94+
if (target.isListShape() || target.isMapShape()) {
95+
var memberName = symbolProvider.toMemberName(member);
96+
writer.putContext("memberName", memberName);
97+
writer.write(
98+
"""
99+
${member:T} ${getterName:L}();
100+
101+
boolean has${memberName:U}();
102+
""");
103+
} else {
104+
writer.write(
105+
"${?isNullable}${member:B}${/isNullable}${^isNullable}${member:N}${/isNullable} ${getterName:L}();");
106+
writer.write("");
107+
}
108+
writer.popState();
109+
}
110+
}
111+
112+
private boolean isMemberFromParentInterface(MemberShape member) {
113+
for (ShapeId mixinId : shape.getMixins()) {
114+
StructureShape mixinShape = model.expectShape(mixinId, StructureShape.class);
115+
if (MixinTrait.isInterfaceMixin(mixinShape)
116+
&& mixinShape.getAllMembers().containsKey(member.getMemberName())) {
117+
return true;
118+
}
119+
}
120+
return false;
121+
}
122+
}
123+
}

codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/generators/SchemasGenerator.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
import software.amazon.smithy.model.shapes.StructureShape;
4141
import software.amazon.smithy.model.shapes.TimestampShape;
4242
import software.amazon.smithy.model.shapes.UnionShape;
43+
import software.amazon.smithy.model.traits.MixinTrait;
4344
import software.amazon.smithy.utils.SmithyInternalApi;
4445

4546
@SmithyInternalApi
@@ -299,6 +300,9 @@ public Void bigDecimalShape(BigDecimalShape bigDecimalShape) {
299300

300301
@Override
301302
public Void structureShape(StructureShape shape) {
303+
if (MixinTrait.isInterfaceMixin(shape)) {
304+
return null;
305+
}
302306
generateStructMemberSchemas(shape, "structureBuilder");
303307
return null;
304308
}

0 commit comments

Comments
 (0)