Skip to content

Commit bad6d30

Browse files
committed
Add support for endpoint trait
1 parent a563959 commit bad6d30

File tree

27 files changed

+606
-7
lines changed

27 files changed

+606
-7
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
@@ -170,6 +170,7 @@ protected MethodSpec.Builder operationBody(MethodSpec.Builder builder, Operation
170170
.addAnnotation(Override.class)
171171
.beginControlFlow("try")
172172
.addCode(ClientClassUtils.callApplySignerOverrideMethod(opModel))
173+
.addCode(ClientClassUtils.addEndpointTraitCode(opModel))
173174
.addCode(getCustomResponseHandler(opModel, returnType)
174175
.orElseGet(() -> protocolSpec.responseHandler(model, opModel)))
175176
.addCode(protocolSpec.errorResponseHandler(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
@@ -24,18 +24,21 @@
2424
import com.squareup.javapoet.TypeVariableName;
2525
import java.util.Optional;
2626
import java.util.function.Consumer;
27+
import java.util.stream.Collectors;
2728
import javax.lang.model.element.Modifier;
2829
import software.amazon.awssdk.auth.signer.EventStreamAws4Signer;
2930
import software.amazon.awssdk.awscore.AwsRequestOverrideConfiguration;
3031
import software.amazon.awssdk.codegen.model.intermediate.IntermediateModel;
3132
import software.amazon.awssdk.codegen.model.intermediate.OperationModel;
3233
import software.amazon.awssdk.codegen.model.intermediate.ShapeModel;
34+
import software.amazon.awssdk.codegen.model.service.HostPrefixProcessor;
3335
import software.amazon.awssdk.codegen.poet.PoetExtensions;
3436
import software.amazon.awssdk.codegen.poet.PoetUtils;
3537
import software.amazon.awssdk.core.ApiName;
3638
import software.amazon.awssdk.core.http.HttpResponseHandler;
3739
import software.amazon.awssdk.core.signer.Signer;
3840
import software.amazon.awssdk.core.util.VersionInfo;
41+
import software.amazon.awssdk.utils.StringUtils;
3942
import software.amazon.awssdk.utils.Validate;
4043

4144
final class ClientClassUtils {
@@ -179,4 +182,44 @@ static CodeBlock callApplySignerOverrideMethod(OperationModel opModel) {
179182

180183
return code.build();
181184
}
185+
186+
static CodeBlock addEndpointTraitCode(OperationModel opModel) {
187+
CodeBlock.Builder builder = CodeBlock.builder();
188+
189+
if (opModel.getEndpointTrait() != null && !StringUtils.isEmpty(opModel.getEndpointTrait().getHostPrefix())) {
190+
String hostPrefix = opModel.getEndpointTrait().getHostPrefix();
191+
HostPrefixProcessor processor = new HostPrefixProcessor(hostPrefix);
192+
193+
builder.addStatement("String hostPrefix = $S", hostPrefix);
194+
195+
if (processor.c2jNames().isEmpty()) {
196+
builder.addStatement("String resolvedHostExpression = $S", processor.hostWithStringSpecifier());
197+
} else {
198+
processor.c2jNames()
199+
.forEach(name -> builder.addStatement("$T.paramNotBlank($L, $S)", Validate.class,
200+
inputShapeMemberGetter(opModel, name),
201+
name));
202+
203+
builder.addStatement("String resolvedHostExpression = String.format($S, $L)",
204+
processor.hostWithStringSpecifier(),
205+
processor.c2jNames().stream()
206+
.map(n -> inputShapeMemberGetter(opModel, n))
207+
.collect(Collectors.joining(",")));
208+
}
209+
}
210+
211+
return builder.build();
212+
}
213+
214+
/**
215+
* Given operation and c2j name, returns the String that represents calling the
216+
* c2j member's getter method in the opmodel input shape.
217+
*
218+
* For example, Operation is CreateConnection and c2j name is CatalogId,
219+
* returns "createConnectionRequest.catalogId()"
220+
*/
221+
private static String inputShapeMemberGetter(OperationModel opModel, String c2jName) {
222+
return opModel.getInput().getVariableName() + "." +
223+
opModel.getInputShape().getMemberByC2jName(c2jName).getFluentGetterMethodName() + "()";
224+
}
182225
}

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
@@ -146,6 +146,7 @@ private List<MethodSpec> operationMethodSpecs(OperationModel opModel) {
146146
methods.add(SyncClientInterface.operationMethodSignature(model, opModel)
147147
.addAnnotation(Override.class)
148148
.addCode(ClientClassUtils.callApplySignerOverrideMethod(opModel))
149+
.addCode(ClientClassUtils.addEndpointTraitCode(opModel))
149150
.addCode(getCustomResponseHandler(opModel, returnType)
150151
.orElseGet(() -> protocolSpec.responseHandler(model, opModel)))
151152
.addCode(protocolSpec.errorResponseHandler(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
@@ -161,6 +161,7 @@ public CodeBlock executionHandler(OperationModel opModel) {
161161
.add("\n\nreturn clientHandler.execute(new $T<$T, $T>()\n" +
162162
".withResponseHandler($N)\n" +
163163
".withErrorResponseHandler($N)\n" +
164+
hostPrefixExpression(opModel) +
164165
".withInput($L)\n",
165166
ClientExecutionParams.class,
166167
requestType,
@@ -227,6 +228,7 @@ public CodeBlock asyncExecutionHandler(IntermediateModel intermediateModel, Oper
227228
"$L" +
228229
".withResponseHandler($L)\n" +
229230
".withErrorResponseHandler(errorResponseHandler)\n" +
231+
hostPrefixExpression(opModel) +
230232
asyncRequestBody +
231233
".withInput($L)$L)$L;",
232234
// 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
@@ -25,6 +25,7 @@
2525
import software.amazon.awssdk.codegen.model.intermediate.IntermediateModel;
2626
import software.amazon.awssdk.codegen.model.intermediate.OperationModel;
2727
import software.amazon.awssdk.core.client.handler.SyncClientHandler;
28+
import software.amazon.awssdk.utils.StringUtils;
2829

2930
public interface ProtocolSpec {
3031

@@ -60,4 +61,10 @@ default List<MethodSpec> additionalMethods() {
6061
default List<FieldSpec> additionalFields() {
6162
return new ArrayList<>();
6263
}
64+
65+
default String hostPrefixExpression(OperationModel opModel) {
66+
return opModel.getEndpointTrait() != null && !StringUtils.isEmpty(opModel.getEndpointTrait().getHostPrefix())
67+
? ".hostPrefixExpression(resolvedHostExpression)\n"
68+
: "";
69+
}
6370
}

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
@@ -130,6 +130,7 @@ public CodeBlock executionHandler(OperationModel opModel) {
130130
.add("\n\nreturn clientHandler.execute(new $T<$T, $T>()" +
131131
".withResponseHandler($N)" +
132132
".withErrorResponseHandler($N)" +
133+
hostPrefixExpression(opModel) +
133134
".withInput($L)",
134135
ClientExecutionParams.class,
135136
requestType,
@@ -159,6 +160,7 @@ public CodeBlock asyncExecutionHandler(IntermediateModel intermediateModel, Oper
159160
".withMarshaller(new $T(protocolFactory))" +
160161
".withResponseHandler(responseHandler)" +
161162
".withErrorResponseHandler($N)\n" +
163+
hostPrefixExpression(opModel) +
162164
asyncRequestBody +
163165
".withInput($L) $L);",
164166
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)