Skip to content

Add support for endpoint trait #795

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Nov 28, 2018
Merged
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 @@ -158,6 +158,7 @@ public Map<String, OperationModel> constructOperations() {
operationModel.setDocumentation(op.getDocumentation());
operationModel.setIsAuthenticated(isAuthenticated(op));
operationModel.setPaginated(isPaginated(op));
operationModel.setEndpointTrait(op.getEndpoint());

Input input = op.getInput();
if (input != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import software.amazon.awssdk.codegen.docs.OperationDocs;
import software.amazon.awssdk.codegen.docs.SimpleMethodOverload;
import software.amazon.awssdk.codegen.internal.Utils;
import software.amazon.awssdk.codegen.model.service.EndpointTrait;

public class OperationModel extends DocumentationModel {

Expand Down Expand Up @@ -50,6 +51,8 @@ public class OperationModel extends DocumentationModel {
@JsonIgnore
private ShapeModel outputShape;

private EndpointTrait endpointTrait;

public String getOperationName() {
return operationName;
}
Expand Down Expand Up @@ -189,6 +192,20 @@ public void setPaginated(boolean paginated) {
isPaginated = paginated;
}

/**
* Returns the endpoint trait that will be used to resolve the endpoint of an API.
*/
public EndpointTrait getEndpointTrait() {
return endpointTrait;
}

/**
* Sets the endpoint trait that will be used to resolve the endpoint of an API.
*/
public void setEndpointTrait(EndpointTrait endpointTrait) {
this.endpointTrait = endpointTrait;
}

/**
* @return True if the operation has an event stream member in the output shape. False otherwise.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Copyright 2010-2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/

package software.amazon.awssdk.codegen.model.service;

/**
* This trait can be used to resolve the endpoint of an API using the original endpoint
* derived from client or set by customer.
*
* This trait allows using modeled members in the input shape to modify the endpoint. This is for SDK internal use.
*
* See `API Operation Endpoint Trait` SEP
*/
public final class EndpointTrait {
/**
* Expression that must be expanded by the client before invoking the API call.
* The expanded expression is added as a prefix to the original endpoint host derived on the client.
*/
private String hostPrefix;

public String getHostPrefix() {
return hostPrefix;
}

public void setHostPrefix(String hostPrefix) {
this.hostPrefix = hostPrefix;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
* Copyright 2010-2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/

package software.amazon.awssdk.codegen.model.service;

import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import software.amazon.awssdk.utils.StringUtils;

/**
* Class to process the hostPrefix value in the {@link EndpointTrait} class.
* This is used during client generation.
*/
public final class HostPrefixProcessor {
/**
* Pattern to retrieve the content between the curly braces
*/
private static final String CURLY_BRACES_PATTERN = "\\{([^}]+)}";

private static final Pattern PATTERN = Pattern.compile(CURLY_BRACES_PATTERN);

/**
* This is the same as the {@link EndpointTrait#hostPrefix} expression with labels replaced by "%s"
*
* For example, if expression in host trait is "{Bucket}-{AccountId}-", then
* hostWithStringSpecifier will be "%s-%s-"
*/
private String hostWithStringSpecifier;

/**
* The list of member c2j names in input shape that are referenced in the host expression.
*
* For example, if expression in host trait is "{Bucket}-{AccountId}-", then the
* list would contain [Bucket, AccountId].
*/
private List<String> c2jNames;

public HostPrefixProcessor(String hostExpression) {
this.hostWithStringSpecifier = hostExpression;
this.c2jNames = new ArrayList<>();
replaceHostLabelsWithStringSpecifier(hostExpression);
}

/**
* Replace all the labels in host with %s symbols and collect the input shape member names into a list
*/
private void replaceHostLabelsWithStringSpecifier(String hostExpression) {
if (StringUtils.isEmpty(hostExpression)) {
throw new IllegalArgumentException("Given host prefix is either null or empty");
}

Matcher matcher = PATTERN.matcher(hostExpression);

while (matcher.find()) {
String matched = matcher.group(1);
c2jNames.add(matched);
hostWithStringSpecifier = hostWithStringSpecifier.replaceFirst("\\{" + matched + "}", "%s");
}
}

public String hostWithStringSpecifier() {
return hostWithStringSpecifier;
}

public List<String> c2jNames() {
return c2jNames;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ public class Operation {

private boolean requiresApiKey;

private EndpointTrait endpoint;

@JsonProperty("authtype")
private AuthType authType = AuthType.IAM;

Expand Down Expand Up @@ -135,4 +137,12 @@ public boolean requiresApiKey() {
public void setRequiresApiKey(boolean requiresApiKey) {
this.requiresApiKey = requiresApiKey;
}

public EndpointTrait getEndpoint() {
return endpoint;
}

public void setEndpoint(EndpointTrait endpoint) {
this.endpoint = endpoint;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ protected MethodSpec.Builder operationBody(MethodSpec.Builder builder, Operation
.addAnnotation(Override.class)
.beginControlFlow("try")
.addCode(ClientClassUtils.callApplySignerOverrideMethod(opModel))
.addCode(ClientClassUtils.addEndpointTraitCode(opModel))
.addCode(protocolSpec.responseHandler(model, opModel))
.addCode(protocolSpec.errorResponseHandler(opModel))
.addCode(eventToByteBufferPublisher(opModel))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,20 @@
import com.squareup.javapoet.TypeName;
import com.squareup.javapoet.TypeVariableName;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import javax.lang.model.element.Modifier;
import software.amazon.awssdk.auth.signer.EventStreamAws4Signer;
import software.amazon.awssdk.awscore.AwsRequestOverrideConfiguration;
import software.amazon.awssdk.codegen.model.intermediate.IntermediateModel;
import software.amazon.awssdk.codegen.model.intermediate.OperationModel;
import software.amazon.awssdk.codegen.model.intermediate.ShapeModel;
import software.amazon.awssdk.codegen.model.service.HostPrefixProcessor;
import software.amazon.awssdk.codegen.poet.PoetExtensions;
import software.amazon.awssdk.codegen.poet.PoetUtils;
import software.amazon.awssdk.core.ApiName;
import software.amazon.awssdk.core.signer.Signer;
import software.amazon.awssdk.core.util.VersionInfo;
import software.amazon.awssdk.utils.StringUtils;
import software.amazon.awssdk.utils.Validate;

final class ClientClassUtils {
Expand Down Expand Up @@ -161,4 +164,44 @@ static CodeBlock callApplySignerOverrideMethod(OperationModel opModel) {

return code.build();
}

static CodeBlock addEndpointTraitCode(OperationModel opModel) {
CodeBlock.Builder builder = CodeBlock.builder();

if (opModel.getEndpointTrait() != null && !StringUtils.isEmpty(opModel.getEndpointTrait().getHostPrefix())) {
String hostPrefix = opModel.getEndpointTrait().getHostPrefix();
HostPrefixProcessor processor = new HostPrefixProcessor(hostPrefix);

builder.addStatement("String hostPrefix = $S", hostPrefix);

if (processor.c2jNames().isEmpty()) {
builder.addStatement("String resolvedHostExpression = $S", processor.hostWithStringSpecifier());
} else {
processor.c2jNames()
.forEach(name -> builder.addStatement("$T.paramNotBlank($L, $S)", Validate.class,
inputShapeMemberGetter(opModel, name),
name));

builder.addStatement("String resolvedHostExpression = String.format($S, $L)",
processor.hostWithStringSpecifier(),
processor.c2jNames().stream()
.map(n -> inputShapeMemberGetter(opModel, n))
.collect(Collectors.joining(",")));
}
}

return builder.build();
}

/**
* Given operation and c2j name, returns the String that represents calling the
* c2j member's getter method in the opmodel input shape.
*
* For example, Operation is CreateConnection and c2j name is CatalogId,
* returns "createConnectionRequest.catalogId()"
*/
private static String inputShapeMemberGetter(OperationModel opModel, String c2jName) {
return opModel.getInput().getVariableName() + "." +
opModel.getInputShape().getMemberByC2jName(c2jName).getFluentGetterMethodName() + "()";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ private List<MethodSpec> operationMethodSpecs(OperationModel opModel) {
methods.add(SyncClientInterface.operationMethodSignature(model, opModel)
.addAnnotation(Override.class)
.addCode(ClientClassUtils.callApplySignerOverrideMethod(opModel))
.addCode(ClientClassUtils.addEndpointTraitCode(opModel))
.addCode(protocolSpec.responseHandler(model, opModel))
.addCode(protocolSpec.errorResponseHandler(opModel))
.addCode(protocolSpec.executionHandler(opModel))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ public CodeBlock executionHandler(OperationModel opModel) {
.add("\n\nreturn clientHandler.execute(new $T<$T, $T>()\n" +
".withResponseHandler($N)\n" +
".withErrorResponseHandler($N)\n" +
hostPrefixExpression(opModel) +
".withInput($L)\n",
ClientExecutionParams.class,
requestType,
Expand Down Expand Up @@ -232,6 +233,7 @@ public CodeBlock asyncExecutionHandler(IntermediateModel intermediateModel, Oper
"$L" +
".withResponseHandler($L)\n" +
".withErrorResponseHandler(errorResponseHandler)\n" +
hostPrefixExpression(opModel) +
asyncRequestBody +
".withInput($L)$L)$L;",
// If the operation has an event stream output we use a different future so we don't return the one
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import software.amazon.awssdk.codegen.poet.PoetExtensions;
import software.amazon.awssdk.core.client.handler.SyncClientHandler;
import software.amazon.awssdk.protocols.core.ExceptionMetadata;
import software.amazon.awssdk.utils.StringUtils;

public interface ProtocolSpec {

Expand Down Expand Up @@ -78,4 +79,10 @@ default String populateHttpStatusCode(ShapeModel shapeModel) {
return shapeModel.getHttpStatusCode() != null
? String.format(".httpStatusCode(%d)", shapeModel.getHttpStatusCode()) : "";
}

default String hostPrefixExpression(OperationModel opModel) {
return opModel.getEndpointTrait() != null && !StringUtils.isEmpty(opModel.getEndpointTrait().getHostPrefix())
? ".hostPrefixExpression(resolvedHostExpression)\n"
: "";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ public CodeBlock executionHandler(OperationModel opModel) {
.add("\n\nreturn clientHandler.execute(new $T<$T, $T>()" +
".withResponseHandler($N)" +
".withErrorResponseHandler($N)" +
hostPrefixExpression(opModel) +
".withInput($L)",
ClientExecutionParams.class,
requestType,
Expand Down Expand Up @@ -128,6 +129,7 @@ public CodeBlock asyncExecutionHandler(IntermediateModel intermediateModel, Oper
".withMarshaller(new $T(protocolFactory))" +
".withResponseHandler(responseHandler)" +
".withErrorResponseHandler($N)\n" +
hostPrefixExpression(opModel) +
asyncRequestBody +
".withInput($L) $L);",
ClientExecutionParams.class,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* Copyright 2010-2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/

package software.amazon.awssdk.codegen.model.service;

import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.empty;

import org.junit.Test;

public class HostPrefixProcessorTest {

@Test
public void staticHostLabel() {
String hostPrefix = "data-";

HostPrefixProcessor processor = new HostPrefixProcessor(hostPrefix);
assertThat(processor.hostWithStringSpecifier(), equalTo("data-"));
assertThat(processor.c2jNames(), empty());
}

@Test
public void inputShapeLabels() {
String hostPrefix = "{Bucket}-{AccountId}.";

HostPrefixProcessor processor = new HostPrefixProcessor(hostPrefix);
assertThat(processor.hostWithStringSpecifier(), equalTo("%s-%s."));
assertThat(processor.c2jNames(), contains("Bucket", "AccountId"));
}

@Test
public void emptyCurlyBraces() {
// Pattern should not match the first set of curly braces as there is no characters between them
String host = "{}.foo";

HostPrefixProcessor processor = new HostPrefixProcessor(host);
assertThat(processor.hostWithStringSpecifier(), equalTo("{}.foo"));
assertThat(processor.c2jNames(), empty());
}

@Test (expected = IllegalArgumentException.class)
public void emptyHost() {
new HostPrefixProcessor("");
}

@Test (expected = IllegalArgumentException.class)
public void nullHost() {
new HostPrefixProcessor(null);
}
}
Loading