Skip to content

Commit 8dde988

Browse files
committed
Add support for endpoint trait
1 parent fee061b commit 8dde988

File tree

27 files changed

+655
-4
lines changed

27 files changed

+655
-4
lines changed

codegen/src/main/java/software/amazon/awssdk/codegen/AddOperations.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ public Map<String, OperationModel> constructOperations() {
155155
operationModel.setDocumentation(op.getDocumentation());
156156
operationModel.setIsAuthenticated(isAuthenticated(op));
157157
operationModel.setPaginated(isPaginated(op));
158+
operationModel.setEndpointTrait(op.getEndpoint());
158159

159160
Input input = op.getInput();
160161
if (input != null) {

codegen/src/main/java/software/amazon/awssdk/codegen/model/intermediate/OperationModel.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import software.amazon.awssdk.codegen.docs.OperationDocs;
2424
import software.amazon.awssdk.codegen.docs.SimpleMethodOverload;
2525
import software.amazon.awssdk.codegen.internal.Utils;
26+
import software.amazon.awssdk.codegen.model.service.EndpointTrait;
2627

2728
public class OperationModel extends DocumentationModel {
2829

@@ -50,6 +51,8 @@ public class OperationModel extends DocumentationModel {
5051
@JsonIgnore
5152
private ShapeModel outputShape;
5253

54+
private EndpointTrait endpointTrait;
55+
5356
public String getOperationName() {
5457
return operationName;
5558
}
@@ -189,6 +192,20 @@ public void setPaginated(boolean paginated) {
189192
isPaginated = paginated;
190193
}
191194

195+
/**
196+
* Returns the endpoint trait that will be used to resolve the endpoint of an API.
197+
*/
198+
public EndpointTrait getEndpointTrait() {
199+
return endpointTrait;
200+
}
201+
202+
/**
203+
* Sets the endpoint trait that will be used to resolve the endpoint of an API.
204+
*/
205+
public void setEndpointTrait(EndpointTrait endpointTrait) {
206+
this.endpointTrait = endpointTrait;
207+
}
208+
192209
/**
193210
* @return True if the operation has an event stream member in the output shape. False otherwise.
194211
*/
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* Copyright 2010-2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package software.amazon.awssdk.codegen.model.service;
17+
18+
/**
19+
* This trait can be used to resolve the endpoint of an API using the original endpoint
20+
* derived from client or set by customer.
21+
*
22+
* This trait allows using modeled members in the input shape to modify the endpoint. This is for SDK internal use.
23+
*
24+
* See `API Operation Endpoint Trait` SEP
25+
*/
26+
public final class EndpointTrait {
27+
/**
28+
* Expression that must be expanded by the client before invoking the API call.
29+
* The expanded expression is added as a prefix to the original endpoint host derived on the client.
30+
*/
31+
private String hostPrefix;
32+
33+
public String getHostPrefix() {
34+
return hostPrefix;
35+
}
36+
37+
public void setHostPrefix(String hostPrefix) {
38+
this.hostPrefix = hostPrefix;
39+
}
40+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/*
2+
* Copyright 2010-2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package software.amazon.awssdk.codegen.model.service;
17+
18+
import java.util.ArrayList;
19+
import java.util.List;
20+
import java.util.regex.Matcher;
21+
import java.util.regex.Pattern;
22+
import software.amazon.awssdk.utils.StringUtils;
23+
24+
/**
25+
* Class to process the hostPrefix value in the {@link EndpointTrait} class.
26+
* This is used during client generation.
27+
*/
28+
public final class HostPrefixProcessor {
29+
/**
30+
* Pattern to retrieve the content between the curly braces
31+
*/
32+
private static final String CURLY_BRACES_PATTERN = "\\{([^}]+)}";
33+
34+
private static final Pattern PATTERN = Pattern.compile(CURLY_BRACES_PATTERN);
35+
36+
/**
37+
* This is the same as the {@link EndpointTrait#hostPrefix} expression with labels replaced by "%s"
38+
*
39+
* For example, if expression in host trait is "{Bucket}-{AccountId}-", then
40+
* hostWithStringSpecifier will be "%s-%s-"
41+
*/
42+
private String hostWithStringSpecifier;
43+
44+
/**
45+
* The list of member c2j names in input shape that are referenced in the host expression.
46+
*
47+
* For example, if expression in host trait is "{Bucket}-{AccountId}-", then the
48+
* list would contain [Bucket, AccountId].
49+
*/
50+
private List<String> c2jNames;
51+
52+
public HostPrefixProcessor(String hostExpression) {
53+
this.hostWithStringSpecifier = hostExpression;
54+
this.c2jNames = new ArrayList<>();
55+
replaceHostLabelsWithStringSpecifier(hostExpression);
56+
}
57+
58+
/**
59+
* Replace all the labels in host with %s symbols and collect the input shape member names into a list
60+
*/
61+
private void replaceHostLabelsWithStringSpecifier(String hostExpression) {
62+
if (StringUtils.isEmpty(hostExpression)) {
63+
throw new IllegalArgumentException("Given host prefix is either null or empty");
64+
}
65+
66+
Matcher matcher = PATTERN.matcher(hostExpression);
67+
68+
while (matcher.find()) {
69+
String matched = matcher.group(1);
70+
c2jNames.add(matched);
71+
hostWithStringSpecifier = hostWithStringSpecifier.replaceFirst("\\{" + matched + "}", "%s");
72+
}
73+
}
74+
75+
public String hostWithStringSpecifier() {
76+
return hostWithStringSpecifier;
77+
}
78+
79+
public List<String> c2jNames() {
80+
return c2jNames;
81+
}
82+
}

codegen/src/main/java/software/amazon/awssdk/codegen/model/service/Operation.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ public class Operation {
3838

3939
private boolean requiresApiKey;
4040

41+
private EndpointTrait endpoint;
42+
4143
@JsonProperty("authtype")
4244
private AuthType authType = AuthType.IAM;
4345

@@ -135,4 +137,12 @@ public boolean requiresApiKey() {
135137
public void setRequiresApiKey(boolean requiresApiKey) {
136138
this.requiresApiKey = requiresApiKey;
137139
}
140+
141+
public EndpointTrait getEndpoint() {
142+
return endpoint;
143+
}
144+
145+
public void setEndpoint(EndpointTrait endpoint) {
146+
this.endpoint = endpoint;
147+
}
138148
}

codegen/src/main/java/software/amazon/awssdk/codegen/poet/client/AsyncClientClass.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ protected MethodSpec.Builder operationBody(MethodSpec.Builder builder, Operation
169169
.addAnnotation(Override.class)
170170
.beginControlFlow("try")
171171
.addCode(ClientClassUtils.callApplySignerOverrideMethod(opModel))
172+
.addCode(ClientClassUtils.addEndpointTraitCode(opModel))
172173
.addCode(protocolSpec.responseHandler(model, opModel))
173174
.addCode(protocolSpec.errorResponseHandler(opModel))
174175
.addCode(eventToByteBufferPublisher(opModel))

codegen/src/main/java/software/amazon/awssdk/codegen/poet/client/ClientClassUtils.java

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,20 @@
2323
import com.squareup.javapoet.TypeName;
2424
import com.squareup.javapoet.TypeVariableName;
2525
import java.util.function.Consumer;
26+
import java.util.stream.Collectors;
2627
import javax.lang.model.element.Modifier;
2728
import software.amazon.awssdk.auth.signer.EventStreamAws4Signer;
2829
import software.amazon.awssdk.awscore.AwsRequestOverrideConfiguration;
2930
import software.amazon.awssdk.codegen.model.intermediate.IntermediateModel;
3031
import software.amazon.awssdk.codegen.model.intermediate.OperationModel;
3132
import software.amazon.awssdk.codegen.model.intermediate.ShapeModel;
33+
import software.amazon.awssdk.codegen.model.service.HostPrefixProcessor;
3234
import software.amazon.awssdk.codegen.poet.PoetExtensions;
3335
import software.amazon.awssdk.codegen.poet.PoetUtils;
3436
import software.amazon.awssdk.core.ApiName;
3537
import software.amazon.awssdk.core.signer.Signer;
3638
import software.amazon.awssdk.core.util.VersionInfo;
39+
import software.amazon.awssdk.utils.StringUtils;
3740
import software.amazon.awssdk.utils.Validate;
3841

3942
final class ClientClassUtils {
@@ -161,4 +164,44 @@ static CodeBlock callApplySignerOverrideMethod(OperationModel opModel) {
161164

162165
return code.build();
163166
}
167+
168+
static CodeBlock addEndpointTraitCode(OperationModel opModel) {
169+
CodeBlock.Builder builder = CodeBlock.builder();
170+
171+
if (opModel.getEndpointTrait() != null && !StringUtils.isEmpty(opModel.getEndpointTrait().getHostPrefix())) {
172+
String hostPrefix = opModel.getEndpointTrait().getHostPrefix();
173+
HostPrefixProcessor processor = new HostPrefixProcessor(hostPrefix);
174+
175+
builder.addStatement("String hostPrefix = $S", hostPrefix);
176+
177+
if (processor.c2jNames().isEmpty()) {
178+
builder.addStatement("String resolvedHostExpression = $S", processor.hostWithStringSpecifier());
179+
} else {
180+
processor.c2jNames()
181+
.forEach(name -> builder.addStatement("$T.paramNotBlank($L, $S)", Validate.class,
182+
inputShapeMemberGetter(opModel, name),
183+
name));
184+
185+
builder.addStatement("String resolvedHostExpression = String.format($S, $L)",
186+
processor.hostWithStringSpecifier(),
187+
processor.c2jNames().stream()
188+
.map(n -> inputShapeMemberGetter(opModel, n))
189+
.collect(Collectors.joining(",")));
190+
}
191+
}
192+
193+
return builder.build();
194+
}
195+
196+
/**
197+
* Given operation and c2j name, returns the String that represents calling the
198+
* c2j member's getter method in the opmodel input shape.
199+
*
200+
* For example, Operation is CreateConnection and c2j name is CatalogId,
201+
* returns "createConnectionRequest.catalogId()"
202+
*/
203+
private static String inputShapeMemberGetter(OperationModel opModel, String c2jName) {
204+
return opModel.getInput().getVariableName() + "." +
205+
opModel.getInputShape().getMemberByC2jName(c2jName).getFluentGetterMethodName() + "()";
206+
}
164207
}

codegen/src/main/java/software/amazon/awssdk/codegen/poet/client/SyncClientClass.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ private List<MethodSpec> operationMethodSpecs(OperationModel opModel) {
143143
methods.add(SyncClientInterface.operationMethodSignature(model, opModel)
144144
.addAnnotation(Override.class)
145145
.addCode(ClientClassUtils.callApplySignerOverrideMethod(opModel))
146+
.addCode(ClientClassUtils.addEndpointTraitCode(opModel))
146147
.addCode(protocolSpec.responseHandler(model, opModel))
147148
.addCode(protocolSpec.errorResponseHandler(opModel))
148149
.addCode(protocolSpec.executionHandler(opModel))

codegen/src/main/java/software/amazon/awssdk/codegen/poet/client/specs/JsonProtocolSpec.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@ public CodeBlock executionHandler(OperationModel opModel) {
165165
.add("\n\nreturn clientHandler.execute(new $T<$T, $T>()\n" +
166166
".withResponseHandler($N)\n" +
167167
".withErrorResponseHandler($N)\n" +
168+
hostPrefixExpression(opModel) +
168169
".withInput($L)\n",
169170
ClientExecutionParams.class,
170171
requestType,
@@ -231,6 +232,7 @@ public CodeBlock asyncExecutionHandler(IntermediateModel intermediateModel, Oper
231232
"$L" +
232233
".withResponseHandler($L)\n" +
233234
".withErrorResponseHandler(errorResponseHandler)\n" +
235+
hostPrefixExpression(opModel) +
234236
asyncRequestBody +
235237
".withInput($L)$L)$L;",
236238
// If the operation has an event stream output we use a different future so we don't return the one

codegen/src/main/java/software/amazon/awssdk/codegen/poet/client/specs/ProtocolSpec.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import software.amazon.awssdk.codegen.poet.PoetExtensions;
3131
import software.amazon.awssdk.core.client.handler.SyncClientHandler;
3232
import software.amazon.awssdk.protocols.core.ExceptionMetadata;
33+
import software.amazon.awssdk.utils.StringUtils;
3334

3435
public interface ProtocolSpec {
3536

@@ -78,4 +79,10 @@ default String populateHttpStatusCode(ShapeModel shapeModel) {
7879
return shapeModel.getHttpStatusCode() != null
7980
? String.format(".httpStatusCode(%d)", shapeModel.getHttpStatusCode()) : "";
8081
}
82+
83+
default String hostPrefixExpression(OperationModel opModel) {
84+
return opModel.getEndpointTrait() != null && !StringUtils.isEmpty(opModel.getEndpointTrait().getHostPrefix())
85+
? ".hostPrefixExpression(resolvedHostExpression)\n"
86+
: "";
87+
}
8188
}

codegen/src/main/java/software/amazon/awssdk/codegen/poet/client/specs/QueryProtocolSpec.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ public CodeBlock executionHandler(OperationModel opModel) {
9898
.add("\n\nreturn clientHandler.execute(new $T<$T, $T>()" +
9999
".withResponseHandler($N)" +
100100
".withErrorResponseHandler($N)" +
101+
hostPrefixExpression(opModel) +
101102
".withInput($L)",
102103
ClientExecutionParams.class,
103104
requestType,
@@ -127,6 +128,7 @@ public CodeBlock asyncExecutionHandler(IntermediateModel intermediateModel, Oper
127128
".withMarshaller(new $T(protocolFactory))" +
128129
".withResponseHandler(responseHandler)" +
129130
".withErrorResponseHandler($N)\n" +
131+
hostPrefixExpression(opModel) +
130132
asyncRequestBody +
131133
".withInput($L) $L);",
132134
ClientExecutionParams.class,
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
* Copyright 2010-2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package software.amazon.awssdk.codegen.model.service;
17+
18+
import static org.hamcrest.CoreMatchers.equalTo;
19+
import static org.hamcrest.MatcherAssert.assertThat;
20+
import static org.hamcrest.Matchers.contains;
21+
import static org.hamcrest.Matchers.empty;
22+
23+
import org.junit.Test;
24+
25+
public class HostPrefixProcessorTest {
26+
27+
@Test
28+
public void staticHostLabel() {
29+
String hostPrefix = "data-";
30+
31+
HostPrefixProcessor processor = new HostPrefixProcessor(hostPrefix);
32+
assertThat(processor.hostWithStringSpecifier(), equalTo("data-"));
33+
assertThat(processor.c2jNames(), empty());
34+
}
35+
36+
@Test
37+
public void inputShapeLabels() {
38+
String hostPrefix = "{Bucket}-{AccountId}.";
39+
40+
HostPrefixProcessor processor = new HostPrefixProcessor(hostPrefix);
41+
assertThat(processor.hostWithStringSpecifier(), equalTo("%s-%s."));
42+
assertThat(processor.c2jNames(), contains("Bucket", "AccountId"));
43+
}
44+
45+
@Test
46+
public void emptyCurlyBraces() {
47+
// Pattern should not match the first set of curly braces as there is no characters between them
48+
String host = "{}.foo";
49+
50+
HostPrefixProcessor processor = new HostPrefixProcessor(host);
51+
assertThat(processor.hostWithStringSpecifier(), equalTo("{}.foo"));
52+
assertThat(processor.c2jNames(), empty());
53+
}
54+
55+
@Test (expected = IllegalArgumentException.class)
56+
public void emptyHost() {
57+
new HostPrefixProcessor("");
58+
}
59+
60+
@Test (expected = IllegalArgumentException.class)
61+
public void nullHost() {
62+
new HostPrefixProcessor(null);
63+
}
64+
}

0 commit comments

Comments
 (0)