Initial code commit
This commit is contained in:
parent
bfb8e48368
commit
f66101301e
|
@ -0,0 +1,31 @@
|
|||
# doc builds
|
||||
docs/smithy.egg-info
|
||||
docs/dist
|
||||
*.pyc
|
||||
|
||||
# Eclipse
|
||||
.classpath
|
||||
.project
|
||||
.settings/
|
||||
|
||||
# Intellij
|
||||
.idea/
|
||||
*.iml
|
||||
*.iws
|
||||
|
||||
# Mac
|
||||
.DS_Store
|
||||
|
||||
# Maven
|
||||
target/
|
||||
**/dependency-reduced-pom.xml
|
||||
|
||||
# Gradle
|
||||
/.gradle
|
||||
build/
|
||||
!smithy-build/src/main/java/software/amazon/smithy/build
|
||||
!smithy-build/src/main/resources/software/amazon/smithy/build
|
||||
!smithy-build/src/test/java/software/amazon/smithy/build
|
||||
!smithy-build/src/test/resources/software/amazon/smithy/build
|
||||
*/out/
|
||||
*/*/out/
|
|
@ -0,0 +1,3 @@
|
|||
# Smithy Changelog
|
||||
|
||||
## Next-Release TBD
|
28
NOTICE
28
NOTICE
|
@ -1,2 +1,28 @@
|
|||
Smithy
|
||||
Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
|
||||
**********************
|
||||
THIRD PARTY COMPONENTS
|
||||
**********************
|
||||
|
||||
This software includes third party software subject to the following copyrights.
|
||||
|
||||
Apache Commons Text
|
||||
Copyright 2014-2018 The Apache Software Foundation
|
||||
|
||||
This product includes software developed at
|
||||
The Apache Software Foundation (http://www.apache.org/).
|
||||
|
||||
|
||||
Apache Commons Text
|
||||
Copyright 2014-2018 The Apache Software Foundation
|
||||
|
||||
This product includes software developed at
|
||||
The Apache Software Foundation (http://www.apache.org/).
|
||||
|
||||
|
||||
Elephant Bird (toSnakeCase method in the codegen module)
|
||||
Copyright 2014-2018 The Apache Software Foundation
|
||||
|
||||
This product includes software developed at
|
||||
The Apache Software Foundation (http://www.apache.org/).
|
||||
|
|
10
README.md
10
README.md
|
@ -1,7 +1,9 @@
|
|||
## Smithy
|
||||
# Smithy
|
||||
|
||||
Smithy is a protocol-agnostic interface definition language and set of tools for generating clients, servers, and documentation for any programming language.
|
||||
Smithy is a protocol-agnostic interface definition language and set of tools
|
||||
for generating clients, servers, and documentation for any programming
|
||||
language.
|
||||
|
||||
## License
|
||||
# License
|
||||
|
||||
This library is licensed under the Apache 2.0 License.
|
||||
This library is licensed under the Apache 2.0 License.
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* Copyright 2019 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.
|
||||
*/
|
||||
|
||||
dependencies {
|
||||
api(project(":smithy-model"))
|
||||
api(project(":smithy-openapi"))
|
||||
api(project(":smithy-aws-traits"))
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* Copyright 2019 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.
|
||||
*/
|
||||
|
||||
import software.amazon.smithy.aws.apigateway.openapi.AddApiKeySource;
|
||||
import software.amazon.smithy.aws.apigateway.openapi.AddAuthorizers;
|
||||
import software.amazon.smithy.aws.apigateway.openapi.AddBinaryTypes;
|
||||
import software.amazon.smithy.aws.apigateway.openapi.AddRequestValidators;
|
||||
import software.amazon.smithy.openapi.fromsmithy.SmithyOpenApiPlugin;
|
||||
|
||||
module software.amazon.smithy.aws.apigateway.openapi {
|
||||
requires java.logging;
|
||||
requires software.amazon.smithy.model;
|
||||
requires software.amazon.smithy.aws.traits;
|
||||
requires software.amazon.smithy.openapi;
|
||||
|
||||
provides SmithyOpenApiPlugin with
|
||||
AddAuthorizers,
|
||||
AddRequestValidators,
|
||||
AddBinaryTypes,
|
||||
AddApiKeySource;
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* Copyright 2019 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.smithy.aws.apigateway.openapi;
|
||||
|
||||
import java.util.logging.Logger;
|
||||
import software.amazon.smithy.aws.traits.apigateway.ApiKeySourceTrait;
|
||||
import software.amazon.smithy.openapi.fromsmithy.Context;
|
||||
import software.amazon.smithy.openapi.fromsmithy.SmithyOpenApiPlugin;
|
||||
import software.amazon.smithy.openapi.model.OpenApi;
|
||||
|
||||
public final class AddApiKeySource implements SmithyOpenApiPlugin {
|
||||
private static final String EXTENSION_NAME = "x-amazon-apigateway-api-key-source";
|
||||
private static final Logger LOGGER = Logger.getLogger(AddApiKeySource.class.getName());
|
||||
|
||||
@Override
|
||||
public OpenApi after(Context context, OpenApi openApi) {
|
||||
return context.getService().getTrait(ApiKeySourceTrait.class)
|
||||
.map(trait -> {
|
||||
LOGGER.fine(() -> String.format(
|
||||
"Adding %s trait to %s", EXTENSION_NAME, context.getService().getId()));
|
||||
return openApi.toBuilder().putExtension(EXTENSION_NAME, trait.getValue()).build();
|
||||
})
|
||||
.orElse(openApi);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
* Copyright 2019 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.smithy.aws.apigateway.openapi;
|
||||
|
||||
import java.util.logging.Logger;
|
||||
import software.amazon.smithy.aws.traits.apigateway.AuthorizersTrait;
|
||||
import software.amazon.smithy.openapi.fromsmithy.Context;
|
||||
import software.amazon.smithy.openapi.fromsmithy.SmithyOpenApiPlugin;
|
||||
import software.amazon.smithy.openapi.model.SecurityScheme;
|
||||
|
||||
/**
|
||||
* Adds API Gateway authorizers to their corresponding security schemes.
|
||||
*
|
||||
* <p>The {@link AuthorizersTrait} is applied to a service shape to define
|
||||
* a custom API Gateway authorizer. This trait is a map of authentication
|
||||
* scheme name to the authorizer definition. This plugin finds each
|
||||
* authorizer and applies the API Gateway specific OpenAPI extension
|
||||
* {@code x-amazon-apigateway-authorizer} to define the authorizer in the
|
||||
* OpenAPI security scheme that corresponds to the referenced authentication
|
||||
* scheme.
|
||||
*/
|
||||
public final class AddAuthorizers implements SmithyOpenApiPlugin {
|
||||
private static final String EXTENSION_NAME = "x-amazon-apigateway-authorizer";
|
||||
private static final String CLIENT_EXTENSION_NAME = "x-amazon-apigateway-authtype";
|
||||
private static final Logger LOGGER = Logger.getLogger(AddApiKeySource.class.getName());
|
||||
|
||||
@Override
|
||||
public SecurityScheme updateSecurityScheme(
|
||||
Context context,
|
||||
String authName,
|
||||
String securitySchemeName,
|
||||
SecurityScheme securityScheme
|
||||
) {
|
||||
return context.getService().getTrait(AuthorizersTrait.class)
|
||||
.map(trait -> addAuthorizers(authName, securitySchemeName, securityScheme, trait))
|
||||
.orElse(securityScheme);
|
||||
}
|
||||
|
||||
private SecurityScheme addAuthorizers(
|
||||
String authName,
|
||||
String securitySchemeName,
|
||||
SecurityScheme securityScheme,
|
||||
AuthorizersTrait trait
|
||||
) {
|
||||
if (!trait.getAllAuthorizers().containsKey(authName)) {
|
||||
return securityScheme;
|
||||
}
|
||||
|
||||
var authorizer = trait.getAllAuthorizers().get(authName);
|
||||
LOGGER.fine(() -> String.format(
|
||||
"Adding the `%s` OpenAPI extension to the `%s` security scheme based on the `%s` trait for the "
|
||||
+ "`%s` authentication scheme.",
|
||||
EXTENSION_NAME, securitySchemeName, trait.getName(), authName));
|
||||
|
||||
var builder = securityScheme.toBuilder();
|
||||
authorizer.getClientType().ifPresent(type -> builder.putExtension(CLIENT_EXTENSION_NAME, type));
|
||||
var authorizerNode = authorizer.toNode().expectObjectNode().withoutMember("clientType");
|
||||
builder.putExtension(EXTENSION_NAME, authorizerNode);
|
||||
return builder.build();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
/*
|
||||
* Copyright 2019 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.smithy.aws.apigateway.openapi;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.logging.Logger;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
import software.amazon.smithy.model.knowledge.HttpBindingIndex;
|
||||
import software.amazon.smithy.model.knowledge.TopDownIndex;
|
||||
import software.amazon.smithy.model.node.ArrayNode;
|
||||
import software.amazon.smithy.model.node.Node;
|
||||
import software.amazon.smithy.model.shapes.ShapeIndex;
|
||||
import software.amazon.smithy.model.traits.MediaTypeTrait;
|
||||
import software.amazon.smithy.openapi.fromsmithy.Context;
|
||||
import software.amazon.smithy.openapi.fromsmithy.SmithyOpenApiPlugin;
|
||||
import software.amazon.smithy.openapi.model.OpenApi;
|
||||
|
||||
/**
|
||||
* Adds API Gateway binary media types as a top-level key in the OpenAPI model
|
||||
* named {@code x-amazon-apigateway-binary-media-types}.
|
||||
*
|
||||
* <p>This data is used by API Gateway to determine which content-types do
|
||||
* not contain UTF-8 data.
|
||||
*/
|
||||
public final class AddBinaryTypes implements SmithyOpenApiPlugin {
|
||||
private static final Logger LOGGER = Logger.getLogger(AddBinaryTypes.class.getName());
|
||||
private static final String EXTENSION_NAME = "x-amazon-apigateway-binary-media-types";
|
||||
private static final String DEFAULT_BINARY_TYPE = "application/octet-stream";
|
||||
|
||||
@Override
|
||||
public OpenApi after(Context context, OpenApi openApi) {
|
||||
List<String> binaryTypes = supportedMediaTypes(context).sorted().collect(Collectors.toList());
|
||||
|
||||
if (!binaryTypes.isEmpty()) {
|
||||
LOGGER.fine(() -> "Adding recognized binary types to model: " + binaryTypes);
|
||||
return openApi.toBuilder()
|
||||
.putExtension(EXTENSION_NAME, Stream.concat(binaryTypes.stream(), Stream.of(DEFAULT_BINARY_TYPE))
|
||||
.distinct()
|
||||
.map(Node::from)
|
||||
.collect(ArrayNode.collect()))
|
||||
.build();
|
||||
}
|
||||
|
||||
return openApi;
|
||||
}
|
||||
|
||||
private Stream<String> supportedMediaTypes(Context context) {
|
||||
ShapeIndex shapeIndex = context.getModel().getShapeIndex();
|
||||
HttpBindingIndex httpBindingIndex = context.getModel().getKnowledge(HttpBindingIndex.class);
|
||||
TopDownIndex topDownIndex = context.getModel().getKnowledge(TopDownIndex.class);
|
||||
|
||||
// Find the media types defined on all request and response bindings.
|
||||
return topDownIndex.getContainedOperations(context.getService()).stream()
|
||||
.flatMap(operation -> Stream.concat(
|
||||
binaryMediaType(shapeIndex, httpBindingIndex.getRequestBindings(operation)).stream(),
|
||||
binaryMediaType(shapeIndex, httpBindingIndex.getResponseBindings(operation)).stream()));
|
||||
}
|
||||
|
||||
private Optional<String> binaryMediaType(ShapeIndex shapes, Map<String, HttpBindingIndex.Binding> httpBindings) {
|
||||
return httpBindings.values().stream()
|
||||
.filter(binding -> binding.getLocation().equals(HttpBindingIndex.Location.PAYLOAD))
|
||||
.map(HttpBindingIndex.Binding::getMember)
|
||||
.flatMap(member -> member.getMemberTrait(shapes, MediaTypeTrait.class).stream())
|
||||
.map(MediaTypeTrait::getValue)
|
||||
.findFirst();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,98 @@
|
|||
/*
|
||||
* Copyright 2019 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.smithy.aws.apigateway.openapi;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import software.amazon.smithy.aws.traits.apigateway.RequestValidatorTrait;
|
||||
import software.amazon.smithy.model.node.Node;
|
||||
import software.amazon.smithy.model.shapes.OperationShape;
|
||||
import software.amazon.smithy.openapi.fromsmithy.Context;
|
||||
import software.amazon.smithy.openapi.fromsmithy.SmithyOpenApiPlugin;
|
||||
import software.amazon.smithy.openapi.model.OpenApi;
|
||||
import software.amazon.smithy.openapi.model.OperationObject;
|
||||
|
||||
/**
|
||||
* Adds the API Gateway x-amazon-apigateway-request-validators object
|
||||
* to the service and x-amazon-apigateway-request-validator to the
|
||||
* service/operations.
|
||||
*
|
||||
* <p>Any operation or service shape with the {@link RequestValidatorTrait}
|
||||
* applied to it will cause that operation to have the {@code x-amazon-apigateway-request-validator}
|
||||
* extension, and adds a {@code x-amazon-apigateway-request-validators} extension
|
||||
* to the top-level OpenAPI document.
|
||||
*
|
||||
* @see <a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-swagger-extensions-request-validators.html">Request validators</a>
|
||||
*/
|
||||
public final class AddRequestValidators implements SmithyOpenApiPlugin {
|
||||
private static final String REQUEST_VALIDATOR = "x-amazon-apigateway-request-validator";
|
||||
private static final String REQUEST_VALIDATORS = "x-amazon-apigateway-request-validators";
|
||||
private static final Map<String, Node> KNOWN_VALIDATORS = Map.of(
|
||||
"params-only",
|
||||
Node.objectNode().withMember("validateRequestParameters", Node.from(true)),
|
||||
"body-only",
|
||||
Node.objectNode().withMember("validateRequestBody", Node.from(true)),
|
||||
"full",
|
||||
Node.objectNode()
|
||||
.withMember("validateRequestParameters", Node.from(true))
|
||||
.withMember("validateRequestBody", Node.from(true))
|
||||
);
|
||||
|
||||
@Override
|
||||
public OperationObject updateOperation(Context context, OperationShape shape, OperationObject operation) {
|
||||
return shape.getTrait(RequestValidatorTrait.class)
|
||||
.map(RequestValidatorTrait::getValue)
|
||||
.map(value -> operation.toBuilder().putExtension(REQUEST_VALIDATOR, value).build())
|
||||
.orElse(operation);
|
||||
}
|
||||
|
||||
@Override
|
||||
public OpenApi after(Context context, OpenApi openapi) {
|
||||
// Find each known request validator on operation shapes.
|
||||
Set<String> validators = context.getModel().getShapeIndex().shapes(OperationShape.class)
|
||||
.flatMap(shape -> shape.getTrait(RequestValidatorTrait.class).stream())
|
||||
.map(RequestValidatorTrait::getValue)
|
||||
.filter(KNOWN_VALIDATORS::containsKey)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
// Check if the service has a request validator.
|
||||
String serviceValidator = null;
|
||||
if (context.getService().getTrait(RequestValidatorTrait.class).isPresent()) {
|
||||
serviceValidator = context.getService().getTrait(RequestValidatorTrait.class).get().getValue();
|
||||
validators.add(serviceValidator);
|
||||
}
|
||||
|
||||
if (validators.isEmpty()) {
|
||||
return openapi;
|
||||
}
|
||||
|
||||
var builder = openapi.toBuilder();
|
||||
|
||||
if (serviceValidator != null) {
|
||||
builder.putExtension(REQUEST_VALIDATOR, serviceValidator);
|
||||
}
|
||||
|
||||
// Add the known request validators to the OpenAPI model.
|
||||
var objectBuilder = Node.objectNodeBuilder();
|
||||
for (var validator : validators) {
|
||||
objectBuilder.withMember(validator, KNOWN_VALIDATORS.get(validator));
|
||||
}
|
||||
|
||||
builder.putExtension(REQUEST_VALIDATORS, objectBuilder.build());
|
||||
return builder.build();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
//
|
||||
// Copyright 2019 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.
|
||||
//
|
||||
|
||||
--add-reads
|
||||
software.amazon.smithy.aws.apigateway.openapi=org.hamcrest
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* Copyright 2019 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.smithy.aws.apigateway.openapi;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import software.amazon.smithy.model.Model;
|
||||
import software.amazon.smithy.model.shapes.ShapeId;
|
||||
import software.amazon.smithy.openapi.fromsmithy.OpenApiConverter;
|
||||
|
||||
public class AddApiKeySourceTest {
|
||||
@Test
|
||||
public void addsApiKeySource() {
|
||||
Model model = Model.assembler()
|
||||
.discoverModels(getClass().getClassLoader())
|
||||
.addImport(getClass().getResource("api-key-source.json"))
|
||||
.assemble()
|
||||
.unwrap();
|
||||
var result = OpenApiConverter.create()
|
||||
.classLoader(getClass().getClassLoader())
|
||||
.convert(model, ShapeId.from("example.smithy#MyService"));
|
||||
var source = result.getExtension("x-amazon-apigateway-api-key-source")
|
||||
.get()
|
||||
.expectStringNode()
|
||||
.getValue();
|
||||
|
||||
assertThat(source, equalTo("HEADER"));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* Copyright 2019 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.smithy.aws.apigateway.openapi;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import software.amazon.smithy.model.Model;
|
||||
import software.amazon.smithy.model.node.Node;
|
||||
import software.amazon.smithy.model.shapes.ShapeId;
|
||||
import software.amazon.smithy.openapi.fromsmithy.OpenApiConverter;
|
||||
|
||||
public class AddAuthorizersTest {
|
||||
@Test
|
||||
public void addsAuthorizers() {
|
||||
Model model = Model.assembler()
|
||||
.discoverModels(getClass().getClassLoader())
|
||||
.addImport(getClass().getResource("authorizers.json"))
|
||||
.assemble()
|
||||
.unwrap();
|
||||
var result = OpenApiConverter.create()
|
||||
.classLoader(getClass().getClassLoader())
|
||||
.convert(model, ShapeId.from("ns.foo#SomeService"));
|
||||
var sigV4 = result.getComponents().getSecuritySchemes().get("sigv4");
|
||||
|
||||
assertThat(sigV4.getType(), equalTo("apiKey"));
|
||||
assertThat(sigV4.getName().get(), equalTo("Authorization"));
|
||||
assertThat(sigV4.getIn().get(), equalTo("header"));
|
||||
assertThat(sigV4.getExtension("x-amazon-apigateway-authtype").get(), equalTo(Node.from("awsSigV4")));
|
||||
var authorizer = sigV4.getExtension("x-amazon-apigateway-authorizer").get().expectObjectNode();
|
||||
assertThat(authorizer.getStringMember("type").get().getValue(), equalTo("request"));
|
||||
assertThat(authorizer.getStringMember("uri").get().getValue(), equalTo("arn:foo:baz"));
|
||||
assertThat(authorizer.getStringMember("credentials").get().getValue(), equalTo("arn:foo:bar"));
|
||||
assertThat(authorizer.getStringMember("identitySource").get().getValue(), equalTo("mapping.expression"));
|
||||
assertThat(authorizer.getStringMember("identityValidationExpression").get().getValue(), equalTo("[A-Z]+"));
|
||||
assertThat(authorizer.getNumberMember("resultTtlInSeconds").get().getValue(), equalTo(100));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* Copyright 2019 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.smithy.aws.apigateway.openapi;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.containsInAnyOrder;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import software.amazon.smithy.model.Model;
|
||||
import software.amazon.smithy.model.node.StringNode;
|
||||
import software.amazon.smithy.model.shapes.ShapeId;
|
||||
import software.amazon.smithy.openapi.fromsmithy.OpenApiConverter;
|
||||
|
||||
public class AddBinaryTypesTest {
|
||||
@Test
|
||||
public void addsBinaryTypes() {
|
||||
Model model = Model.assembler()
|
||||
.discoverModels(getClass().getClassLoader())
|
||||
.addImport(getClass().getResource("binary-types.json"))
|
||||
.assemble()
|
||||
.unwrap();
|
||||
|
||||
var result = OpenApiConverter.create()
|
||||
.classLoader(getClass().getClassLoader())
|
||||
.convert(model, ShapeId.from("example.smithy#MyService"));
|
||||
|
||||
var types = result.getExtension("x-amazon-apigateway-binary-media-types")
|
||||
.get()
|
||||
.expectArrayNode()
|
||||
.getElementsAs(StringNode::getValue);
|
||||
assertThat(types, containsInAnyOrder("application/zip", "image/*", "application/octet-stream"));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* Copyright 2019 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.smithy.aws.apigateway.openapi;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import software.amazon.smithy.model.Model;
|
||||
import software.amazon.smithy.model.node.Node;
|
||||
import software.amazon.smithy.model.shapes.ShapeId;
|
||||
import software.amazon.smithy.openapi.fromsmithy.OpenApiConverter;
|
||||
|
||||
public class AddRequestValidatorsTest {
|
||||
@Test
|
||||
public void addsRequestValidators() {
|
||||
Model model = Model.assembler()
|
||||
.discoverModels(getClass().getClassLoader())
|
||||
.addImport(getClass().getResource("request-validators.json"))
|
||||
.assemble()
|
||||
.unwrap();
|
||||
|
||||
var result = OpenApiConverter.create()
|
||||
.classLoader(getClass().getClassLoader())
|
||||
.convert(model, ShapeId.from("smithy.example#Service"));
|
||||
|
||||
assertThat(result.getExtension("x-amazon-apigateway-request-validator").get(), equalTo(Node.from("full")));
|
||||
var validators = result.getExtension("x-amazon-apigateway-request-validators").get().expectObjectNode();
|
||||
assertTrue(validators.containsMember("body-only"));
|
||||
assertTrue(validators.containsMember("full"));
|
||||
assertFalse(validators.containsMember("params-only"));
|
||||
|
||||
var operation1Val = result.getPaths().get("/1").getPut().get()
|
||||
.getExtension("x-amazon-apigateway-request-validator");
|
||||
assertTrue(operation1Val.isEmpty());
|
||||
|
||||
var operation2Val = result.getPaths().get("/2").getPut().get()
|
||||
.getExtension("x-amazon-apigateway-request-validator");
|
||||
assertThat(operation2Val.get(), equalTo(Node.from("body-only")));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"smithy": "1.0",
|
||||
"example.smithy": {
|
||||
"shapes": {
|
||||
"MyService": {
|
||||
"type": "service",
|
||||
"version": "2006-03-01",
|
||||
"protocols": {
|
||||
"aws.rest-json": {}
|
||||
},
|
||||
"operations": [
|
||||
"MyOperation"
|
||||
],
|
||||
"aws.apigateway#apiKeySource": "HEADER"
|
||||
},
|
||||
"MyOperation": {
|
||||
"type": "operation",
|
||||
"input": "MyOperationInput",
|
||||
"http": {
|
||||
"uri": "/foo",
|
||||
"method": "GET"
|
||||
}
|
||||
},
|
||||
"MyOperationInput": {
|
||||
"type": "structure"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"smithy": "1.0",
|
||||
"ns.foo": {
|
||||
"shapes": {
|
||||
"SomeService": {
|
||||
"type": "service",
|
||||
"version": "2018-03-17",
|
||||
"protocols": {"aws.rest-json": {}},
|
||||
"authentication": {"aws.v4": {}},
|
||||
"aws.apigateway#authorizers": {
|
||||
"aws.v4": {
|
||||
"clientType": "awsSigV4",
|
||||
"type": "request",
|
||||
"uri": "arn:foo:baz",
|
||||
"credentials": "arn:foo:bar",
|
||||
"identitySource": "mapping.expression",
|
||||
"identityValidationExpression": "[A-Z]+",
|
||||
"resultTtlInSeconds": 100
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
{
|
||||
"smithy": "1.0",
|
||||
"example.smithy": {
|
||||
"shapes": {
|
||||
"MyService": {
|
||||
"type": "service",
|
||||
"version": "2006-03-01",
|
||||
"protocols": {
|
||||
"aws.rest-json": {}
|
||||
},
|
||||
"operations": [
|
||||
"MyOperation",
|
||||
"OtherOperation"
|
||||
]
|
||||
},
|
||||
"MyOperation": {
|
||||
"type": "operation",
|
||||
"input": "MyOperationInput",
|
||||
"output": "MyOperationOutput",
|
||||
"http": {
|
||||
"uri": "/foo",
|
||||
"method": "POST"
|
||||
}
|
||||
},
|
||||
"OtherOperation": {
|
||||
"type": "operation",
|
||||
"readonly": true,
|
||||
"output": "OtherOperationOutput",
|
||||
"http": {
|
||||
"uri": "/bar",
|
||||
"method": "GET"
|
||||
}
|
||||
},
|
||||
"MyOperationInput": {
|
||||
"type": "structure",
|
||||
"members": {
|
||||
"payload": {
|
||||
"target": "InboundBinaryPayload",
|
||||
"required": true,
|
||||
"httpPayload": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"MyOperationOutput": {
|
||||
"type": "structure",
|
||||
"members": {
|
||||
"payload": {
|
||||
"target": "OutboundBinaryPayload",
|
||||
"required": true,
|
||||
"httpPayload": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"OtherOperationOutput": {
|
||||
"type": "structure",
|
||||
"members": {
|
||||
"abc": {
|
||||
"target": "StringShape"
|
||||
},
|
||||
"def": {
|
||||
"target": "NonPayloadBinaryShape"
|
||||
}
|
||||
}
|
||||
},
|
||||
"InboundBinaryPayload": {
|
||||
"type": "blob",
|
||||
"mediaType": "image/*"
|
||||
},
|
||||
"OutboundBinaryPayload": {
|
||||
"type": "blob",
|
||||
"mediaType": "application/zip"
|
||||
},
|
||||
"NonPayloadBinaryShape": {
|
||||
"type": "blob",
|
||||
"mediaType": "audio/mp4"
|
||||
},
|
||||
"StringShape": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"smithy": "1.0",
|
||||
"smithy.example": {
|
||||
"shapes": {
|
||||
"Service": {
|
||||
"type": "service",
|
||||
"version": "2006-03-01",
|
||||
"protocols": {"aws.rest-json": {}},
|
||||
"operations": ["Operation1", "Operation2"],
|
||||
"aws.apigateway#requestValidator": "full"
|
||||
},
|
||||
"Operation1": {
|
||||
"type": "operation",
|
||||
"idempotent": true,
|
||||
"http": {"uri": "/1", "method": "PUT"}
|
||||
},
|
||||
"Operation2": {
|
||||
"type": "operation",
|
||||
"aws.apigateway#requestValidator": "body-only",
|
||||
"http": {"uri": "/2", "method": "PUT"}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,96 @@
|
|||
# Smithy AWS core traits
|
||||
|
||||
Provides traits and validators that are used by most AWS services.
|
||||
|
||||
See the Smithy specification for details on how these traits are used.
|
||||
|
||||
## `aws.api` Traits
|
||||
|
||||
* `aws.api#service`: Configures a Smithy service as an AWS service.
|
||||
* `aws.api#arn`: Defines the ARN template of a Smithy resource.
|
||||
* `aws.api#arnReference`: Indicates that a string shape contains an ARN.
|
||||
* `aws.api#unsignedPayload`: Configures that request payload of an
|
||||
operation as unsigned.
|
||||
|
||||
## `aws.iam` Traits
|
||||
|
||||
* `aws.iam#actionPermissionDescription`: Defines the description of what
|
||||
providing access to an operation entails.
|
||||
* `aws.iam#conditionKeyDefinition`: Defines the condition keys used in
|
||||
a service.
|
||||
* `aws.iam#conditionKeys`: Applies condition keys to an operation or resource.
|
||||
* `aws.iam#inferConditionKeys`: Infers the condition keys of a resource.
|
||||
Each operation bound to the resource also uses these condition keys.
|
||||
* `aws.iam#requiredActions`: Defines the actions that a principal must be
|
||||
authorized to invoke in addition to the targetd operation order to invoke
|
||||
an operation
|
||||
|
||||
## Example usage
|
||||
|
||||
Example usage:
|
||||
|
||||
```smithy
|
||||
$version:1.0
|
||||
namespace ns.foo
|
||||
|
||||
@aws.api#service(sdkId: "Some Value")
|
||||
service SomeService {
|
||||
version: "2018-03-17",
|
||||
resources: [SomeResource, RootArnResource, AbsoluteResource],
|
||||
}
|
||||
|
||||
// This resource has an ARN, but no identifier.
|
||||
@aws.api#arn(template: "rootArnResource")
|
||||
resource RootArnResource {}
|
||||
|
||||
// This resource has an ARN and MUST provide a placeholder for its
|
||||
// identifier.
|
||||
@aws.api#arn(template: "someresource/{someId}")
|
||||
resource SomeResource {
|
||||
identifiers: {
|
||||
someId: SomeResourceId,
|
||||
childId: ChildResourceId,
|
||||
},
|
||||
resources: [ChildResource],
|
||||
}
|
||||
|
||||
// This resource has an ARN and MUST provide placeholders for all of its
|
||||
// identifiers. This relative ARN does not include a region or account ID.
|
||||
@aws.api#arn(
|
||||
template: "someresource/{someId}/{childId}",
|
||||
noRegion: true,
|
||||
noAccount: true)
|
||||
resource ChildResource {
|
||||
identifiers: {
|
||||
someId: SomeResourceId,
|
||||
childId: ChildResourceId,
|
||||
},
|
||||
resources: [AnotherChild],
|
||||
}
|
||||
|
||||
resource AnotherChild {
|
||||
identifiers: {
|
||||
someId: SomeResourceId,
|
||||
childId: ChildResourceId,
|
||||
},
|
||||
}
|
||||
|
||||
// This resource uses an ARN as its identifier, so its ARN template is absolute.
|
||||
@aws.api#arn(
|
||||
template: "{arn}",
|
||||
absolute: true)
|
||||
resource AbsoluteResource {
|
||||
identifiers: {
|
||||
arn: AbsoluteResourceArn
|
||||
},
|
||||
}
|
||||
|
||||
@aws.api#arnReference(
|
||||
type: "AWS::SomeService::AbsoluteResource",
|
||||
service: 'ns.foo#SomeService',
|
||||
resource: 'ns.foo#AbsoluteResource')
|
||||
string AbsoluteResourceArn
|
||||
|
||||
string SomeResourceId
|
||||
string ChildResourceId
|
||||
```
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* Copyright 2019 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.
|
||||
*/
|
||||
|
||||
dependencies {
|
||||
api(project(":smithy-model"))
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
/*
|
||||
* Copyright 2019 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.
|
||||
*/
|
||||
|
||||
import software.amazon.smithy.aws.traits.ArnReferenceTrait;
|
||||
import software.amazon.smithy.aws.traits.ArnTemplateValidator;
|
||||
import software.amazon.smithy.aws.traits.ArnTrait;
|
||||
import software.amazon.smithy.aws.traits.AwsModelDiscovery;
|
||||
import software.amazon.smithy.aws.traits.DataTrait;
|
||||
import software.amazon.smithy.aws.traits.SdkServiceIdValidator;
|
||||
import software.amazon.smithy.aws.traits.ServiceTrait;
|
||||
import software.amazon.smithy.aws.traits.UnsignedPayload;
|
||||
import software.amazon.smithy.aws.traits.apigateway.ApiKeySourceTrait;
|
||||
import software.amazon.smithy.aws.traits.apigateway.AuthorizersTrait;
|
||||
import software.amazon.smithy.aws.traits.apigateway.AuthorizersTraitValidator;
|
||||
import software.amazon.smithy.aws.traits.apigateway.RequestValidatorTrait;
|
||||
import software.amazon.smithy.aws.traits.iam.ActionPermissionDescriptionTrait;
|
||||
import software.amazon.smithy.aws.traits.iam.ConditionKeysTrait;
|
||||
import software.amazon.smithy.aws.traits.iam.ConditionKeysValidator;
|
||||
import software.amazon.smithy.aws.traits.iam.DefineConditionKeysTrait;
|
||||
import software.amazon.smithy.aws.traits.iam.InferConditionKeysTrait;
|
||||
import software.amazon.smithy.aws.traits.iam.RequiredActionsTrait;
|
||||
import software.amazon.smithy.model.loader.ModelDiscovery;
|
||||
import software.amazon.smithy.model.traits.TraitService;
|
||||
import software.amazon.smithy.model.validation.Validator;
|
||||
|
||||
module software.amazon.smithy.aws.traits {
|
||||
requires java.logging;
|
||||
requires software.amazon.smithy.model;
|
||||
|
||||
exports software.amazon.smithy.aws.traits;
|
||||
exports software.amazon.smithy.aws.traits.iam;
|
||||
exports software.amazon.smithy.aws.traits.apigateway;
|
||||
|
||||
uses ModelDiscovery;
|
||||
uses TraitService;
|
||||
uses Validator;
|
||||
|
||||
// Allow the AWS trait model to be discovered.
|
||||
provides ModelDiscovery with AwsModelDiscovery;
|
||||
|
||||
// Adds the typed trait classes.
|
||||
provides TraitService with
|
||||
// AWS traits.
|
||||
ServiceTrait,
|
||||
UnsignedPayload,
|
||||
|
||||
// IAM traits.
|
||||
ActionPermissionDescriptionTrait,
|
||||
ArnReferenceTrait,
|
||||
ArnTrait,
|
||||
ConditionKeysTrait,
|
||||
DataTrait,
|
||||
DefineConditionKeysTrait,
|
||||
InferConditionKeysTrait,
|
||||
RequiredActionsTrait,
|
||||
|
||||
// API Gateway traits.
|
||||
AuthorizersTrait,
|
||||
RequestValidatorTrait,
|
||||
ApiKeySourceTrait;
|
||||
|
||||
// Add AWS trait validators.
|
||||
provides Validator with
|
||||
ArnTemplateValidator,
|
||||
ConditionKeysValidator,
|
||||
SdkServiceIdValidator,
|
||||
AuthorizersTraitValidator;
|
||||
}
|
||||
|
|
@ -0,0 +1,137 @@
|
|||
/*
|
||||
* Copyright 2019 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.smithy.aws.traits;
|
||||
|
||||
import static java.util.Collections.unmodifiableMap;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
import software.amazon.smithy.model.Model;
|
||||
import software.amazon.smithy.model.Pair;
|
||||
import software.amazon.smithy.model.knowledge.KnowledgeIndex;
|
||||
import software.amazon.smithy.model.knowledge.TopDownIndex;
|
||||
import software.amazon.smithy.model.shapes.ServiceShape;
|
||||
import software.amazon.smithy.model.shapes.ShapeId;
|
||||
import software.amazon.smithy.model.shapes.ToShapeId;
|
||||
import software.amazon.smithy.model.traits.Trait;
|
||||
|
||||
/**
|
||||
* Resolves and indexes the ARN templates for each resource in a service.
|
||||
*/
|
||||
public final class ArnIndex implements KnowledgeIndex {
|
||||
private final Map<ShapeId, String> arnServices;
|
||||
private final Map<ShapeId, Map<ShapeId, ArnTrait>> templates;
|
||||
|
||||
public ArnIndex(Model model) {
|
||||
// Pre-compute the ARN services.
|
||||
arnServices = unmodifiableMap(model.getShapeIndex().shapes(ServiceShape.class)
|
||||
.flatMap(shape -> Trait.flatMapStream(shape, ServiceTrait.class))
|
||||
.map(pair -> new Pair<>(pair.getLeft().getId(), resolveServiceArn(pair)))
|
||||
.collect(Collectors.toMap(Pair::getLeft, Pair::getRight)));
|
||||
|
||||
// Pre-compute all of the ArnTemplates in a service shape.
|
||||
var topDownIndex = model.getKnowledge(TopDownIndex.class);
|
||||
templates = unmodifiableMap(model.getShapeIndex().shapes(ServiceShape.class)
|
||||
.flatMap(shape -> Trait.flatMapStream(shape, ServiceTrait.class))
|
||||
.map(pair -> compileServiceArns(topDownIndex, pair.getLeft()))
|
||||
.collect(Collectors.toMap(Pair::getLeft, Pair::getRight)));
|
||||
}
|
||||
|
||||
private static String resolveServiceArn(Pair<ServiceShape, ServiceTrait> pair) {
|
||||
return pair.getRight().getArnNamespace();
|
||||
}
|
||||
|
||||
private Pair<ShapeId, Map<ShapeId, ArnTrait>> compileServiceArns(
|
||||
TopDownIndex index,
|
||||
ServiceShape service
|
||||
) {
|
||||
return new Pair<>(service.getId(), unmodifiableMap(index.getContainedResources(service.getId()).stream()
|
||||
.flatMap(resource -> Trait.flatMapStream(resource, ArnTrait.class))
|
||||
.map(pair -> new Pair<>(pair.getLeft().getId(), pair.getRight()))
|
||||
.collect(Collectors.toMap(Pair::getLeft, Pair::getRight))));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the ARN service namespace of a service shape.
|
||||
*
|
||||
* @param serviceId Service shape to get ARN namespace of.
|
||||
* @return Returns the resolved ARN service namespace, defaulting to the
|
||||
* lowercase shape name if not known.
|
||||
*/
|
||||
public String getServiceArnNamespace(ToShapeId serviceId) {
|
||||
return arnServices.containsKey(serviceId.toShapeId())
|
||||
? arnServices.get(serviceId.toShapeId())
|
||||
: serviceId.toShapeId().getName().toLowerCase(Locale.US);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all of the mappings of resources within a service to its
|
||||
* arnTemplate trait.
|
||||
*
|
||||
* @param service Service to retrieve.
|
||||
* @return Returns the mapping of resource ID to arnTemplate traits.
|
||||
*/
|
||||
public Map<ShapeId, ArnTrait> getServiceResourceArns(ToShapeId service) {
|
||||
return templates.getOrDefault(service.toShapeId(), Collections.emptyMap());
|
||||
}
|
||||
|
||||
/**
|
||||
* Expands the relative ARN of a resource with the service name to form a
|
||||
* full ARN template.
|
||||
*
|
||||
* <p>For relative ARNs, the returned template string is in the format of
|
||||
* <code>arn:{AWS::Partition}:service:{AWS::Region}:{AWS::AccountId}:resource</code>
|
||||
* where "service" is the resolved ARN service name of the service and
|
||||
* "resource" is the resource part of the arnTemplate template.
|
||||
* "{AWS::Region}" is added to the template if the arnTemplate "noRegion"
|
||||
* value is not set to true. "{AWS::AccountId}" is added to the template if
|
||||
* the arnTemplate "noAccount" value is not set to true.
|
||||
*
|
||||
* <p>For example, if both "noAccount" and "noRegion" are set to true,
|
||||
* the resolved ARN template might look like "arn:{AWS::Partition}:service:::resource".
|
||||
*
|
||||
* <p>Absolute ARN templates are returned as-is.
|
||||
*
|
||||
* @param service Service shape ID.
|
||||
* @param resource Resource shape ID.
|
||||
* @return Returns the optionally found ARN template for a resource.
|
||||
*/
|
||||
public Optional<String> getFullResourceArnTemplate(ToShapeId service, ToShapeId resource) {
|
||||
return Optional.ofNullable(getServiceResourceArns(service).get(resource.toShapeId()))
|
||||
.map(trait -> {
|
||||
StringBuilder result = new StringBuilder();
|
||||
if (!trait.isAbsolute()) {
|
||||
result.append("arn:")
|
||||
.append("{AWS::Partition}:")
|
||||
.append(getServiceArnNamespace(service))
|
||||
.append(":");
|
||||
if (!trait.isNoRegion()) {
|
||||
result.append("{AWS::Region}");
|
||||
}
|
||||
result.append(":");
|
||||
if (!trait.isNoAccount()) {
|
||||
result.append("{AWS::AccountId}");
|
||||
}
|
||||
result.append(":");
|
||||
}
|
||||
|
||||
return result.append(trait.getTemplate()).toString();
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,146 @@
|
|||
/*
|
||||
* Copyright 2019 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.smithy.aws.traits;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import software.amazon.smithy.model.ToSmithyBuilder;
|
||||
import software.amazon.smithy.model.node.Node;
|
||||
import software.amazon.smithy.model.node.StringNode;
|
||||
import software.amazon.smithy.model.shapes.ShapeId;
|
||||
import software.amazon.smithy.model.traits.AbstractTrait;
|
||||
import software.amazon.smithy.model.traits.AbstractTraitBuilder;
|
||||
import software.amazon.smithy.model.traits.TraitService;
|
||||
|
||||
/**
|
||||
* Indicates that a string shape contains an ARN.
|
||||
*/
|
||||
public final class ArnReferenceTrait extends AbstractTrait implements ToSmithyBuilder<ArnReferenceTrait> {
|
||||
private static final String TRAIT = "aws.api#arnReference";
|
||||
private static final String TYPE = "type";
|
||||
private static final String SERVICE = "service";
|
||||
private static final String RESOURCE = "resource";
|
||||
private static final List<String> PROPERTIES = List.of(TYPE, SERVICE, RESOURCE);
|
||||
|
||||
private String type;
|
||||
private ShapeId service;
|
||||
private ShapeId resource;
|
||||
|
||||
private ArnReferenceTrait(Builder builder) {
|
||||
super(TRAIT, builder.getSourceLocation());
|
||||
this.type = builder.type;
|
||||
this.service = builder.service;
|
||||
this.resource = builder.resource;
|
||||
}
|
||||
|
||||
public static TraitService provider() {
|
||||
return TraitService.createProvider(TRAIT, (target, value) -> {
|
||||
var objectNode = value.expectObjectNode();
|
||||
var builder = builder();
|
||||
objectNode.warnIfAdditionalProperties(PROPERTIES);
|
||||
objectNode.getMember(TYPE)
|
||||
.map(Node::expectStringNode)
|
||||
.map(StringNode::getValue)
|
||||
.ifPresent(builder::type);
|
||||
objectNode.getMember(SERVICE)
|
||||
.map(Node::expectStringNode)
|
||||
.map(stringNode -> stringNode.expectShapeId(target.getNamespace()))
|
||||
.ifPresent(id -> builder.service(id));
|
||||
objectNode.getMember(RESOURCE)
|
||||
.map(Node::expectStringNode)
|
||||
.map(stringNode -> stringNode.expectShapeId(target.getNamespace()))
|
||||
.ifPresent(id -> builder.resource(id));
|
||||
return builder.build();
|
||||
});
|
||||
}
|
||||
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the AWS CloudFormation type of the ARN.
|
||||
*
|
||||
* @return Returns the optional type.
|
||||
*/
|
||||
public Optional<String> getType() {
|
||||
return Optional.ofNullable(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Smithy resource shape ID of the ARN.
|
||||
*
|
||||
* @return Returns the optionally present shape ID.
|
||||
*/
|
||||
public Optional<ShapeId> getResource() {
|
||||
return Optional.ofNullable(resource);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Smithy service shape ID of the ARN.
|
||||
*
|
||||
* @return Returns the optionally present shape ID.
|
||||
*/
|
||||
public Optional<ShapeId> getService() {
|
||||
return Optional.ofNullable(service);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder toBuilder() {
|
||||
return new Builder()
|
||||
.sourceLocation(getSourceLocation())
|
||||
.type(type)
|
||||
.service(service)
|
||||
.resource(resource);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Node createNode() {
|
||||
return Node.objectNode()
|
||||
.withOptionalMember(TYPE, getType().map(Node::from))
|
||||
.withOptionalMember(SERVICE, getService().map(ShapeId::toString).map(Node::from))
|
||||
.withOptionalMember(RESOURCE, getResource().map(ShapeId::toString).map(Node::from));
|
||||
}
|
||||
|
||||
/** Builder for {@link ArnReferenceTrait}. */
|
||||
public static final class Builder extends AbstractTraitBuilder<ArnReferenceTrait, Builder> {
|
||||
private String type;
|
||||
private ShapeId service;
|
||||
private ShapeId resource;
|
||||
|
||||
private Builder() {}
|
||||
|
||||
@Override
|
||||
public ArnReferenceTrait build() {
|
||||
return new ArnReferenceTrait(this);
|
||||
}
|
||||
|
||||
public Builder type(String type) {
|
||||
this.type = type;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder service(ShapeId service) {
|
||||
this.service = service;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder resource(ShapeId resource) {
|
||||
this.resource = resource;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,113 @@
|
|||
/*
|
||||
* Copyright 2019 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.smithy.aws.traits;
|
||||
|
||||
import static java.util.stream.Collectors.toList;
|
||||
import static software.amazon.smithy.model.validation.ValidationUtils.tickedList;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Stream;
|
||||
import software.amazon.smithy.model.Model;
|
||||
import software.amazon.smithy.model.Pair;
|
||||
import software.amazon.smithy.model.shapes.ResourceShape;
|
||||
import software.amazon.smithy.model.shapes.ServiceShape;
|
||||
import software.amazon.smithy.model.shapes.Shape;
|
||||
import software.amazon.smithy.model.shapes.ShapeIndex;
|
||||
import software.amazon.smithy.model.traits.Trait;
|
||||
import software.amazon.smithy.model.validation.AbstractValidator;
|
||||
import software.amazon.smithy.model.validation.ValidationEvent;
|
||||
|
||||
/**
|
||||
* Ensures that all arn traits for a service are valid and that their templates
|
||||
* only reference valid resource identifiers.
|
||||
*/
|
||||
public final class ArnTemplateValidator extends AbstractValidator {
|
||||
private static final Pattern EXPRESSION_PATTERN = Pattern.compile("^[!|+]?[a-zA-Z_][a-zA-Z0-9_]*$");
|
||||
|
||||
@Override
|
||||
public List<ValidationEvent> validate(Model model) {
|
||||
var arnIndex = model.getKnowledge(ArnIndex.class);
|
||||
return model.getShapeIndex().shapes(ServiceShape.class)
|
||||
.flatMap(service -> Trait.flatMapStream(service, ServiceTrait.class))
|
||||
.flatMap(pair -> validateService(model.getShapeIndex(), arnIndex, pair.getLeft()))
|
||||
.collect(toList());
|
||||
}
|
||||
|
||||
private Stream<ValidationEvent> validateService(ShapeIndex index, ArnIndex arnIndex, ServiceShape service) {
|
||||
// Make sure each ARN template contains relevant identifiers.
|
||||
return arnIndex.getServiceResourceArns(service.getId()).entrySet().stream()
|
||||
.flatMap(entry -> index.getShape(entry.getKey())
|
||||
.flatMap(Shape::asResourceShape)
|
||||
.map(resource -> new Pair<>(resource, entry.getValue()))
|
||||
.stream())
|
||||
.flatMap(pair -> validateResourceArn(pair.getLeft(), pair.getRight()));
|
||||
}
|
||||
|
||||
private Stream<ValidationEvent> validateResourceArn(ResourceShape resource, ArnTrait template) {
|
||||
// Fail early on syntax error, otherwise, validate that the
|
||||
// template correspond to identifiers.
|
||||
return syntax(resource, template).map(Stream::of).orElseGet(() -> Stream.concat(
|
||||
enough(resource.getIdentifiers().keySet(), resource, template).stream(),
|
||||
tooMuch(resource.getIdentifiers().keySet(), resource, template).stream())
|
||||
);
|
||||
}
|
||||
|
||||
// Validates the syntax of each template.
|
||||
private Optional<ValidationEvent> syntax(Shape shape, ArnTrait trait) {
|
||||
var invalid = trait.getLabels().stream()
|
||||
.filter(expr -> !EXPRESSION_PATTERN.matcher(expr).find())
|
||||
.collect(toList());
|
||||
|
||||
if (invalid.isEmpty()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
return Optional.of(error(shape, trait, String.format(
|
||||
"aws.api#arn trait contains invalid template labels: %s. Template labels must match the "
|
||||
+ "following regular expression: %s",
|
||||
tickedList(invalid), EXPRESSION_PATTERN.pattern())));
|
||||
}
|
||||
|
||||
// Ensures that a template does not contain extraneous resource identifiers.
|
||||
private Optional<ValidationEvent> tooMuch(Collection<String> names, Shape shape, ArnTrait trait) {
|
||||
var templateCheck = new HashSet<>(trait.getLabels());
|
||||
templateCheck.removeAll(names);
|
||||
if (!templateCheck.isEmpty()) {
|
||||
return Optional.of(error(shape, trait, String.format(
|
||||
"Invalid aws.api#arn trait resource, `%s`. Found template labels in the trait "
|
||||
+ "that are not the names of the identifiers of the resource: %s. Extraneous identifiers: [%s]",
|
||||
trait.getTemplate(), names, tickedList(templateCheck))));
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
// Ensures that a template references all resource identifiers.
|
||||
private Optional<ValidationEvent> enough(Collection<String> names, Shape shape, ArnTrait trait) {
|
||||
var identifierVars = new HashSet<>(names);
|
||||
identifierVars.removeAll(trait.getLabels());
|
||||
if (!identifierVars.isEmpty()) {
|
||||
return Optional.of(error(shape, trait, String.format(
|
||||
"Invalid aws.api#arn trait resource, `%s`. The following resource identifier names "
|
||||
+ "were missing from the `arn` template: %s",
|
||||
trait.getTemplate(), tickedList(identifierVars))));
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,189 @@
|
|||
/*
|
||||
* Copyright 2019 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.smithy.aws.traits;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
import software.amazon.smithy.model.SmithyBuilder;
|
||||
import software.amazon.smithy.model.SourceException;
|
||||
import software.amazon.smithy.model.ToSmithyBuilder;
|
||||
import software.amazon.smithy.model.node.BooleanNode;
|
||||
import software.amazon.smithy.model.node.Node;
|
||||
import software.amazon.smithy.model.traits.AbstractTrait;
|
||||
import software.amazon.smithy.model.traits.AbstractTraitBuilder;
|
||||
import software.amazon.smithy.model.traits.TraitService;
|
||||
|
||||
/**
|
||||
* Configures the ARN template of a resource shape, relative to the
|
||||
* service to which a resource is bound.
|
||||
*/
|
||||
public final class ArnTrait extends AbstractTrait implements ToSmithyBuilder<ArnTrait> {
|
||||
private static final String TRAIT = "aws.api#arn";
|
||||
private static final String TEMPLATE = "template";
|
||||
private static final String ABSOLUTE = "absolute";
|
||||
private static final String NO_REGION = "noRegion";
|
||||
private static final String NO_ACCOUNT = "noAccount";
|
||||
private static final List<String> PROPERTIES = List.of(TEMPLATE, ABSOLUTE, NO_REGION, NO_ACCOUNT);
|
||||
private static final Pattern PATTERN = Pattern.compile("\\{([^}]+)}");
|
||||
|
||||
private final boolean noRegion;
|
||||
private final boolean noAccount;
|
||||
private final boolean absolute;
|
||||
private final String template;
|
||||
private final List<String> labels;
|
||||
|
||||
private ArnTrait(Builder builder) {
|
||||
super(TRAIT, builder.getSourceLocation());
|
||||
this.template = SmithyBuilder.requiredState(TEMPLATE, builder.template);
|
||||
this.noRegion = builder.noRegion;
|
||||
this.noAccount = builder.noAccount;
|
||||
this.absolute = builder.absolute;
|
||||
this.labels = Collections.unmodifiableList(parseLabels(template));
|
||||
|
||||
if (template.startsWith("/")) {
|
||||
throw new SourceException("Invalid aws.api#arn trait. The template must not start with '/'. "
|
||||
+ "Found `" + template + "`", getSourceLocation());
|
||||
}
|
||||
}
|
||||
|
||||
public static TraitService provider() {
|
||||
return TraitService.createProvider(TRAIT, (target, value) -> {
|
||||
var builder = builder();
|
||||
var objectNode = value.expectObjectNode();
|
||||
objectNode.warnIfAdditionalProperties(PROPERTIES);
|
||||
builder.template(objectNode.expectMember(TEMPLATE).expectStringNode().getValue());
|
||||
builder.absolute(objectNode.getMember(ABSOLUTE)
|
||||
.map(Node::expectBooleanNode)
|
||||
.map(BooleanNode::getValue)
|
||||
.orElse(false));
|
||||
builder.noRegion(objectNode.getMember(NO_REGION)
|
||||
.map(Node::expectBooleanNode)
|
||||
.map(BooleanNode::getValue)
|
||||
.orElse(false));
|
||||
builder.noAccount(objectNode.getMember(NO_ACCOUNT)
|
||||
.map(Node::expectBooleanNode)
|
||||
.map(BooleanNode::getValue)
|
||||
.orElse(false));
|
||||
return builder.build();
|
||||
});
|
||||
}
|
||||
|
||||
private static List<String> parseLabels(String resource) {
|
||||
List<String> result = new ArrayList<>();
|
||||
var matcher = PATTERN.matcher(resource);
|
||||
while (matcher.find()) {
|
||||
result.add(matcher.group(1));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Returns a builder used to create {@link ArnTrait}.
|
||||
*/
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Returns the noAccount setting.
|
||||
*/
|
||||
public boolean isNoAccount() {
|
||||
return noAccount;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Returns the noRegion setting.
|
||||
*/
|
||||
public boolean isNoRegion() {
|
||||
return noRegion;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Returns whether or not the ARN is absolute.
|
||||
*/
|
||||
public boolean isAbsolute() {
|
||||
return absolute;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Gets the template of the ARN.
|
||||
*/
|
||||
public String getTemplate() {
|
||||
return template;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Returns the label place holder variable names.
|
||||
*/
|
||||
public List<String> getLabels() {
|
||||
return labels;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Node createNode() {
|
||||
return Node.objectNode()
|
||||
.withMember(TEMPLATE, Node.from(getTemplate()))
|
||||
.withMember(ABSOLUTE, Node.from(isAbsolute()))
|
||||
.withMember(NO_ACCOUNT, Node.from(isNoAccount()))
|
||||
.withMember(NO_REGION, Node.from(isNoRegion()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder toBuilder() {
|
||||
return builder()
|
||||
.sourceLocation(getSourceLocation())
|
||||
.noRegion(isNoRegion())
|
||||
.noAccount(isNoAccount())
|
||||
.template(getTemplate());
|
||||
}
|
||||
|
||||
/** Builder for {@link ArnTrait}. */
|
||||
public static final class Builder extends AbstractTraitBuilder<ArnTrait, Builder> {
|
||||
private boolean noRegion;
|
||||
private boolean noAccount;
|
||||
private boolean absolute;
|
||||
private String template;
|
||||
|
||||
private Builder() {}
|
||||
|
||||
@Override
|
||||
public ArnTrait build() {
|
||||
return new ArnTrait(this);
|
||||
}
|
||||
|
||||
public Builder template(String template) {
|
||||
this.template = template;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder absolute(boolean absolute) {
|
||||
this.absolute = absolute;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder noAccount(boolean noAccount) {
|
||||
this.noAccount = noAccount;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder noRegion(boolean noRegion) {
|
||||
this.noRegion = noRegion;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* Copyright 2019 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.smithy.aws.traits;
|
||||
|
||||
import java.net.URL;
|
||||
import java.util.List;
|
||||
import software.amazon.smithy.model.loader.ModelAssembler;
|
||||
import software.amazon.smithy.model.loader.ModelDiscovery;
|
||||
|
||||
/**
|
||||
* This is used to allow a {@link ModelAssembler} to discover the AWS
|
||||
* core model.
|
||||
*/
|
||||
public final class AwsModelDiscovery implements ModelDiscovery {
|
||||
@Override
|
||||
public List<URL> getModels() {
|
||||
return List.of(
|
||||
getClass().getResource("aws.api.json"),
|
||||
getClass().getResource("aws.iam.json"),
|
||||
getClass().getResource("aws.apigateway.json"));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Copyright 2019 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.smithy.aws.traits;
|
||||
|
||||
import software.amazon.smithy.model.SourceLocation;
|
||||
import software.amazon.smithy.model.traits.StringTrait;
|
||||
import software.amazon.smithy.model.traits.TraitService;
|
||||
|
||||
public final class DataTrait extends StringTrait {
|
||||
private static final String TRAIT = "aws.api#data";
|
||||
|
||||
public DataTrait(String value, SourceLocation sourceLocation) {
|
||||
super(TRAIT, value, sourceLocation);
|
||||
}
|
||||
|
||||
public static TraitService provider() {
|
||||
return TraitService.createStringProvider(TRAIT, DataTrait::new);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,153 @@
|
|||
/*
|
||||
* Copyright 2019 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.smithy.aws.traits;
|
||||
|
||||
import static java.util.stream.Collectors.toList;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Pattern;
|
||||
import software.amazon.smithy.model.Model;
|
||||
import software.amazon.smithy.model.shapes.ServiceShape;
|
||||
import software.amazon.smithy.model.traits.Trait;
|
||||
import software.amazon.smithy.model.validation.AbstractValidator;
|
||||
import software.amazon.smithy.model.validation.ValidationEvent;
|
||||
import software.amazon.smithy.model.validation.ValidationUtils;
|
||||
|
||||
/**
|
||||
* Validates that SDK service IDs are correct and do not match any
|
||||
* prohibited patterns.
|
||||
*
|
||||
* <ul>
|
||||
* <li>Must match the following regex: ^[a-zA-Z][a-zA-Z0-9]*( [a-zA-Z0-9]+)*$</li>
|
||||
* <li>Must not contain "Amazon", "AWS", or "Aws"</li>
|
||||
* <li>Must not case-insensitively end with "Service", "Client", or "API".</li>
|
||||
* </ul>
|
||||
*/
|
||||
public final class SdkServiceIdValidator extends AbstractValidator {
|
||||
private static final Set<String> COMPANY_NAMES = Set.of("AWS", "Aws", "Amazon");
|
||||
private static final Set<String> DISALLOWED_ENDINGS = Set.of("service", "client", "api");
|
||||
private static final Pattern SERVICE_ID_PATTERN = Pattern.compile("^[a-zA-Z][a-zA-Z0-9]*( [a-zA-Z0-9]+)*$");
|
||||
|
||||
/**
|
||||
* Service Id's that have already been shipped.
|
||||
*
|
||||
* No new serviceId's should be added to this list in the future.
|
||||
*/
|
||||
private static final Set<String> PREEXISTING_SERVICE_IDS = Set.of(
|
||||
"ACM PCA",
|
||||
"Config Service",
|
||||
"Cost and Usage Report Service",
|
||||
"Application Discovery Service",
|
||||
"Database Migration Service",
|
||||
"Directory Service",
|
||||
"Elasticsearch Service",
|
||||
"IoT 1Click Devices Service",
|
||||
"IoTAnalytics",
|
||||
"Lex Model Building Service",
|
||||
"Lex Runtime Service",
|
||||
"Marketplace Entitlement Service",
|
||||
"mq",
|
||||
"Resource Groups Tagging API");
|
||||
|
||||
@Override
|
||||
public List<ValidationEvent> validate(Model model) {
|
||||
return model.getShapeIndex().shapes(ServiceShape.class)
|
||||
.flatMap(service -> Trait.flatMapStream(service, ServiceTrait.class))
|
||||
.flatMap(pair -> validateService(pair.getLeft(), pair.getRight()).stream())
|
||||
.collect(toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given value is a previously released but
|
||||
* invalid service ID.
|
||||
*
|
||||
* @param serviceId Service ID value to check.
|
||||
* @return Returns true if the service ID is approved but invalid.
|
||||
*/
|
||||
public static boolean isPreviouslyReleasedInvalidServiceId(String serviceId) {
|
||||
return PREEXISTING_SERVICE_IDS.contains(serviceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a service ID value.
|
||||
*
|
||||
* @param serviceId Service ID to validate.
|
||||
* @throws IllegalArgumentException if the service ID is invalid.
|
||||
*/
|
||||
public static void validateServiceId(String serviceId) {
|
||||
if (isPreviouslyReleasedInvalidServiceId(serviceId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<String> messages = new ArrayList<>();
|
||||
|
||||
if (!validForPattern(serviceId)) {
|
||||
messages.add(String.format("Does not match the required pattern `%s`", SERVICE_ID_PATTERN.pattern()));
|
||||
}
|
||||
|
||||
if (containsCompanyName(serviceId)) {
|
||||
messages.add(String.format(
|
||||
"Must not contain any of the following company names: [%s]",
|
||||
ValidationUtils.tickedList(COMPANY_NAMES)));
|
||||
}
|
||||
|
||||
endsWithForbiddenWord(serviceId).ifPresent(suffix -> {
|
||||
messages.add(String.format("Must not case-insensitively end with `%s`", suffix));
|
||||
});
|
||||
|
||||
if (serviceId.length() < 2 || serviceId.length() > 50) {
|
||||
messages.add("Must be between 2 and 50 characters long.");
|
||||
}
|
||||
|
||||
if (!messages.isEmpty()) {
|
||||
throw new IllegalArgumentException(String.format(
|
||||
"Invalid SDK service ID value, `%s`: %s",
|
||||
serviceId,
|
||||
String.join(";", messages)));
|
||||
}
|
||||
}
|
||||
|
||||
private Optional<ValidationEvent> validateService(ServiceShape service, ServiceTrait trait) {
|
||||
var value = trait.getSdkId();
|
||||
|
||||
try {
|
||||
validateServiceId(value);
|
||||
return Optional.empty();
|
||||
} catch (IllegalArgumentException e) {
|
||||
return Optional.of(error(service, trait, e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean containsCompanyName(String value) {
|
||||
return COMPANY_NAMES.stream().anyMatch(value::contains);
|
||||
}
|
||||
|
||||
private static Optional<String> endsWithForbiddenWord(String value) {
|
||||
var lowercase = value.toLowerCase(Locale.US);
|
||||
|
||||
return DISALLOWED_ENDINGS.stream()
|
||||
.filter(lowercase::endsWith)
|
||||
.findFirst();
|
||||
}
|
||||
|
||||
private static boolean validForPattern(String value) {
|
||||
return SERVICE_ID_PATTERN.matcher(value).find();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,271 @@
|
|||
/*
|
||||
* Copyright 2019 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.smithy.aws.traits;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
import java.util.logging.Logger;
|
||||
import software.amazon.smithy.model.SmithyBuilder;
|
||||
import software.amazon.smithy.model.SourceException;
|
||||
import software.amazon.smithy.model.ToSmithyBuilder;
|
||||
import software.amazon.smithy.model.node.Node;
|
||||
import software.amazon.smithy.model.node.ObjectNode;
|
||||
import software.amazon.smithy.model.node.StringNode;
|
||||
import software.amazon.smithy.model.shapes.ShapeId;
|
||||
import software.amazon.smithy.model.traits.AbstractTrait;
|
||||
import software.amazon.smithy.model.traits.AbstractTraitBuilder;
|
||||
import software.amazon.smithy.model.traits.TraitService;
|
||||
|
||||
/**
|
||||
* Registers a service as an AWS service. This trait is required for all AWS
|
||||
* services modeled in Smithy.
|
||||
*/
|
||||
public final class ServiceTrait extends AbstractTrait implements ToSmithyBuilder<ServiceTrait> {
|
||||
private static final Logger LOGGER = Logger.getLogger(ServiceTrait.class.getName());
|
||||
private static final String TRAIT = "aws.api#service";
|
||||
|
||||
private final String abbreviation;
|
||||
private final String cloudFormationName;
|
||||
private final String arnNamespace;
|
||||
private final String sdkId;
|
||||
private final String cloudTrailEventSource;
|
||||
|
||||
private ServiceTrait(Builder builder) {
|
||||
super(TRAIT, builder.getSourceLocation());
|
||||
this.sdkId = SmithyBuilder.requiredState("sdkId", builder.sdkId);
|
||||
this.arnNamespace = SmithyBuilder.requiredState("arnNamespace", builder.arnNamespace);
|
||||
this.cloudFormationName = SmithyBuilder.requiredState("cloudFormationName", builder.cloudFormationName);
|
||||
this.cloudTrailEventSource = SmithyBuilder.requiredState(
|
||||
"cloudTrailEventSource", builder.cloudTrailEventSource);
|
||||
this.abbreviation = builder.abbreviation;
|
||||
}
|
||||
|
||||
public static TraitService provider() {
|
||||
return TraitService.createProvider(TRAIT, (target, value) -> {
|
||||
var objectNode = value.expectObjectNode();
|
||||
objectNode.warnIfAdditionalProperties(Arrays.asList(
|
||||
"sdkId", "arnNamespace", "cloudFormationName", "cloudTrailEventSource", "abbreviation"));
|
||||
|
||||
var builder = builder();
|
||||
String sdkId = getOneStringValue(objectNode, "sdkId", "sdkServiceId")
|
||||
.orElseThrow(() -> new SourceException(String.format(
|
||||
"No sdkId was provided. Perhaps you could set this to %s?",
|
||||
target.getName()), value));
|
||||
builder.sdkId(sdkId);
|
||||
getOneStringValue(objectNode, "arnNamespace", "arnService")
|
||||
.ifPresent(builder::arnNamespace);
|
||||
getOneStringValue(objectNode, "cloudFormationName", "productName")
|
||||
.ifPresent(builder::cloudFormationName);
|
||||
objectNode.getStringMember("cloudTrailEventSource").map(StringNode::getValue)
|
||||
.ifPresent(builder::cloudTrailEventSource);
|
||||
objectNode.getStringMember("abbreviation").map(StringNode::getValue)
|
||||
.ifPresent(builder::abbreviation);
|
||||
return builder.build(target);
|
||||
});
|
||||
}
|
||||
|
||||
private static Optional<String> getOneStringValue(ObjectNode object, String key1, String key2) {
|
||||
return object.getStringMember(key1)
|
||||
.or(() -> {
|
||||
var result = object.getStringMember(key2);
|
||||
if (result.isPresent()) {
|
||||
LOGGER.warning(() -> "The `" + TRAIT + "` property `" + key2 + "` is deprecated. Use `"
|
||||
+ key1 + "` instead.");
|
||||
}
|
||||
return result;
|
||||
})
|
||||
.map(StringNode::getValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Creates a builder used to build a {@link ServiceTrait}.
|
||||
*/
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the AWS ARN service namespace of the service.
|
||||
*
|
||||
* <p>If not set, this value defaults to the name of the service shape
|
||||
* converted to lowercase. This value is combined with resources contained
|
||||
* within the service to form ARNs for resources. Only resources that
|
||||
* explicitly define the 'aws.api#arnTemplate' trait are assigned ARNs,
|
||||
* and their relative ARNs are combined with the service's arnNamespace to
|
||||
* form an ARN.
|
||||
*
|
||||
* @return Returns the ARN service name (e.g., "route53").
|
||||
*/
|
||||
public String getArnNamespace() {
|
||||
return arnNamespace;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the AWS CloudFormation service name.
|
||||
*
|
||||
* <p>When not set, this value defaults to the name of the service shape.
|
||||
*
|
||||
* @return Returns the optionally present AWS CloudFormation type prefix.
|
||||
*/
|
||||
public String getCloudFormationName() {
|
||||
return cloudFormationName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the SDK service ID.
|
||||
*
|
||||
* <p>This value is used to generate SDK class names.
|
||||
*
|
||||
* @return Returns the AWS SDK service ID value.
|
||||
*/
|
||||
public String getSdkId() {
|
||||
return sdkId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the CloudTrail event source name of the service.
|
||||
*
|
||||
* @return Returns the event source name.
|
||||
*/
|
||||
public String getCloudTrailEventSource() {
|
||||
return cloudTrailEventSource;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the abbreviated name of the service (if available).
|
||||
*
|
||||
* @return Returns the service abbreviation.
|
||||
*/
|
||||
public Optional<String> getAbbreviation() {
|
||||
return Optional.ofNullable(abbreviation);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder toBuilder() {
|
||||
return new Builder()
|
||||
.sdkId(sdkId)
|
||||
.sourceLocation(getSourceLocation())
|
||||
.cloudFormationName(cloudFormationName)
|
||||
.arnNamespace(arnNamespace)
|
||||
.cloudTrailEventSource(cloudTrailEventSource)
|
||||
.abbreviation(abbreviation);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Node createNode() {
|
||||
return Node.objectNode()
|
||||
.withMember("sdkId", Node.from(sdkId))
|
||||
.withMember("arnNamespace", Node.from(getArnNamespace()))
|
||||
.withMember("cloudFormationName", Node.from(getCloudFormationName()))
|
||||
.withMember("cloudTrailEventSource", Node.from(getCloudTrailEventSource()))
|
||||
.withOptionalMember("abbreviation", getAbbreviation().map(Node::from));
|
||||
}
|
||||
|
||||
/** Builder for {@link ServiceTrait}. */
|
||||
public static final class Builder extends AbstractTraitBuilder<ServiceTrait, Builder> {
|
||||
private String abbreviation;
|
||||
private String sdkId;
|
||||
private String cloudFormationName;
|
||||
private String arnNamespace;
|
||||
private String cloudTrailEventSource;
|
||||
|
||||
private Builder() {}
|
||||
|
||||
@Override
|
||||
public ServiceTrait build() {
|
||||
return new ServiceTrait(this);
|
||||
}
|
||||
|
||||
public ServiceTrait build(ShapeId target) {
|
||||
// Fill in default values if they weren't set.
|
||||
if (arnNamespace == null) {
|
||||
arnNamespace(target.getName().toLowerCase(Locale.US));
|
||||
}
|
||||
|
||||
if (cloudFormationName == null) {
|
||||
cloudFormationName(target.getName());
|
||||
}
|
||||
|
||||
if (cloudTrailEventSource == null) {
|
||||
cloudTrailEventSource = arnNamespace + ".amazonaws.com";
|
||||
}
|
||||
|
||||
return new ServiceTrait(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the AWS CloudFormation resource type service name.
|
||||
*
|
||||
* <p>Must match the following regex {@code ^[a-z0-9.\-]{1,63}$}
|
||||
*
|
||||
* @param cloudFormationName AWS CloudFormation resource type service name.
|
||||
* @return Returns the builder.
|
||||
*/
|
||||
public Builder cloudFormationName(String cloudFormationName) {
|
||||
this.cloudFormationName = cloudFormationName;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the ARN service namespace of the service.
|
||||
*
|
||||
* <p>Must match the following regex: {@code ^[A-Z][A-Za-z0-9]+$}
|
||||
*
|
||||
* @param arnNamespace ARN service namespace to set.
|
||||
* @return Returns the builder.
|
||||
*/
|
||||
public Builder arnNamespace(String arnNamespace) {
|
||||
this.arnNamespace = arnNamespace;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the SDK service ID trait used to control client class names.
|
||||
*
|
||||
* <p>Must match the following regex: {@code ^[a-zA-Z][a-zA-Z0-9]*( [a-zA-Z0-9]+)*$}
|
||||
*
|
||||
* @param sdkId SDK service ID to set.
|
||||
* @return Returns the builder.
|
||||
*/
|
||||
public Builder sdkId(String sdkId) {
|
||||
this.sdkId = sdkId;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the CloudTrail event source name of the service.
|
||||
*
|
||||
* @param cloudTrailEventSource CloudTrail event source name of the service.
|
||||
* @return Returns the builder.
|
||||
*/
|
||||
public Builder cloudTrailEventSource(String cloudTrailEventSource) {
|
||||
this.cloudTrailEventSource = cloudTrailEventSource;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the abbreviated name of the service.
|
||||
*
|
||||
* @param abbreviation Abbreviated service name.
|
||||
* @return Returns the builder.
|
||||
*/
|
||||
public Builder abbreviation(String abbreviation) {
|
||||
this.abbreviation = abbreviation;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* Copyright 2019 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.smithy.aws.traits;
|
||||
|
||||
import java.util.List;
|
||||
import software.amazon.smithy.model.FromSourceLocation;
|
||||
import software.amazon.smithy.model.SourceLocation;
|
||||
import software.amazon.smithy.model.ToSmithyBuilder;
|
||||
import software.amazon.smithy.model.traits.StringListTrait;
|
||||
import software.amazon.smithy.model.traits.TraitService;
|
||||
|
||||
/**
|
||||
* Indicates that the payload of an operation is not to be signed.
|
||||
*
|
||||
* <p>Providing a list of strings will limit the effect of this trait to
|
||||
* only specific authentication schemes by name.
|
||||
*/
|
||||
public final class UnsignedPayload extends StringListTrait implements ToSmithyBuilder<UnsignedPayload> {
|
||||
private static final String TRAIT = "aws.api#unsignedPayload";
|
||||
|
||||
public UnsignedPayload(List<String> values, FromSourceLocation sourceLocation) {
|
||||
super(TRAIT, values, sourceLocation);
|
||||
}
|
||||
|
||||
public UnsignedPayload(FromSourceLocation sourceLocation) {
|
||||
this(List.of(), sourceLocation);
|
||||
}
|
||||
|
||||
public UnsignedPayload() {
|
||||
this(List.of(), SourceLocation.NONE);
|
||||
}
|
||||
|
||||
public static TraitService provider() {
|
||||
return TraitService.createStringListProvider(TRAIT, UnsignedPayload::new);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder toBuilder() {
|
||||
return builder().values(getValues());
|
||||
}
|
||||
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
public static final class Builder extends StringListTrait.Builder<UnsignedPayload, Builder> {
|
||||
private Builder() {}
|
||||
|
||||
@Override
|
||||
public UnsignedPayload build() {
|
||||
return new UnsignedPayload(getValues(), getSourceLocation());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* Copyright 2019 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.smithy.aws.traits.apigateway;
|
||||
|
||||
import software.amazon.smithy.model.FromSourceLocation;
|
||||
import software.amazon.smithy.model.SourceLocation;
|
||||
import software.amazon.smithy.model.traits.StringTrait;
|
||||
import software.amazon.smithy.model.traits.TraitService;
|
||||
|
||||
public final class ApiKeySourceTrait extends StringTrait {
|
||||
public static final String NAME = "aws.apigateway#apiKeySource";
|
||||
|
||||
public ApiKeySourceTrait(String value, FromSourceLocation sourceLocation) {
|
||||
super(NAME, value, sourceLocation);
|
||||
}
|
||||
|
||||
public ApiKeySourceTrait(String value) {
|
||||
this(value, SourceLocation.NONE);
|
||||
}
|
||||
|
||||
public static TraitService provider() {
|
||||
return TraitService.createStringProvider(NAME, ApiKeySourceTrait::new);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,323 @@
|
|||
package software.amazon.smithy.aws.traits.apigateway;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import software.amazon.smithy.model.SmithyBuilder;
|
||||
import software.amazon.smithy.model.ToSmithyBuilder;
|
||||
import software.amazon.smithy.model.node.Node;
|
||||
import software.amazon.smithy.model.node.NumberNode;
|
||||
import software.amazon.smithy.model.node.ObjectNode;
|
||||
import software.amazon.smithy.model.node.StringNode;
|
||||
import software.amazon.smithy.model.node.ToNode;
|
||||
|
||||
/**
|
||||
* Represents an API Gateway authorizer.
|
||||
*
|
||||
* @see AuthorizersTrait
|
||||
*/
|
||||
public final class Authorizer implements ToNode, ToSmithyBuilder<Authorizer> {
|
||||
private static final String CLIENT_TYPE_KEY = "clientType";
|
||||
private static final String TYPE_KEY = "type";
|
||||
private static final String URI_KEY = "uri";
|
||||
private static final String CREDENTIALS_KEY = "credentials";
|
||||
private static final String IDENTITY_SOURCE_KEY = "identitySource";
|
||||
private static final String IDENTITY_VALIDATION_EXPRESSION_KEY = "identityValidationExpression";
|
||||
private static final String RESULT_TTL_IN_SECONDS = "resultTtlInSeconds";
|
||||
private static final List<String> PROPERTIES = List.of(
|
||||
CLIENT_TYPE_KEY, TYPE_KEY, URI_KEY, CREDENTIALS_KEY, IDENTITY_SOURCE_KEY,
|
||||
IDENTITY_VALIDATION_EXPRESSION_KEY, RESULT_TTL_IN_SECONDS);
|
||||
|
||||
private final String clientType;
|
||||
private final String type;
|
||||
private final String uri;
|
||||
private final String credentials;
|
||||
private final String identitySource;
|
||||
private final String identityValidationExpression;
|
||||
private final Integer resultTtlInSeconds;
|
||||
|
||||
private Authorizer(Builder builder) {
|
||||
clientType = builder.clientType;
|
||||
type = SmithyBuilder.requiredState(TYPE_KEY, builder.type);
|
||||
uri = SmithyBuilder.requiredState(URI_KEY, builder.uri);
|
||||
credentials = builder.credentials;
|
||||
identitySource = builder.identitySource;
|
||||
identityValidationExpression = builder.identityValidationExpression;
|
||||
resultTtlInSeconds = builder.resultTtlInSeconds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a builder for an Authorizer.
|
||||
*
|
||||
* @return Returns the created builder.
|
||||
*/
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the client authentication type.
|
||||
*
|
||||
* <p>A value identifying the category of authorizer, such
|
||||
* as "oauth2" or "awsSigv4."
|
||||
*
|
||||
* @return Returns the optionally defined client authentication type.
|
||||
*/
|
||||
public Optional<String> getClientType() {
|
||||
return Optional.ofNullable(clientType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the type of the authorizer.
|
||||
*
|
||||
* <p>This is a required property and the value must be "token",
|
||||
* for an authorizer with the caller identity embedded in an
|
||||
* authorization token, or "request", for an authorizer with the
|
||||
* caller identity contained in request parameters.
|
||||
*
|
||||
* @return Returns the authorizer type.
|
||||
*/
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the Uniform Resource Identifier (URI) of the authorizer
|
||||
* Lambda function.
|
||||
*
|
||||
* @return Returns the Lambda URI.
|
||||
*/
|
||||
public String getUri() {
|
||||
return uri;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the Credentials required for invoking the authorizer, if any, in
|
||||
* the form of an ARN of an IAM execution role.
|
||||
*
|
||||
* <p>For example, "arn:aws:iam::account-id:IAM_role".
|
||||
*
|
||||
* @return Returns the optional credential ARN.
|
||||
*/
|
||||
public Optional<String> getCredentials() {
|
||||
return Optional.ofNullable(credentials);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the comma-separated list of mapping expressions of the request
|
||||
* parameters as the identity source.
|
||||
*
|
||||
* <p>This property is only applicable for the authorizer of the
|
||||
* "request" type only.
|
||||
*
|
||||
* @return Returns the optional identity source string.
|
||||
*/
|
||||
public Optional<String> getIdentitySource() {
|
||||
return Optional.ofNullable(identitySource);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the regular expression for validating the token as the incoming
|
||||
* identity. For example, {@code "^x-[a-z]+"}.
|
||||
*
|
||||
* @return Returns the identity regular expression.
|
||||
*/
|
||||
public Optional<String> getIdentityValidationExpression() {
|
||||
return Optional.ofNullable(identityValidationExpression);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the number of seconds during which the resulting IAM policy
|
||||
* is cached.
|
||||
*
|
||||
* @return Returns the cache amount in seconds.
|
||||
*/
|
||||
public Optional<Integer> getResultTtlInSeconds() {
|
||||
return Optional.ofNullable(resultTtlInSeconds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder toBuilder() {
|
||||
return builder()
|
||||
.clientType(clientType)
|
||||
.type(type)
|
||||
.uri(uri)
|
||||
.credentials(credentials)
|
||||
.identitySource(identitySource)
|
||||
.identityValidationExpression(identityValidationExpression)
|
||||
.resultTtlInSeconds(resultTtlInSeconds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Node toNode() {
|
||||
return Node.objectNodeBuilder()
|
||||
.withOptionalMember(CLIENT_TYPE_KEY, getClientType().map(Node::from))
|
||||
.withMember(TYPE_KEY, Node.from(getType()))
|
||||
.withMember(URI_KEY, Node.from(getUri()))
|
||||
.withOptionalMember(CREDENTIALS_KEY, getCredentials().map(Node::from))
|
||||
.withOptionalMember(IDENTITY_SOURCE_KEY, getIdentitySource().map(Node::from))
|
||||
.withOptionalMember(IDENTITY_VALIDATION_EXPRESSION_KEY,
|
||||
getIdentityValidationExpression().map(Node::from))
|
||||
.withOptionalMember(RESULT_TTL_IN_SECONDS, getResultTtlInSeconds().map(Node::from))
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
} else if (!(o instanceof Authorizer)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Authorizer that = (Authorizer) o;
|
||||
return Objects.equals(clientType, that.clientType)
|
||||
&& type.equals(that.type)
|
||||
&& uri.equals(that.uri)
|
||||
&& Objects.equals(credentials, that.credentials)
|
||||
&& Objects.equals(identitySource, that.identitySource)
|
||||
&& Objects.equals(identityValidationExpression, that.identityValidationExpression)
|
||||
&& Objects.equals(resultTtlInSeconds, that.resultTtlInSeconds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(clientType, type, uri);
|
||||
}
|
||||
|
||||
static Authorizer fromNode(ObjectNode node) {
|
||||
node.warnIfAdditionalProperties(PROPERTIES);
|
||||
Builder builder = builder();
|
||||
node.getStringMember(CLIENT_TYPE_KEY)
|
||||
.map(StringNode::getValue)
|
||||
.ifPresent(builder::clientType);
|
||||
builder.type(node.expectMember(TYPE_KEY).expectStringNode().getValue());
|
||||
builder.uri(node.expectMember(URI_KEY).expectStringNode().getValue());
|
||||
node.getStringMember(CREDENTIALS_KEY)
|
||||
.map(StringNode::getValue)
|
||||
.ifPresent(builder::credentials);
|
||||
node.getStringMember(IDENTITY_SOURCE_KEY)
|
||||
.map(StringNode::getValue)
|
||||
.ifPresent(builder::identitySource);
|
||||
node.getStringMember(IDENTITY_VALIDATION_EXPRESSION_KEY)
|
||||
.map(StringNode::getValue)
|
||||
.ifPresent(builder::identityValidationExpression);
|
||||
node.getNumberMember(RESULT_TTL_IN_SECONDS)
|
||||
.map(NumberNode::getValue)
|
||||
.map(Number::intValue)
|
||||
.ifPresent(builder::resultTtlInSeconds);
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder used to create an {@link Authorizer}.
|
||||
*/
|
||||
public static final class Builder implements SmithyBuilder<Authorizer> {
|
||||
private String clientType;
|
||||
private String type;
|
||||
private String uri;
|
||||
private String credentials;
|
||||
private String identitySource;
|
||||
private String identityValidationExpression;
|
||||
private Integer resultTtlInSeconds;
|
||||
|
||||
@Override
|
||||
public Authorizer build() {
|
||||
return new Authorizer(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the client authentication type.
|
||||
*
|
||||
* @param clientType Client authentication type such as "oauth2" or "awsSigv4."
|
||||
* @return Returns the builder.
|
||||
*/
|
||||
public Builder clientType(String clientType) {
|
||||
this.clientType = clientType;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the type of the authorizer.
|
||||
*
|
||||
* <p>This is a required property and the value must be "token",
|
||||
* for an authorizer with the caller identity embedded in an
|
||||
* authorization token, or "request", for an authorizer with the
|
||||
* caller identity contained in request parameters.
|
||||
*
|
||||
* @param type authorizer type.
|
||||
* @return Returns the builder.
|
||||
*/
|
||||
public Builder type(String type) {
|
||||
this.type = type;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Uniform Resource Identifier (URI) of the authorizer
|
||||
* Lambda function.
|
||||
*
|
||||
* <p>The syntax is as follows:
|
||||
*
|
||||
* @param uri the Lambda URI to set.
|
||||
* @return Returns the builder.
|
||||
*/
|
||||
public Builder uri(String uri) {
|
||||
this.uri = uri;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Credentials required for invoking the authorizer, if any, in
|
||||
* the form of an ARN of an IAM execution role.
|
||||
*
|
||||
* <p>For example, "arn:aws:iam::account-id:IAM_role".
|
||||
*
|
||||
* @param credentials Credentials ARN to set.
|
||||
* @return Returns the builder.
|
||||
*/
|
||||
public Builder credentials(String credentials) {
|
||||
this.credentials = credentials;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the comma-separated list of mapping expressions of the request
|
||||
* parameters as the identity source.
|
||||
*
|
||||
* <p>This property is only applicable for the authorizer of the
|
||||
* "request" type only.
|
||||
*
|
||||
* @param identitySource Identity source CSV to set.
|
||||
* @return Returns the builder.
|
||||
*/
|
||||
public Builder identitySource(String identitySource) {
|
||||
this.identitySource = identitySource;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the regular expression for validating the token as the incoming
|
||||
* identity. For example, {@code "^x-[a-z]+"}.
|
||||
*
|
||||
* @param identityValidationExpression Expression to set.
|
||||
* @return Returns the builder.
|
||||
*/
|
||||
public Builder identityValidationExpression(String identityValidationExpression) {
|
||||
this.identityValidationExpression = identityValidationExpression;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the number of seconds during which the resulting IAM policy
|
||||
* is cached.
|
||||
*
|
||||
* @param resultTtlInSeconds Number of seconds to cache.
|
||||
* @return Returns the builder.
|
||||
*/
|
||||
public Builder resultTtlInSeconds(Integer resultTtlInSeconds) {
|
||||
this.resultTtlInSeconds = resultTtlInSeconds;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,151 @@
|
|||
package software.amazon.smithy.aws.traits.apigateway;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import software.amazon.smithy.model.ToSmithyBuilder;
|
||||
import software.amazon.smithy.model.node.Node;
|
||||
import software.amazon.smithy.model.node.ObjectNode;
|
||||
import software.amazon.smithy.model.traits.AbstractTrait;
|
||||
import software.amazon.smithy.model.traits.AbstractTraitBuilder;
|
||||
import software.amazon.smithy.model.traits.TraitService;
|
||||
|
||||
/**
|
||||
* Defines a map of API Gateway {@code x-amazon-apigateway-authorizer}
|
||||
* values that correspond to Smithy authorization definitions.
|
||||
*
|
||||
* <p>The key in each key-value pair of the {@code aws.apigateway#authorizers}
|
||||
* trait must correspond to the name of an authorization scheme of the
|
||||
* service the trait is bound to. When used to generate and OpenAPI model,
|
||||
* the {@code aws.apigateway#authorizers} trait is used to add the
|
||||
* {@code x-amazon-apigateway-authorizer} OpenAPI extension to the
|
||||
* generated security scheme.
|
||||
*
|
||||
* @see <a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-swagger-extensions-authorizer.html">API Gateway Authorizers</a>
|
||||
*/
|
||||
public final class AuthorizersTrait extends AbstractTrait implements ToSmithyBuilder<AuthorizersTrait> {
|
||||
public static final String NAME = "aws.apigateway#authorizers";
|
||||
|
||||
private final Map<String, Authorizer> authorizers;
|
||||
|
||||
private AuthorizersTrait(Builder builder) {
|
||||
super(NAME, builder.getSourceLocation());
|
||||
authorizers = Map.copyOf(builder.authorizers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Service provider for the AuthorizersTrait.
|
||||
*
|
||||
* @return Returns the TraitService provider.
|
||||
*/
|
||||
public static TraitService provider() {
|
||||
return TraitService.createProvider(NAME, (target, value) -> {
|
||||
Builder builder = builder().sourceLocation(value);
|
||||
value.expectObjectNode().getMembers().forEach((key, node) -> {
|
||||
var authorizer = Authorizer.fromNode(node.expectObjectNode());
|
||||
builder.putAuthorizer(key.getValue(), authorizer);
|
||||
});
|
||||
return builder.build();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a builder for the trait.
|
||||
*
|
||||
* @return Returns the created builder.
|
||||
*/
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a specific authorizer by name.
|
||||
*
|
||||
* @param name Name of the authorizer to get.
|
||||
* @return Returns the optionally found authorizer.
|
||||
*/
|
||||
public Optional<Authorizer> getAuthorizer(String name) {
|
||||
return Optional.ofNullable(authorizers.get(name));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an immuatable map of authorizer names to their definitions.
|
||||
*
|
||||
* @return Returns the authorizers.
|
||||
*/
|
||||
public Map<String, Authorizer> getAllAuthorizers() {
|
||||
return authorizers;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder toBuilder() {
|
||||
return builder().authorizers(authorizers);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Node createNode() {
|
||||
return authorizers.entrySet().stream()
|
||||
.sorted(Comparator.comparing(Map.Entry::getKey))
|
||||
.collect(ObjectNode.collectStringKeys(Map.Entry::getKey, Map.Entry::getValue));
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds an {@link AuthorizersTrait}.
|
||||
*/
|
||||
public static final class Builder extends AbstractTraitBuilder<AuthorizersTrait, Builder> {
|
||||
private final Map<String, Authorizer> authorizers = new HashMap<>();
|
||||
|
||||
@Override
|
||||
public AuthorizersTrait build() {
|
||||
return new AuthorizersTrait(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an authorizer.
|
||||
*
|
||||
* @param name Name of the authorizer that must correspond to
|
||||
* an authentication scheme name.
|
||||
* @param authorizer Authorizer definition.
|
||||
* @return Returns the builder.
|
||||
*/
|
||||
public Builder putAuthorizer(String name, Authorizer authorizer) {
|
||||
authorizers.put(name, Objects.requireNonNull(authorizer));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces all of the authorizers with the given map.
|
||||
*
|
||||
* @param authorizers Map of authorizer names to their definitions.
|
||||
* @return Returns the builder.
|
||||
*/
|
||||
public Builder authorizers(Map<String, Authorizer> authorizers) {
|
||||
clearAuthorizers();
|
||||
authorizers.forEach(this::putAuthorizer);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes an authorizer by name.
|
||||
*
|
||||
* @param name Name of the authorizer to remove.
|
||||
* @return Returns the builder.
|
||||
*/
|
||||
public Builder removeAuthorizer(String name) {
|
||||
authorizers.remove(name);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all of the authorizers in the builder.
|
||||
*
|
||||
* @return Returns the builder.
|
||||
*/
|
||||
public Builder clearAuthorizers() {
|
||||
authorizers.clear();
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
package software.amazon.smithy.aws.traits.apigateway;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
import software.amazon.smithy.model.Model;
|
||||
import software.amazon.smithy.model.shapes.ServiceShape;
|
||||
import software.amazon.smithy.model.traits.AuthenticationTrait;
|
||||
import software.amazon.smithy.model.validation.AbstractValidator;
|
||||
import software.amazon.smithy.model.validation.ValidationEvent;
|
||||
import software.amazon.smithy.model.validation.ValidationUtils;
|
||||
|
||||
/**
|
||||
* Each authorizers must match one of the authentication schemes on the service.
|
||||
*/
|
||||
public class AuthorizersTraitValidator extends AbstractValidator {
|
||||
@Override
|
||||
public List<ValidationEvent> validate(Model model) {
|
||||
return model.getShapeIndex().shapes(ServiceShape.class)
|
||||
.flatMap(this::validateService)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private Stream<ValidationEvent> validateService(ServiceShape service) {
|
||||
var schemeNames = service.getTrait(AuthenticationTrait.class)
|
||||
.map(AuthenticationTrait::getAuthenticationSchemes)
|
||||
.map(Map::keySet)
|
||||
.orElse(Set.of());
|
||||
var authorizerNames = service.getTrait(AuthorizersTrait.class)
|
||||
.map(AuthorizersTrait::getAllAuthorizers)
|
||||
.map(map -> new HashSet<>(map.keySet()))
|
||||
.orElseGet(HashSet::new);
|
||||
|
||||
authorizerNames.removeAll(schemeNames);
|
||||
return authorizerNames.stream().map(name -> error(service, String.format(
|
||||
"Invalid `%s` entry `%s` does not match one of the `authentication` schemes defined "
|
||||
+ "on the service: [%s]",
|
||||
AuthorizersTrait.NAME, name, ValidationUtils.tickedList(schemeNames))));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* Copyright 2019 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.smithy.aws.traits.apigateway;
|
||||
|
||||
import software.amazon.smithy.model.FromSourceLocation;
|
||||
import software.amazon.smithy.model.SourceLocation;
|
||||
import software.amazon.smithy.model.traits.StringTrait;
|
||||
import software.amazon.smithy.model.traits.TraitService;
|
||||
|
||||
public final class RequestValidatorTrait extends StringTrait {
|
||||
public static final String NAME = "aws.apigateway#requestValidator";
|
||||
|
||||
public RequestValidatorTrait(String value, FromSourceLocation sourceLocation) {
|
||||
super(NAME, value, sourceLocation);
|
||||
}
|
||||
|
||||
public RequestValidatorTrait(String value) {
|
||||
this(value, SourceLocation.NONE);
|
||||
}
|
||||
|
||||
public static TraitService provider() {
|
||||
return TraitService.createStringProvider(NAME, RequestValidatorTrait::new);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* Copyright 2019 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.smithy.aws.traits.iam;
|
||||
|
||||
import software.amazon.smithy.model.SourceLocation;
|
||||
import software.amazon.smithy.model.traits.StringTrait;
|
||||
import software.amazon.smithy.model.traits.TraitService;
|
||||
|
||||
/**
|
||||
* Defines the description of what providing access to an operation entails.
|
||||
*/
|
||||
public final class ActionPermissionDescriptionTrait extends StringTrait {
|
||||
private static final String TRAIT = "aws.iam#actionPermissionDescription";
|
||||
|
||||
private ActionPermissionDescriptionTrait(String value, SourceLocation sourceLocation) {
|
||||
super(TRAIT, value, sourceLocation);
|
||||
}
|
||||
|
||||
public static TraitService provider() {
|
||||
return TraitService.createStringProvider(TRAIT, ActionPermissionDescriptionTrait::new);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,143 @@
|
|||
/*
|
||||
* Copyright 2019 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.smithy.aws.traits.iam;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import software.amazon.smithy.model.SmithyBuilder;
|
||||
import software.amazon.smithy.model.ToSmithyBuilder;
|
||||
import software.amazon.smithy.model.node.Node;
|
||||
import software.amazon.smithy.model.node.ObjectNode;
|
||||
import software.amazon.smithy.model.node.StringNode;
|
||||
import software.amazon.smithy.model.node.ToNode;
|
||||
|
||||
public final class ConditionKeyDefinition implements ToNode, ToSmithyBuilder<ConditionKeyDefinition> {
|
||||
private static final String TYPE = "type";
|
||||
private static final String DOCUMENTATION = "documentation";
|
||||
private static final String EXTERNAL_DOCUMENTATION = "externalDocumentation";
|
||||
private static final List<String> SUPPORTED_PROPERTIES = List.of(TYPE, DOCUMENTATION, EXTERNAL_DOCUMENTATION);
|
||||
|
||||
private final String type;
|
||||
private final String documentation;
|
||||
private final String externalDocumentation;
|
||||
|
||||
private ConditionKeyDefinition(Builder builder) {
|
||||
type = SmithyBuilder.requiredState(TYPE, builder.type);
|
||||
documentation = builder.documentation;
|
||||
externalDocumentation = builder.externalDocumentation;
|
||||
}
|
||||
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
public static ConditionKeyDefinition fromNode(Node value) {
|
||||
ObjectNode objectNode = value.expectObjectNode();
|
||||
objectNode.warnIfAdditionalProperties(SUPPORTED_PROPERTIES);
|
||||
Builder builder = builder()
|
||||
.type(objectNode.expectMember(TYPE).expectStringNode().getValue());
|
||||
objectNode.getStringMember(DOCUMENTATION).map(StringNode::getValue)
|
||||
.ifPresent(builder::documentation);
|
||||
objectNode.getStringMember(EXTERNAL_DOCUMENTATION).map(StringNode::getValue)
|
||||
.ifPresent(builder::externalDocumentation);
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The IAM policy type of the value that will supplied for this condition key.
|
||||
*/
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return A short description of the role of the condition key.
|
||||
*/
|
||||
public Optional<String> getDocumentation() {
|
||||
return Optional.ofNullable(documentation);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return A URL to the documentation page.
|
||||
*/
|
||||
public Optional<String> getExternalDocumentation() {
|
||||
return Optional.ofNullable(externalDocumentation);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SmithyBuilder<ConditionKeyDefinition> toBuilder() {
|
||||
return builder()
|
||||
.documentation(documentation)
|
||||
.externalDocumentation(externalDocumentation)
|
||||
.type(type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Node toNode() {
|
||||
return Node.objectNodeBuilder()
|
||||
.withMember(TYPE, Node.from(type))
|
||||
.withOptionalMember(DOCUMENTATION, getDocumentation().map(Node::from))
|
||||
.withOptionalMember(EXTERNAL_DOCUMENTATION, getExternalDocumentation().map(Node::from))
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
} else if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ConditionKeyDefinition that = (ConditionKeyDefinition) o;
|
||||
return Objects.equals(type, that.type)
|
||||
&& Objects.equals(documentation, that.documentation)
|
||||
&& Objects.equals(externalDocumentation, that.externalDocumentation);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(type, documentation, externalDocumentation);
|
||||
}
|
||||
|
||||
public static final class Builder implements SmithyBuilder<ConditionKeyDefinition> {
|
||||
private String type;
|
||||
private String documentation;
|
||||
private String externalDocumentation;
|
||||
|
||||
@Override
|
||||
public ConditionKeyDefinition build() {
|
||||
return new ConditionKeyDefinition(this);
|
||||
}
|
||||
|
||||
public Builder type(String type) {
|
||||
this.type = type;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder documentation(String documentation) {
|
||||
this.documentation = documentation;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder externalDocumentation(String externalDocumentation) {
|
||||
this.externalDocumentation = externalDocumentation;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,244 @@
|
|||
/*
|
||||
* Copyright 2019 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.smithy.aws.traits.iam;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import software.amazon.smithy.aws.traits.ArnReferenceTrait;
|
||||
import software.amazon.smithy.aws.traits.ServiceTrait;
|
||||
import software.amazon.smithy.model.Model;
|
||||
import software.amazon.smithy.model.knowledge.IdentifierBindingIndex;
|
||||
import software.amazon.smithy.model.knowledge.KnowledgeIndex;
|
||||
import software.amazon.smithy.model.shapes.ResourceShape;
|
||||
import software.amazon.smithy.model.shapes.ServiceShape;
|
||||
import software.amazon.smithy.model.shapes.Shape;
|
||||
import software.amazon.smithy.model.shapes.ShapeId;
|
||||
import software.amazon.smithy.model.shapes.ShapeIndex;
|
||||
import software.amazon.smithy.model.shapes.ToShapeId;
|
||||
|
||||
/**
|
||||
* Provides an index of condition keys for a service, including any condition
|
||||
* keys inferred from resource identifiers.
|
||||
*/
|
||||
public final class ConditionKeysIndex implements KnowledgeIndex {
|
||||
private static final String STRING_TYPE = "String";
|
||||
private static final String ARN_TYPE = "ARN";
|
||||
|
||||
private final Map<ShapeId, Map<String, ConditionKeyDefinition>> serviceConditionKeys = new HashMap<>();
|
||||
private final Map<ShapeId, Map<ShapeId, Set<String>>> resourceConditionKeys = new HashMap<>();
|
||||
|
||||
public ConditionKeysIndex(Model model) {
|
||||
var index = model.getShapeIndex();
|
||||
var bindingIndex = model.getKnowledge(IdentifierBindingIndex.class);
|
||||
|
||||
index.shapes(ServiceShape.class).forEach(service -> {
|
||||
service.getTrait(ServiceTrait.class).ifPresent(trait -> {
|
||||
// Copy over the explicitly defined condition keys into the service map.
|
||||
// This will be mutated when adding inferred resource condition keys.
|
||||
serviceConditionKeys.put(service.getId(), new HashMap<>(
|
||||
service.getTrait(DefineConditionKeysTrait.class)
|
||||
.map(DefineConditionKeysTrait::getConditionKeys)
|
||||
.orElse(Map.of())));
|
||||
resourceConditionKeys.put(service.getId(), new HashMap<>());
|
||||
|
||||
// Defines the scoping of any derived condition keys.
|
||||
String arnRoot = trait.getArnNamespace();
|
||||
|
||||
// Compute the keys of child resources.
|
||||
service.getResources().stream().flatMap(id -> index.getShape(id).stream()).forEach(resource -> {
|
||||
compute(index, bindingIndex, service.getId(), arnRoot, resource, null, Set.of());
|
||||
});
|
||||
|
||||
// Compute the keys of operations of the service.
|
||||
service.getOperations().stream().flatMap(id -> index.getShape(id).stream()).forEach(operation -> {
|
||||
compute(index, bindingIndex, service.getId(), arnRoot, operation, null, Set.of());
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all of the explicit and inferred condition keys used in the entire service.
|
||||
*
|
||||
* <p>The result does not include global condition keys like "aws:accountId".
|
||||
* Use {@link #getConditionKeyNames} to find all of the condition keys used
|
||||
* but not necessarily defined for a service.
|
||||
*
|
||||
* @param service Service shape/shapeId to get.
|
||||
* @return Returns the conditions keys of the service or an empty map when not found.
|
||||
*/
|
||||
public Map<String, ConditionKeyDefinition> getDefinedConditionKeys(ToShapeId service) {
|
||||
return Collections.unmodifiableMap(serviceConditionKeys.getOrDefault(service.toShapeId(), Map.of()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all of the condition key names used in a service.
|
||||
*
|
||||
* @param service Service shape/shapeId use to scope the result.
|
||||
* @return Returns the conditions keys of the service or an empty map when not found.
|
||||
*/
|
||||
public Set<String> getConditionKeyNames(ToShapeId service) {
|
||||
return resourceConditionKeys.getOrDefault(service.toShapeId(), Map.of())
|
||||
.values().stream()
|
||||
.flatMap(Set::stream)
|
||||
.collect(Collectors.toUnmodifiableSet());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all of the defined condition keys used in an operation or resource, including
|
||||
* any inferred keys and keys inherited by parent resource bindings.
|
||||
*
|
||||
* @param service Service shape/shapeId use to scope the result.
|
||||
* @param resourceOrOperation Resource or operation shape/shapeId
|
||||
* @return Returns the conditions keys of the service or an empty map when not found.
|
||||
*/
|
||||
public Set<String> getConditionKeyNames(ToShapeId service, ToShapeId resourceOrOperation) {
|
||||
ShapeId serviceId = service.toShapeId();
|
||||
ShapeId subjectId = resourceOrOperation.toShapeId();
|
||||
return Collections.unmodifiableSet(
|
||||
resourceConditionKeys.getOrDefault(serviceId, Map.of()).getOrDefault(subjectId, Set.of()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all of the defined condition keys used in an operation or resource, including
|
||||
* any inferred keys and keys inherited by parent resource bindings.
|
||||
*
|
||||
* <p>The result does not include global condition keys like "aws:accountId".
|
||||
* Use {@link #getConditionKeyNames} to find all of the condition keys used
|
||||
* but not necessarily defined for a resource or operation.
|
||||
*
|
||||
* @param service Service shape/shapeId use to scope the result.
|
||||
* @param resourceOrOperation Resource or operation shape/shapeId
|
||||
* @return Returns the conditions keys of the service or an empty map when not found.
|
||||
*/
|
||||
public Map<String, ConditionKeyDefinition> getDefinedConditionKeys(
|
||||
ToShapeId service,
|
||||
ToShapeId resourceOrOperation
|
||||
) {
|
||||
var serviceDefinitions = getDefinedConditionKeys(service);
|
||||
Map<String, ConditionKeyDefinition> definitions = new HashMap<>();
|
||||
|
||||
for (var name : getConditionKeyNames(service, resourceOrOperation)) {
|
||||
if (serviceDefinitions.containsKey(name)) {
|
||||
definitions.put(name, serviceDefinitions.get(name));
|
||||
}
|
||||
}
|
||||
|
||||
return definitions;
|
||||
}
|
||||
|
||||
private void compute(
|
||||
ShapeIndex index,
|
||||
IdentifierBindingIndex bindingIndex,
|
||||
ShapeId service,
|
||||
String arnRoot,
|
||||
Shape subject,
|
||||
ResourceShape parent,
|
||||
Set<String> parentDefinitions
|
||||
) {
|
||||
Set<String> definitions = new HashSet<>(parentDefinitions);
|
||||
resourceConditionKeys.get(service).put(subject.getId(), definitions);
|
||||
subject.getTrait(ConditionKeysTrait.class).ifPresent(trait -> definitions.addAll(trait.getValues()));
|
||||
|
||||
// Continue recursing into resources and computing keys.
|
||||
subject.asResourceShape().ifPresent(resource -> {
|
||||
// Add any inferred resource identifiers to the resource and to the service-wide definitions.
|
||||
Map<String, String> childIdentifiers = resource.hasTrait(InferConditionKeysTrait.class)
|
||||
? inferChildResourceIdentifiers(index, service, arnRoot, resource, parent)
|
||||
: Map.of();
|
||||
|
||||
// Compute the keys of each child operation.
|
||||
resource.getAllOperations().stream().flatMap(id -> index.getShape(id).stream()).forEach(child -> {
|
||||
// Only apply child identifiers to the operation if the operation binds them
|
||||
// (for example, list operations omit one or more child identifiers).
|
||||
Set<String> operationKeys = new HashSet<>(definitions);
|
||||
for (var binding : bindingIndex.getOperationBindings(resource, child).keySet()) {
|
||||
if (childIdentifiers.containsKey(binding)) {
|
||||
operationKeys.add(childIdentifiers.get(binding));
|
||||
}
|
||||
}
|
||||
compute(index, bindingIndex, service, arnRoot, child, resource, operationKeys);
|
||||
});
|
||||
|
||||
// Child resources always inherit the identifiers of the parent.
|
||||
definitions.addAll(childIdentifiers.values());
|
||||
|
||||
// Compute the keys of each child resource.
|
||||
resource.getResources().stream().flatMap(id -> index.getShape(id).stream()).forEach(child -> {
|
||||
compute(index, bindingIndex, service, arnRoot, child, resource, definitions);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private Map<String, String> inferChildResourceIdentifiers(
|
||||
ShapeIndex index,
|
||||
ShapeId service,
|
||||
String arnRoot,
|
||||
ResourceShape resource,
|
||||
ResourceShape parent
|
||||
) {
|
||||
Map<String, String> result = new HashMap<>();
|
||||
|
||||
// We want child resources to reuse parent resource context keys, so
|
||||
// extract out identifiers that were introduced by the child resource.
|
||||
Set<String> parentIds = parent == null ? Set.of() : parent.getIdentifiers().keySet();
|
||||
Set<String> childIds = new HashSet<>(resource.getIdentifiers().keySet());
|
||||
childIds.removeAll(parentIds);
|
||||
|
||||
for (var childId : childIds) {
|
||||
index.getShape(resource.getIdentifiers().get(childId)).ifPresent(shape -> {
|
||||
// Only infer identifiers introduced by a child. Children should
|
||||
// use their parent identifiers and not duplicate them.
|
||||
var builder = ConditionKeyDefinition.builder();
|
||||
if (shape.hasTrait(ArnReferenceTrait.class)) {
|
||||
// Use an ARN type if the targeted shape has the arnReference trait.
|
||||
builder.type(ARN_TYPE);
|
||||
} else {
|
||||
// Fall back to a string type otherwise.
|
||||
builder.type(STRING_TYPE);
|
||||
}
|
||||
|
||||
// Compute a simple doc string: "[NAME] resource [NAME] identifier"
|
||||
builder.documentation(computeIdentifierDocs(resource.getId(), childId));
|
||||
// The identifier name is comprised of "[arn service]:[Resource name][uppercase identifier name]
|
||||
String computeIdentifierName = computeIdentifierName(arnRoot, resource, childId);
|
||||
// Add the computed identifier binding and resolved context key to the result map.
|
||||
result.put(childId, computeIdentifierName);
|
||||
// Register the newly inferred context key definition with the service.
|
||||
serviceConditionKeys.get(service).put(computeIdentifierName, builder.build());
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static String computeIdentifierDocs(ShapeId id, String identifierName) {
|
||||
return id.getName() + " resource " + identifierName + " identifier";
|
||||
}
|
||||
|
||||
private static String computeIdentifierName(String arnRoot, ResourceShape resource, String identifierName) {
|
||||
return arnRoot + ":" + resource.getId().getName() + ucfirst(identifierName);
|
||||
}
|
||||
|
||||
private static String ucfirst(String value) {
|
||||
return value.isEmpty() ? value : value.substring(0, 1).toUpperCase(Locale.US) + value.substring(1);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* Copyright 2019 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.smithy.aws.traits.iam;
|
||||
|
||||
import java.util.List;
|
||||
import software.amazon.smithy.model.FromSourceLocation;
|
||||
import software.amazon.smithy.model.SourceLocation;
|
||||
import software.amazon.smithy.model.ToSmithyBuilder;
|
||||
import software.amazon.smithy.model.traits.StringListTrait;
|
||||
import software.amazon.smithy.model.traits.TraitService;
|
||||
|
||||
/**
|
||||
* Applies condition keys to an operation or resource.
|
||||
*/
|
||||
public final class ConditionKeysTrait extends StringListTrait implements ToSmithyBuilder<ConditionKeysTrait> {
|
||||
private static final String TRAIT = "aws.iam#conditionKeys";
|
||||
|
||||
public ConditionKeysTrait(List<String> keys, FromSourceLocation sourceLocation) {
|
||||
super(TRAIT, keys, sourceLocation);
|
||||
}
|
||||
|
||||
public ConditionKeysTrait(List<String> keys) {
|
||||
this(keys, SourceLocation.NONE);
|
||||
}
|
||||
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
public static TraitService provider() {
|
||||
return TraitService.createStringListProvider(TRAIT, ConditionKeysTrait::new);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder toBuilder() {
|
||||
ConditionKeysTrait.Builder builder = builder().sourceLocation(getSourceLocation());
|
||||
getValues().forEach(builder::addValue);
|
||||
return builder;
|
||||
}
|
||||
|
||||
public static final class Builder extends StringListTrait.Builder<ConditionKeysTrait, Builder> {
|
||||
private Builder() {}
|
||||
|
||||
@Override
|
||||
public ConditionKeysTrait build() {
|
||||
return new ConditionKeysTrait(getValues(), getSourceLocation());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* Copyright 2019 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.smithy.aws.traits.iam;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import software.amazon.smithy.aws.traits.ServiceTrait;
|
||||
import software.amazon.smithy.model.Model;
|
||||
import software.amazon.smithy.model.knowledge.TopDownIndex;
|
||||
import software.amazon.smithy.model.shapes.ServiceShape;
|
||||
import software.amazon.smithy.model.validation.AbstractValidator;
|
||||
import software.amazon.smithy.model.validation.ValidationEvent;
|
||||
import software.amazon.smithy.model.validation.ValidationUtils;
|
||||
|
||||
/**
|
||||
* Ensures that condition keys referenced by operations bound within the
|
||||
* closure of a service are defined either explicitly using the
|
||||
* {@code defineConditionKeys} trait or through an inferred resource
|
||||
* identifier condition key.
|
||||
*
|
||||
* <p>Condition keys that refer to global "aws:*" keys are allowed to not
|
||||
* be defined on the service.
|
||||
*/
|
||||
public class ConditionKeysValidator extends AbstractValidator {
|
||||
@Override
|
||||
public List<ValidationEvent> validate(Model model) {
|
||||
var conditionIndex = model.getKnowledge(ConditionKeysIndex.class);
|
||||
var topDownIndex = model.getKnowledge(TopDownIndex.class);
|
||||
|
||||
return model.getShapeIndex().shapes(ServiceShape.class)
|
||||
.filter(service -> service.hasTrait(ServiceTrait.class))
|
||||
.flatMap(service -> {
|
||||
List<ValidationEvent> results = new ArrayList<>();
|
||||
var knownKeys = conditionIndex.getDefinedConditionKeys(service).keySet();
|
||||
|
||||
for (var operation : topDownIndex.getContainedOperations(service)) {
|
||||
for (var name : conditionIndex.getConditionKeyNames(service, operation)) {
|
||||
if (!knownKeys.contains(name) && !name.startsWith("aws:")) {
|
||||
results.add(error(operation, String.format(
|
||||
"This operation scoped within the `%s` service refers to an undefined "
|
||||
+ "condition key `%s`. Expected one of the following defined condition "
|
||||
+ "keys: [%s]",
|
||||
service.getId(), name, ValidationUtils.tickedList(knownKeys))));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results.stream();
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,111 @@
|
|||
/*
|
||||
* Copyright 2019 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.smithy.aws.traits.iam;
|
||||
|
||||
import java.util.AbstractMap;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import software.amazon.smithy.model.ToSmithyBuilder;
|
||||
import software.amazon.smithy.model.node.Node;
|
||||
import software.amazon.smithy.model.node.ObjectNode;
|
||||
import software.amazon.smithy.model.traits.AbstractTrait;
|
||||
import software.amazon.smithy.model.traits.AbstractTraitBuilder;
|
||||
import software.amazon.smithy.model.traits.TraitService;
|
||||
|
||||
/**
|
||||
* Defines condition keys used in a service.
|
||||
*/
|
||||
public final class DefineConditionKeysTrait extends AbstractTrait implements ToSmithyBuilder<DefineConditionKeysTrait> {
|
||||
private static final String TRAIT = "aws.iam#defineConditionKeys";
|
||||
|
||||
private final Map<String, ConditionKeyDefinition> conditionKeys;
|
||||
|
||||
private DefineConditionKeysTrait(Builder builder) {
|
||||
super(TRAIT, builder.getSourceLocation());
|
||||
conditionKeys = Map.copyOf(builder.conditionKeys);
|
||||
}
|
||||
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
public static TraitService provider() {
|
||||
return TraitService.createProvider(TRAIT, (target, value) -> {
|
||||
Builder builder = builder();
|
||||
for (var entry : value.expectObjectNode().getMembers().entrySet()) {
|
||||
var definition = ConditionKeyDefinition.fromNode(entry.getValue().expectObjectNode());
|
||||
builder.putConditionKey(entry.getKey().getValue(), definition);
|
||||
}
|
||||
return builder.build();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all condition keys of the service.
|
||||
*
|
||||
* @return Returns the immutable map of condition key name to definition.
|
||||
*/
|
||||
public Map<String, ConditionKeyDefinition> getConditionKeys() {
|
||||
return conditionKeys;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific condition key by name.
|
||||
*
|
||||
* @param name Name of the condition key to get.
|
||||
* @return Returns the optionall found condition key.
|
||||
*/
|
||||
public Optional<ConditionKeyDefinition> getConditionKey(String name) {
|
||||
return Optional.ofNullable(conditionKeys.get(name));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Node createNode() {
|
||||
return conditionKeys.entrySet().stream()
|
||||
.map(entry -> new AbstractMap.SimpleImmutableEntry<>(
|
||||
Node.from(entry.getKey()), entry.getValue().toNode()))
|
||||
.collect(ObjectNode.collect(Map.Entry::getKey, Map.Entry::getValue));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder toBuilder() {
|
||||
Builder builder = builder().sourceLocation(getSourceLocation());
|
||||
conditionKeys.forEach(builder::putConditionKey);
|
||||
return builder;
|
||||
}
|
||||
|
||||
public static final class Builder extends AbstractTraitBuilder<DefineConditionKeysTrait, Builder> {
|
||||
private final Map<String, ConditionKeyDefinition> conditionKeys = new HashMap<>();
|
||||
|
||||
private Builder() {}
|
||||
|
||||
@Override
|
||||
public DefineConditionKeysTrait build() {
|
||||
return new DefineConditionKeysTrait(this);
|
||||
}
|
||||
|
||||
public Builder putConditionKey(String name, ConditionKeyDefinition definition) {
|
||||
conditionKeys.put(name, definition);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder removeConditionKey(String name) {
|
||||
conditionKeys.remove(name);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* Copyright 2019 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.smithy.aws.traits.iam;
|
||||
|
||||
import software.amazon.smithy.model.SourceLocation;
|
||||
import software.amazon.smithy.model.traits.BooleanTrait;
|
||||
import software.amazon.smithy.model.traits.TraitService;
|
||||
|
||||
/**
|
||||
* Infers the condition keys of a resource.
|
||||
*/
|
||||
public final class InferConditionKeysTrait extends BooleanTrait {
|
||||
private static final String TRAIT = "aws.iam#inferConditionKeys";
|
||||
|
||||
public InferConditionKeysTrait(SourceLocation sourceLocation) {
|
||||
super(TRAIT, sourceLocation);
|
||||
}
|
||||
|
||||
public InferConditionKeysTrait() {
|
||||
this(SourceLocation.NONE);
|
||||
}
|
||||
|
||||
public static TraitService provider() {
|
||||
return TraitService.createAnnotationProvider(TRAIT, InferConditionKeysTrait::new);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* Copyright 2019 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.smithy.aws.traits.iam;
|
||||
|
||||
import java.util.List;
|
||||
import software.amazon.smithy.model.FromSourceLocation;
|
||||
import software.amazon.smithy.model.SourceLocation;
|
||||
import software.amazon.smithy.model.ToSmithyBuilder;
|
||||
import software.amazon.smithy.model.traits.StringListTrait;
|
||||
import software.amazon.smithy.model.traits.TraitService;
|
||||
|
||||
public final class RequiredActionsTrait extends StringListTrait implements ToSmithyBuilder<RequiredActionsTrait> {
|
||||
private static final String TRAIT = "aws.iam#requiredActions";
|
||||
|
||||
public RequiredActionsTrait(List<String> actions, FromSourceLocation sourceLocation) {
|
||||
super(TRAIT, actions, sourceLocation);
|
||||
}
|
||||
|
||||
public RequiredActionsTrait(List<String> actions) {
|
||||
this(actions, SourceLocation.NONE);
|
||||
}
|
||||
|
||||
public static TraitService provider() {
|
||||
return TraitService.createStringListProvider(TRAIT, RequiredActionsTrait::new);
|
||||
}
|
||||
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder toBuilder() {
|
||||
Builder builder = builder().sourceLocation(getSourceLocation());
|
||||
getValues().forEach(builder::addValue);
|
||||
return builder;
|
||||
}
|
||||
|
||||
public static final class Builder extends StringListTrait.Builder<RequiredActionsTrait, Builder> {
|
||||
@Override
|
||||
public RequiredActionsTrait build() {
|
||||
return new RequiredActionsTrait(getValues(), getSourceLocation());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,128 @@
|
|||
{
|
||||
"smithy": "1.0",
|
||||
"aws.api": {
|
||||
"traitDefs": {
|
||||
"arn": {
|
||||
"selector": "resource",
|
||||
"shape": "aws.api#ArnTrait",
|
||||
"externalDocumentation": "https://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html",
|
||||
"documentation": "Specifies an ARN template for the resource."
|
||||
},
|
||||
"service": {
|
||||
"selector": "service",
|
||||
"shape": "aws.api#AwsServiceTrait"
|
||||
},
|
||||
"arnReference": {
|
||||
"selector": "string",
|
||||
"shape": "aws.api#ArnReferenceTrait",
|
||||
"documentation": "Marks a string as containing an ARN."
|
||||
},
|
||||
"unsignedPayload": {
|
||||
"selector": "operation",
|
||||
"shape": "aws.api#StringList",
|
||||
"documentation": "Indicates that the request payload of a signed request is not to be used as part of the signature. Providing a list of strings will limit the effect of this trait to only specific authentication schemes by name."
|
||||
},
|
||||
"data": {
|
||||
"selector": ":test(simpleType, collection, structure, union, member)",
|
||||
"shape": "aws.api#DataLevel",
|
||||
"documentation": "Designates the target as containing data of a known classification level."
|
||||
}
|
||||
},
|
||||
"shapes": {
|
||||
"ArnTrait": {
|
||||
"type": "structure",
|
||||
"members": {
|
||||
"template": {
|
||||
"target": "smithy.api#String",
|
||||
"required": true
|
||||
},
|
||||
"absolute": {
|
||||
"target": "smithy.api#Boolean"
|
||||
},
|
||||
"noRegion": {
|
||||
"target": "smithy.api#Boolean"
|
||||
},
|
||||
"noAccount": {
|
||||
"target": "smithy.api#Boolean"
|
||||
}
|
||||
},
|
||||
"private": true
|
||||
},
|
||||
"AwsServiceTrait": {
|
||||
"type": "structure",
|
||||
"members": {
|
||||
"sdkId": {
|
||||
"target": "smithy.api#String",
|
||||
"required": true
|
||||
},
|
||||
"arnNamespace": {
|
||||
"target": "aws.api#ArnNamespace"
|
||||
},
|
||||
"cloudFormationName": {
|
||||
"target": "aws.api#CloudFormationName"
|
||||
},
|
||||
"cloudTrailEventSource": {
|
||||
"target": "smithy.api#String"
|
||||
}
|
||||
},
|
||||
"private": true
|
||||
},
|
||||
"ArnNamespace": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-z0-9.\\-]{1,63}$",
|
||||
"private": true
|
||||
},
|
||||
"CloudFormationName": {
|
||||
"type": "string",
|
||||
"pattern": "^[A-Z][A-Za-z0-9]+$",
|
||||
"private": true
|
||||
},
|
||||
"ArnReferenceTrait": {
|
||||
"type": "structure",
|
||||
"members": {
|
||||
"type": {
|
||||
"target": "smithy.api#String"
|
||||
},
|
||||
"resource": {
|
||||
"target": "smithy.api#String"
|
||||
},
|
||||
"service": {
|
||||
"target": "smithy.api#String"
|
||||
}
|
||||
},
|
||||
"private": true
|
||||
},
|
||||
"DataLevel": {
|
||||
"type": "string",
|
||||
"private": true,
|
||||
"enum": {
|
||||
"content": {
|
||||
"name": "CUSTOMER_CONTENT",
|
||||
"documentation": "Customer content means any software (including machine images), data, text, audio, video or images that customers or any customer end user transfers to AWS for processing, storage or hosting by AWS services in connection with the customer’s accounts and any computational results that customers or any customer end user derive from the foregoing through their use of AWS services."
|
||||
},
|
||||
"account": {
|
||||
"name": "CUSTOMER_ACCOUNT_INFORMATION"
|
||||
},
|
||||
"usage": {
|
||||
"name": "ACCOUNT_USAGE_DATA",
|
||||
"documentation": "Account usage data means usage data related to a customer’s account, such as resource identifiers, usage statistics, logging data, and analytics."
|
||||
},
|
||||
"tagging": {
|
||||
"name": "TAG_DATA",
|
||||
"documentation": "Designates metadata tags applied to AWS resources."
|
||||
},
|
||||
"permissions": {
|
||||
"name": "PERMISSIONS_DATA",
|
||||
"documentation": "Designates security and access roles, rules, usage policies, and permissions."
|
||||
}
|
||||
}
|
||||
},
|
||||
"StringList": {
|
||||
"type": "list",
|
||||
"member": {
|
||||
"target": "smithy.api#String"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
{
|
||||
"smithy": "1.0",
|
||||
"aws.apigateway": {
|
||||
"traitDefs": {
|
||||
"apiKeySource": {
|
||||
"selector": "service",
|
||||
"shape": "smithy.api#String",
|
||||
"documentation": "Specifies the source of the caller identifier that will be used to throttle API methods that require a key.",
|
||||
"externalDocumentation": "https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-swagger-extensions-api-key-source.html",
|
||||
"tags": ["internal"]
|
||||
},
|
||||
"authorizers": {
|
||||
"selector": "service",
|
||||
"shape": "aws.apigateway#Authorizers",
|
||||
"documentation": "Lambda authorizers to attach to the authentication schemes defined on this service.",
|
||||
"tags": ["internal"]
|
||||
},
|
||||
"requestValidator": {
|
||||
"selector": ":test(service, operation)",
|
||||
"shape": "smithy.api#String",
|
||||
"documentation": "Selects which request validation strategy to use. One of: 'full', 'params-only', 'body-only'",
|
||||
"tags": ["internal"]
|
||||
}
|
||||
},
|
||||
"shapes": {
|
||||
"Authorizers": {
|
||||
"type": "map",
|
||||
"private": true,
|
||||
"documentation": "A list of API Gateway authorizers to augment the service's declared authentication mechanisms.",
|
||||
"key": {"target": "smithy.api#String"},
|
||||
"value": {"target": "aws.apigateway#Authorizer"}
|
||||
},
|
||||
"Authorizer": {
|
||||
"type": "structure",
|
||||
"private": true,
|
||||
"documentation": "An object that associates an authorizer and associated metadata with an authentication mechanism.",
|
||||
"members": {
|
||||
"clientType": {
|
||||
"target": "smithy.api#String",
|
||||
"documentation": "The client authentication type. A value identifying the category of authorizer, such as \"oauth2\" or \"awsSigv4\"."
|
||||
},
|
||||
"type": {
|
||||
"target": "smithy.api#String",
|
||||
"required": true,
|
||||
"documentation": "The type of the authorizer. This is a required property and the value must be \"token\", for an authorizer with the caller identity embedded in an authorization token, or \"request\", for an authorizer with the caller identity contained in request parameters."
|
||||
},
|
||||
"uri": {
|
||||
"target": "smithy.api#String",
|
||||
"required": true,
|
||||
"documentation": "The Uniform Resource Identifier (URI) of the authorizer Lambda function"
|
||||
},
|
||||
"credentials": {
|
||||
"target": "smithy.api#String",
|
||||
"documentation": "Credentials required for invoking the authorizer"
|
||||
},
|
||||
"identitySource": {
|
||||
"target": "smithy.api#String",
|
||||
"documentation": "Comma-separated list of mapping expressions of the request parameters as the identity source. Applicable for the authorizer of the \"request\" type only."
|
||||
},
|
||||
"identityValidationExpression": {
|
||||
"target": "smithy.api#String",
|
||||
"documentation": "A regular expression for validating the token as the incoming identity"
|
||||
},
|
||||
"resultTtlInSeconds": {
|
||||
"target": "smithy.api#Integer",
|
||||
"documentation": "The number of seconds for which the resulting IAM policy is cached."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
{
|
||||
"smithy": "1.0",
|
||||
"aws.iam": {
|
||||
"traitDefs": {
|
||||
"actionPermissionDescription": {
|
||||
"selector": "operation",
|
||||
"shape": "smithy.api#String",
|
||||
"documentation": "A brief description of what granting the user permission to invoke an operation would entail. This description should begin with something similar to 'Enables the user to...' or 'Grants permission to...'"
|
||||
},
|
||||
"defineConditionKeys": {
|
||||
"selector": "service",
|
||||
"shape": "aws.iam#DefineConditionKeysTrait",
|
||||
"documentation": "Defines the set of condition keys that appear within a service in addition to inferred and global condition keys."
|
||||
},
|
||||
"conditionKeys": {
|
||||
"selector": ":test(resource, operation)",
|
||||
"shape": "aws.iam#IdentifierList",
|
||||
"documentation": "Applies condition keys by name to a resource or operation"
|
||||
},
|
||||
"inferConditionKeys": {
|
||||
"selector": "resource",
|
||||
"documentation": "Infers the condition keys of a resource"
|
||||
},
|
||||
"requiredActions": {
|
||||
"selector": "operation",
|
||||
"shape": "aws.iam#IdentifierList",
|
||||
"documentation": "Other actions that the invoker must be authorized to perform when executing the targeted operation."
|
||||
}
|
||||
},
|
||||
"shapes": {
|
||||
"IdentifierList": {
|
||||
"type": "list",
|
||||
"private": true,
|
||||
"member": { "target": "aws.iam#IamIdentifier" }
|
||||
},
|
||||
"IamIdentifier": {
|
||||
"type": "string",
|
||||
"private": true,
|
||||
"pattern": "^([A-Za-z0-9][A-Za-z0-9-\\.]{0,62}:[^:]+)$"
|
||||
},
|
||||
"DefineConditionKeysTrait": {
|
||||
"type": "map",
|
||||
"private": true,
|
||||
"key": {"target": "aws.iam#IamIdentifier"},
|
||||
"value": {"target": "aws.iam#ConditionKeyDefinition"}
|
||||
},
|
||||
"ConditionKeyDefinition": {
|
||||
"type": "structure",
|
||||
"private": true,
|
||||
"members": {
|
||||
"documentation": {
|
||||
"target": "smithy.api#String"
|
||||
},
|
||||
"externalDocumentation": {
|
||||
"target": "smithy.api#String"
|
||||
},
|
||||
"type": {
|
||||
"target": "aws.iam#ConditionKeyType",
|
||||
"required": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"ConditionKeyType": {
|
||||
"type": "string",
|
||||
"private": true,
|
||||
"documentation": "The IAM policy type of the value that will supplied for this context key",
|
||||
"smithy.api#enum": {
|
||||
"ARN": {},
|
||||
"ArrayOfARN": {},
|
||||
"Binary": {},
|
||||
"ArrayOfBinary": {},
|
||||
"String": {},
|
||||
"ArrayOfString": {},
|
||||
"Numeric": {},
|
||||
"ArrayOfNumeric": {},
|
||||
"Date": {},
|
||||
"ArrayOfDate": {},
|
||||
"Bool": {},
|
||||
"ArrayOfBool": {},
|
||||
"IPAddress": {},
|
||||
"ArrayOfIPAddress": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
//
|
||||
// Copyright 2019 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.
|
||||
//
|
||||
|
||||
--add-reads
|
||||
software.amazon.smithy.aws.traits=org.hamcrest
|
|
@ -0,0 +1,102 @@
|
|||
/*
|
||||
* Copyright 2019 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.smithy.aws.traits;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.hasKey;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import software.amazon.smithy.model.Model;
|
||||
import software.amazon.smithy.model.shapes.Shape;
|
||||
import software.amazon.smithy.model.shapes.ShapeId;
|
||||
|
||||
public class ArnIndexTest {
|
||||
private static Model model;
|
||||
|
||||
@BeforeAll
|
||||
public static void beforeClass() {
|
||||
model = Model.assembler()
|
||||
.discoverModels(ArnIndexTest.class.getClassLoader())
|
||||
.addImport(ArnIndexTest.class.getResource("test-model.json"))
|
||||
.assemble()
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loadsFromModel() {
|
||||
ArnIndex arnIndex = new ArnIndex(model);
|
||||
ShapeId id = ShapeId.from("ns.foo#SomeService");
|
||||
Shape someResource = model.getShapeIndex().getShape(ShapeId.from("ns.foo#SomeResource")).get();
|
||||
ArnTrait template1 = ArnTrait.builder()
|
||||
.template("someresource/{someId}")
|
||||
.build();
|
||||
Shape childResource = model.getShapeIndex().getShape(ShapeId.from("ns.foo#ChildResource")).get();
|
||||
ArnTrait template2 = ArnTrait.builder()
|
||||
.template("someresource/{someId}/{childId}")
|
||||
.build();
|
||||
Shape rootArnResource = model.getShapeIndex().getShape(ShapeId.from("ns.foo#RootArnResource")).get();
|
||||
ArnTrait template3 = ArnTrait.builder()
|
||||
.template("rootArnResource")
|
||||
.noAccount(true)
|
||||
.noRegion(true)
|
||||
.build();
|
||||
|
||||
assertThat(arnIndex.getServiceArnNamespace(id), equalTo("service"));
|
||||
Map<ShapeId, ArnTrait> templates = arnIndex.getServiceResourceArns(id);
|
||||
assertThat(templates, hasKey(someResource.getId()));
|
||||
assertThat(templates, hasKey(childResource.getId()));
|
||||
assertThat(templates, hasKey(rootArnResource.getId()));
|
||||
assertThat(templates.get(someResource.getId()), equalTo(template1));
|
||||
assertThat(templates.get(childResource.getId()), equalTo(template2));
|
||||
assertThat(templates.get(rootArnResource.getId()), equalTo(template3));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void computesFullArnTemplate() {
|
||||
ArnIndex arnIndex = new ArnIndex(model);
|
||||
ShapeId service = ShapeId.from("ns.foo#SomeService");
|
||||
|
||||
assertThat(arnIndex.getFullResourceArnTemplate(service, ShapeId.from("ns.foo#SomeResource")),
|
||||
equalTo(Optional.of("arn:{AWS::Partition}:service:{AWS::Region}:{AWS::AccountId}:someresource/{someId}")));
|
||||
assertThat(arnIndex.getFullResourceArnTemplate(service, ShapeId.from("ns.foo#ChildResource")),
|
||||
equalTo(Optional.of("arn:{AWS::Partition}:service:{AWS::Region}:{AWS::AccountId}:someresource/{someId}/{childId}")));
|
||||
assertThat(arnIndex.getFullResourceArnTemplate(service, ShapeId.from("ns.foo#RootArnResource")),
|
||||
equalTo(Optional.of("arn:{AWS::Partition}:service:::rootArnResource")));
|
||||
assertThat(arnIndex.getFullResourceArnTemplate(service, ShapeId.from("ns.foo#Invalid")),
|
||||
equalTo(Optional.empty()));
|
||||
assertThat(arnIndex.getFullResourceArnTemplate(service, ShapeId.from("ns.foo#AbsoluteResource")),
|
||||
equalTo(Optional.of("{arn}")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void returnsDefaultServiceArnNamespace() {
|
||||
ArnIndex arnIndex = new ArnIndex(model);
|
||||
|
||||
assertThat(arnIndex.getServiceArnNamespace(ShapeId.from("ns.foo#NonAwsService")), equalTo("nonawsservice"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void returnsDefaultServiceArnNamespaceForAwsService() {
|
||||
ArnIndex arnIndex = new ArnIndex(model);
|
||||
|
||||
assertThat(arnIndex.getServiceArnNamespace(ShapeId.from("ns.foo#EmptyAwsService")),
|
||||
equalTo("emptyawsservice"));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
* Copyright 2019 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.smithy.aws.traits;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.instanceOf;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.util.Optional;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import software.amazon.smithy.model.Model;
|
||||
import software.amazon.smithy.model.node.Node;
|
||||
import software.amazon.smithy.model.shapes.Shape;
|
||||
import software.amazon.smithy.model.shapes.ShapeId;
|
||||
import software.amazon.smithy.model.traits.Trait;
|
||||
import software.amazon.smithy.model.traits.TraitFactory;
|
||||
|
||||
public class ArnReferenceTraitTest {
|
||||
|
||||
@Test
|
||||
public void loadsEmptyTrait() {
|
||||
Node node = Node.parse("{}");
|
||||
TraitFactory provider = TraitFactory.createServiceFactory();
|
||||
Optional<Trait> trait = provider.createTrait("aws.api#arnReference", ShapeId.from("ns.foo#foo"), node);
|
||||
|
||||
assertTrue(trait.isPresent());
|
||||
assertThat(trait.get(), instanceOf(ArnReferenceTrait.class));
|
||||
ArnReferenceTrait arn = (ArnReferenceTrait) trait.get();
|
||||
assertThat(arn.toBuilder().build(), equalTo(arn));
|
||||
assertThat(arn.getService(), equalTo(Optional.empty()));
|
||||
assertThat(arn.getResource(), equalTo(Optional.empty()));
|
||||
assertThat(arn.getType(), equalTo(Optional.empty()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loadsTraitWithOptionalValues() {
|
||||
Node node = Node.parse("{\"resource\": \"com.foo#Baz\", \"service\": \"com.foo#Bar\", \"type\": \"AWS::Foo::Baz\"}");
|
||||
TraitFactory provider = TraitFactory.createServiceFactory();
|
||||
Optional<Trait> trait = provider.createTrait("aws.api#arnReference", ShapeId.from("ns.foo#foo"), node);
|
||||
|
||||
ArnReferenceTrait arn = (ArnReferenceTrait) trait.get();
|
||||
assertThat(arn.toBuilder().build(), equalTo(arn));
|
||||
assertThat(arn.getService(), equalTo(Optional.of(ShapeId.from("com.foo#Bar"))));
|
||||
assertThat(arn.getResource(), equalTo(Optional.of(ShapeId.from("com.foo#Baz"))));
|
||||
assertThat(arn.getType(), equalTo(Optional.of("AWS::Foo::Baz")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loadsTraitWithOptionalValuesAndRelativeShapeIds() {
|
||||
Node node = Node.parse("{\"resource\": \"Baz\", \"service\": \"Bar\", \"type\": \"AWS::Foo::Baz\"}");
|
||||
TraitFactory provider = TraitFactory.createServiceFactory();
|
||||
Optional<Trait> trait = provider.createTrait("aws.api#arnReference", ShapeId.from("ns.foo#String"), node);
|
||||
|
||||
ArnReferenceTrait arn = (ArnReferenceTrait) trait.get();
|
||||
assertThat(arn.toBuilder().build(), equalTo(arn));
|
||||
assertThat(arn.getService(), equalTo(Optional.of(ShapeId.from("ns.foo#Bar"))));
|
||||
assertThat(arn.getResource(), equalTo(Optional.of(ShapeId.from("ns.foo#Baz"))));
|
||||
assertThat(arn.getType(), equalTo(Optional.of("AWS::Foo::Baz")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loadsFromModel() {
|
||||
Model result = Model.assembler()
|
||||
.discoverModels(getClass().getClassLoader())
|
||||
.addImport(getClass().getResource("test-model.json"))
|
||||
.assemble()
|
||||
.unwrap();
|
||||
Shape service = result.getShapeIndex()
|
||||
.getShape(ShapeId.from("ns.foo#AbsoluteResourceArn")).get();
|
||||
ArnReferenceTrait trait = service.getTrait(ArnReferenceTrait.class).get();
|
||||
|
||||
assertThat(trait.getType(), equalTo(Optional.of("AWS::SomeService::AbsoluteResource")));
|
||||
assertThat(trait.getResource(), equalTo(Optional.of(ShapeId.from("ns.foo#AbsoluteResource"))));
|
||||
assertThat(trait.getService(), equalTo(Optional.of(ShapeId.from("ns.foo#SomeService"))));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
* Copyright 2019 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.smithy.aws.traits;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.hasSize;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import software.amazon.smithy.model.Model;
|
||||
import software.amazon.smithy.model.ValidatedResult;
|
||||
import software.amazon.smithy.model.loader.ModelAssembler;
|
||||
import software.amazon.smithy.model.validation.Severity;
|
||||
import software.amazon.smithy.model.validation.ValidationEvent;
|
||||
|
||||
public class ArnTemplateValidatorTest {
|
||||
private static ModelAssembler baseModel;
|
||||
|
||||
@BeforeAll
|
||||
public static void beforeClass() {
|
||||
baseModel = Model.assembler().discoverModels(ArnTemplateValidatorTest.class.getClassLoader());
|
||||
}
|
||||
|
||||
private ValidatedResult<Model> getModel(String name) {
|
||||
return baseModel.copy().addImport(getClass().getResource(name)).assemble();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void findsMissingIdentifiers() {
|
||||
ValidatedResult<Model> result = getModel("not-enough-identifiers.json");
|
||||
|
||||
assertThat(result.getValidationEvents(Severity.ERROR), hasSize(2));
|
||||
List<ValidationEvent> events = result.getValidationEvents(Severity.ERROR);
|
||||
events.sort(Comparator.comparing(event -> event.getShapeId().get().toString()));
|
||||
|
||||
String message1 = events.get(0).getMessage();
|
||||
assertThat(message1, containsString("a"));
|
||||
assertThat(message1, containsString("`aid`"));
|
||||
|
||||
String message2 = events.get(1).getMessage();
|
||||
assertThat(message2, containsString("a/b"));
|
||||
assertThat(message2, containsString("`aid`, `bid`"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void findsExtraneousIdentifiers() {
|
||||
ValidatedResult<Model> result = getModel("too-many-identifiers.json");
|
||||
|
||||
assertThat(result.getValidationEvents(Severity.ERROR), hasSize(2));
|
||||
List<ValidationEvent> events = result.getValidationEvents(Severity.ERROR);
|
||||
events.sort(Comparator.comparing(event -> event.getShapeId().get().toString()));
|
||||
|
||||
assertThat(events.get(0).getMessage(), equalTo(
|
||||
"Invalid aws.api#arn trait resource, `a/{aid}/{InvalidId}/{InvalidId2}`. Found template "
|
||||
+ "labels in the trait that are not the names of the identifiers of the resource: [aid]. "
|
||||
+ "Extraneous identifiers: [`InvalidId`, `InvalidId2`]"));
|
||||
assertThat(events.get(1).getMessage(), containsString(
|
||||
"Invalid aws.api#arn trait resource, `a/{aid}/{InvalidId}/{InvalidId2}/b/{bid}/{AnotherInvalid}`. "
|
||||
+ "Found template labels in the trait that are not the names of the identifiers of the resource"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void validatesTemplatePlaceHolders() {
|
||||
ValidatedResult<Model> result = getModel("invalid-arn-template-string.json");
|
||||
|
||||
assertThat(result.getValidationEvents(Severity.ERROR), hasSize(1));
|
||||
List<ValidationEvent> events = result.getValidationEvents(Severity.ERROR);
|
||||
assertThat(events.get(0).getMessage(),
|
||||
containsString("aws.api#arn trait contains invalid template labels"));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,102 @@
|
|||
/*
|
||||
* Copyright 2019 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.smithy.aws.traits;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.contains;
|
||||
import static org.hamcrest.Matchers.empty;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.instanceOf;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.util.Optional;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import software.amazon.smithy.model.SourceException;
|
||||
import software.amazon.smithy.model.node.Node;
|
||||
import software.amazon.smithy.model.shapes.ShapeId;
|
||||
import software.amazon.smithy.model.traits.Trait;
|
||||
import software.amazon.smithy.model.traits.TraitFactory;
|
||||
|
||||
public class ArnTraitTest {
|
||||
|
||||
@Test
|
||||
public void loadsTraitWithFromNode() {
|
||||
Node node = Node.parse("{\"template\": \"resourceName\"}");
|
||||
TraitFactory provider = TraitFactory.createServiceFactory();
|
||||
Optional<Trait> trait = provider.createTrait("aws.api#arn", ShapeId.from("ns.foo#foo"), node);
|
||||
|
||||
assertTrue(trait.isPresent());
|
||||
assertThat(trait.get(), instanceOf(ArnTrait.class));
|
||||
ArnTrait arnTrait = (ArnTrait) trait.get();
|
||||
assertThat(arnTrait.getTemplate(), equalTo("resourceName"));
|
||||
assertThat(arnTrait.isNoAccount(), is(false));
|
||||
assertThat(arnTrait.isNoRegion(), is(false));
|
||||
assertThat(arnTrait.getLabels(), empty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void canSetRegionAndServiceToNo() {
|
||||
Node node = Node.parse("{\"noAccount\": true, \"noRegion\": true, \"absolute\": false, \"template\": \"foo\"}");
|
||||
TraitFactory provider = TraitFactory.createServiceFactory();
|
||||
Optional<Trait> trait = provider.createTrait("aws.api#arn", ShapeId.from("ns.foo#foo"), node);
|
||||
|
||||
assertTrue(trait.isPresent());
|
||||
ArnTrait arnTrait = (ArnTrait) trait.get();
|
||||
assertThat(arnTrait.getTemplate(), equalTo("foo"));
|
||||
assertThat(arnTrait.isNoAccount(), is(true));
|
||||
assertThat(arnTrait.isNoRegion(), is(true));
|
||||
assertThat(arnTrait.toNode(), equalTo(node));
|
||||
assertThat(arnTrait.toBuilder().build(), equalTo(arnTrait));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void canSetIncludeTemplateExpressions() {
|
||||
Node node = Node.parse("{\"noAccount\": false, \"noRegion\": false, \"template\": \"foo/{Baz}/bar/{Bam}/boo/{Boo}\"}");
|
||||
TraitFactory provider = TraitFactory.createServiceFactory();
|
||||
Optional<Trait> trait = provider.createTrait("aws.api#arn", ShapeId.from("ns.foo#foo"), node);
|
||||
|
||||
assertTrue(trait.isPresent());
|
||||
ArnTrait arnTrait = (ArnTrait) trait.get();
|
||||
assertThat(arnTrait.getTemplate(), equalTo("foo/{Baz}/bar/{Bam}/boo/{Boo}"));
|
||||
assertThat(arnTrait.getLabels(), contains("Baz", "Bam", "Boo"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resourcePartCannotStartWithSlash() {
|
||||
assertThrows(SourceException.class, () -> {
|
||||
Node node = Node.parse("{\"template\": \"/resource\"}");
|
||||
TraitFactory.createServiceFactory().createTrait("aws.api#arn", ShapeId.from("ns.foo#foo"), node);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void validatesAccountValue() {
|
||||
assertThrows(SourceException.class, () -> {
|
||||
Node node = Node.parse("{\"template\": \"foo\", \"noAccount\": \"invalid\"}");
|
||||
TraitFactory.createServiceFactory().createTrait("aws.api#arn", ShapeId.from("ns.foo#foo"), node);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void validatesRegionValue() {
|
||||
assertThrows(SourceException.class, () -> {
|
||||
Node node = Node.parse("{\"template\": \"foo\", \"noRegion\": \"invalid\"}");
|
||||
TraitFactory.createServiceFactory().createTrait("aws.api#arn", ShapeId.from("ns.foo#foo"), node);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* Copyright 2019 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.smithy.aws.traits;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import software.amazon.smithy.model.Model;
|
||||
import software.amazon.smithy.model.shapes.ShapeId;
|
||||
|
||||
public class DataTraitTest {
|
||||
|
||||
private Model getModel() {
|
||||
return Model.assembler()
|
||||
.discoverModels(getClass().getClassLoader())
|
||||
.addImport(getClass().getResource("data-classification-model.json"))
|
||||
.assemble()
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loadsWithString() {
|
||||
Model model = getModel();
|
||||
assertTrue(model.getShapeIndex().getShape(ShapeId.from("ns.foo#A"))
|
||||
.flatMap(shape -> shape.getTrait(DataTrait.class))
|
||||
.filter(trait -> trait.getValue().equals("account"))
|
||||
.isPresent());
|
||||
|
||||
assertTrue(model.getShapeIndex().getShape(ShapeId.from("ns.foo#B"))
|
||||
.flatMap(shape -> shape.getTrait(DataTrait.class))
|
||||
.filter(trait -> trait.getValue().equals("tagging"))
|
||||
.isPresent());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
/*
|
||||
* Copyright 2019 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.smithy.aws.traits;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.hamcrest.Matchers.empty;
|
||||
import static org.hamcrest.Matchers.not;
|
||||
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import software.amazon.smithy.model.Model;
|
||||
import software.amazon.smithy.model.ValidatedResult;
|
||||
import software.amazon.smithy.model.shapes.ServiceShape;
|
||||
import software.amazon.smithy.model.shapes.ShapeId;
|
||||
import software.amazon.smithy.model.validation.Severity;
|
||||
|
||||
public class SdkServiceIdValidatorTest {
|
||||
@Test
|
||||
public void validatesServiceShapesDuringBuild() {
|
||||
ShapeId id = ShapeId.from("com.foo#Baz");
|
||||
ServiceShape serviceShape = ServiceShape.builder()
|
||||
.id(id)
|
||||
.version("2016-04-01")
|
||||
.addTrait(ServiceTrait.builder().sdkId("AWS Foo").build(id))
|
||||
.build();
|
||||
ValidatedResult<Model> result = Model.assembler()
|
||||
.addShape(serviceShape)
|
||||
.discoverModels(getClass().getClassLoader())
|
||||
.assemble();
|
||||
|
||||
assertThat(result.getValidationEvents(Severity.ERROR), not(empty()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doesNotAllowCompanyNames() {
|
||||
var thrown = Assertions.assertThrows(IllegalArgumentException.class, () ->
|
||||
SdkServiceIdValidator.validateServiceId("AWS Foo"));
|
||||
|
||||
assertThat(thrown.getMessage(), containsString("company names"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doesNotAllowBadSuffix() {
|
||||
var thrown = Assertions.assertThrows(IllegalArgumentException.class, () ->
|
||||
SdkServiceIdValidator.validateServiceId("Foo Service"));
|
||||
|
||||
assertThat(thrown.getMessage(), containsString("case-insensitively end with"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mustMatchRegex() {
|
||||
Assertions.assertThrows(IllegalArgumentException.class, () ->
|
||||
SdkServiceIdValidator.validateServiceId("!Nope!"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void noTrailingWhitespace() {
|
||||
Assertions.assertThrows(IllegalArgumentException.class, () ->
|
||||
SdkServiceIdValidator.validateServiceId("Foo "));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doesNotAllowShortIds() {
|
||||
var thrown = Assertions.assertThrows(IllegalArgumentException.class, () ->
|
||||
SdkServiceIdValidator.validateServiceId("F"));
|
||||
|
||||
assertThat(thrown.getMessage(), containsString("2 and 50"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doesNotAllowLongIds() {
|
||||
var thrown = Assertions.assertThrows(IllegalArgumentException.class, () ->
|
||||
SdkServiceIdValidator.validateServiceId("Foobarbazqux Foobarbazqux Foobarbazqux Foobarbazqux"));
|
||||
|
||||
assertThat(thrown.getMessage(), containsString("2 and 50"));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,98 @@
|
|||
/*
|
||||
* Copyright 2019 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.smithy.aws.traits;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.instanceOf;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.util.Optional;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import software.amazon.smithy.model.Model;
|
||||
import software.amazon.smithy.model.node.Node;
|
||||
import software.amazon.smithy.model.shapes.ServiceShape;
|
||||
import software.amazon.smithy.model.shapes.ShapeId;
|
||||
import software.amazon.smithy.model.traits.Trait;
|
||||
import software.amazon.smithy.model.traits.TraitFactory;
|
||||
|
||||
public class ServiceTraitTest {
|
||||
|
||||
@Test
|
||||
public void loadsTraitWithString() {
|
||||
Node node = Node.parse("{\"sdkServiceId\": \"Foo\"}");
|
||||
TraitFactory provider = TraitFactory.createServiceFactory();
|
||||
Optional<Trait> trait = provider.createTrait("aws.api#service", ShapeId.from("ns.foo#Foo"), node);
|
||||
|
||||
assertTrue(trait.isPresent());
|
||||
assertThat(trait.get(), instanceOf(ServiceTrait.class));
|
||||
ServiceTrait serviceTrait = (ServiceTrait) trait.get();
|
||||
assertThat(serviceTrait.getSdkId(), equalTo("Foo"));
|
||||
assertThat(serviceTrait.getCloudFormationName(), equalTo("Foo"));
|
||||
assertThat(serviceTrait.getArnNamespace(), equalTo("foo"));
|
||||
assertThat(serviceTrait.getCloudTrailEventSource(), equalTo("foo.amazonaws.com"));
|
||||
assertThat(serviceTrait.toBuilder().build(), equalTo(serviceTrait));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loadsTraitWithOptionalValues() {
|
||||
Node node = Node.parse("{\"sdkServiceId\": \"Foo\", \"arnService\": \"service\", \"productName\": \"Baz\"}");
|
||||
TraitFactory provider = TraitFactory.createServiceFactory();
|
||||
Optional<Trait> trait = provider.createTrait("aws.api#service", ShapeId.from("ns.foo#foo"), node);
|
||||
|
||||
assertTrue(trait.isPresent());
|
||||
assertThat(trait.get(), instanceOf(ServiceTrait.class));
|
||||
ServiceTrait serviceTrait = (ServiceTrait) trait.get();
|
||||
assertThat(serviceTrait.getSdkId(), equalTo("Foo"));
|
||||
assertThat(serviceTrait.getArnNamespace(), equalTo("service"));
|
||||
assertThat(serviceTrait.getCloudFormationName(), equalTo("Baz"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loadsEventSource() {
|
||||
Node node = Node.parse("{\"sdkServiceId\": \"Foo\", \"cloudTrailEventSource\": \"foo.amazonaws.com\"}");
|
||||
TraitFactory provider = TraitFactory.createServiceFactory();
|
||||
Optional<Trait> trait = provider.createTrait("aws.api#service", ShapeId.from("ns.foo#foo"), node);
|
||||
|
||||
assertTrue(trait.isPresent());
|
||||
assertThat(trait.get(), instanceOf(ServiceTrait.class));
|
||||
ServiceTrait serviceTrait = (ServiceTrait) trait.get();
|
||||
assertThat(serviceTrait.getCloudTrailEventSource(), equalTo("foo.amazonaws.com"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requiresSdkServiceId() {
|
||||
assertThrows(IllegalStateException.class, () -> ServiceTrait.builder().build());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loadsFromModel() {
|
||||
Model result = Model.assembler()
|
||||
.discoverModels(getClass().getClassLoader())
|
||||
.addImport(getClass().getResource("test-model.json"))
|
||||
.assemble()
|
||||
.unwrap();
|
||||
ServiceShape service = result.getShapeIndex()
|
||||
.getShape(ShapeId.from("ns.foo#SomeService")).get()
|
||||
.asServiceShape().get();
|
||||
ServiceTrait trait = service.getTrait(ServiceTrait.class).get();
|
||||
|
||||
assertThat(trait.getSdkId(), equalTo("Some Value"));
|
||||
assertThat(trait.getCloudFormationName(), equalTo("SomeService"));
|
||||
assertThat(trait.getArnNamespace(), equalTo("service"));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* Copyright 2019 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.smithy.aws.traits;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import software.amazon.smithy.model.Model;
|
||||
import software.amazon.smithy.model.shapes.ShapeId;
|
||||
|
||||
public class UnsignedPayloadTest {
|
||||
@Test
|
||||
public void loadsFromModel() {
|
||||
Model result = Model.assembler()
|
||||
.discoverModels(getClass().getClassLoader())
|
||||
.addImport(getClass().getResource("unsigned-request-payload.json"))
|
||||
.assemble()
|
||||
.unwrap();
|
||||
|
||||
assertTrue(result.getShapeIndex()
|
||||
.getShape(ShapeId.from("ns.foo#Unsigned1"))
|
||||
.flatMap(shape -> shape.getTrait(UnsignedPayload.class))
|
||||
.isPresent());
|
||||
|
||||
assertTrue(result.getShapeIndex()
|
||||
.getShape(ShapeId.from("ns.foo#Unsigned2"))
|
||||
.flatMap(shape -> shape.getTrait(UnsignedPayload.class))
|
||||
.filter(trait -> trait.getValues().equals(List.of("aws.v4")))
|
||||
.isPresent());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
package software.amazon.smithy.aws.traits.apigateway;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.instanceOf;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import software.amazon.smithy.model.node.Node;
|
||||
import software.amazon.smithy.model.shapes.ShapeId;
|
||||
import software.amazon.smithy.model.traits.TraitFactory;
|
||||
|
||||
public class ApiKeySourceTraitTest {
|
||||
@Test
|
||||
public void registersTrait() {
|
||||
TraitFactory factory = TraitFactory.createServiceFactory();
|
||||
var id = ShapeId.from("smithy.example#Foo");
|
||||
var trait = factory.createTrait(ApiKeySourceTrait.NAME, id, Node.from("HEADER")).get();
|
||||
|
||||
assertThat(trait, instanceOf(ApiKeySourceTrait.class));
|
||||
assertThat(factory.createTrait(ApiKeySourceTrait.NAME, id, trait.toNode()).get(), equalTo(trait));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
package software.amazon.smithy.aws.traits.apigateway;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.instanceOf;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import software.amazon.smithy.model.node.Node;
|
||||
import software.amazon.smithy.model.shapes.ShapeId;
|
||||
import software.amazon.smithy.model.traits.TraitFactory;
|
||||
|
||||
public class AuthorizersTraitTest {
|
||||
@Test
|
||||
public void registersTrait() {
|
||||
TraitFactory factory = TraitFactory.createServiceFactory();
|
||||
var id = ShapeId.from("smithy.example#Foo");
|
||||
var node = Node.objectNodeBuilder()
|
||||
.withMember("aws.v4", Node.objectNodeBuilder()
|
||||
.withMember("clientType", "awsSigV4")
|
||||
.withMember("type", "request")
|
||||
.withMember("uri", "arn:foo:baz")
|
||||
.withMember("credentials", "arn:foo:bar")
|
||||
.withMember("identitySource", "mapping.expression")
|
||||
.withMember("identityValidationExpression", "[A-Z]+")
|
||||
.withMember("resultTtlInSeconds", 100)
|
||||
.build())
|
||||
.build();
|
||||
var trait = factory.createTrait(AuthorizersTrait.NAME, id, node).get();
|
||||
|
||||
assertThat(trait, instanceOf(AuthorizersTrait.class));
|
||||
assertThat(factory.createTrait(AuthorizersTrait.NAME, id, trait.toNode()).get(), equalTo(trait));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
package software.amazon.smithy.aws.traits.apigateway;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.instanceOf;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import software.amazon.smithy.model.node.Node;
|
||||
import software.amazon.smithy.model.shapes.ShapeId;
|
||||
import software.amazon.smithy.model.traits.TraitFactory;
|
||||
|
||||
public class RequestValidatorTraitTest {
|
||||
@Test
|
||||
public void registersTrait() {
|
||||
TraitFactory factory = TraitFactory.createServiceFactory();
|
||||
var id = ShapeId.from("smithy.example#Foo");
|
||||
var trait = factory.createTrait(RequestValidatorTrait.NAME, id, Node.from("full")).get();
|
||||
|
||||
assertThat(trait, instanceOf(RequestValidatorTrait.class));
|
||||
assertThat(factory.createTrait(RequestValidatorTrait.NAME, id, trait.toNode()).get(), equalTo(trait));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
package software.amazon.smithy.aws.traits.apigateway;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import software.amazon.smithy.model.Model;
|
||||
import software.amazon.smithy.model.loader.ModelAssembler;
|
||||
import software.amazon.smithy.model.validation.testrunner.SmithyTestSuite;
|
||||
|
||||
public class RunnerTest {
|
||||
@Test
|
||||
public void testRunner() {
|
||||
ModelAssembler assembler = Model.assembler(getClass().getClassLoader())
|
||||
.discoverModels(getClass().getClassLoader());
|
||||
|
||||
System.out.println(SmithyTestSuite.runner()
|
||||
.setModelAssemblerFactory(assembler::copy)
|
||||
.addTestCasesFromUrl(RunnerTest.class.getResource("errorfiles"))
|
||||
.run());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* Copyright 2019 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.smithy.aws.traits.iam;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.util.Optional;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import software.amazon.smithy.model.node.Node;
|
||||
import software.amazon.smithy.model.shapes.ShapeId;
|
||||
import software.amazon.smithy.model.traits.Trait;
|
||||
import software.amazon.smithy.model.traits.TraitFactory;
|
||||
|
||||
public class ActionPermissionDescriptionTraitTest {
|
||||
@Test
|
||||
public void createsTrait() {
|
||||
Node node = Node.from("Foo baz bar");
|
||||
TraitFactory provider = TraitFactory.createServiceFactory();
|
||||
Optional<Trait> trait = provider.createTrait(
|
||||
"aws.iam#actionPermissionDescription", ShapeId.from("ns.foo#foo"), node);
|
||||
|
||||
assertTrue(trait.isPresent());
|
||||
ActionPermissionDescriptionTrait actionDescription = (ActionPermissionDescriptionTrait) trait.get();
|
||||
assertThat(actionDescription.getValue(), equalTo("Foo baz bar"));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
/*
|
||||
* Copyright 2019 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.smithy.aws.traits.iam;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.contains;
|
||||
import static org.hamcrest.Matchers.containsInAnyOrder;
|
||||
import static org.hamcrest.Matchers.not;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import software.amazon.smithy.model.Model;
|
||||
import software.amazon.smithy.model.shapes.ShapeId;
|
||||
import software.amazon.smithy.model.validation.Severity;
|
||||
import software.amazon.smithy.model.validation.ValidationEvent;
|
||||
|
||||
public class ConditionKeysIndexTest {
|
||||
@Test
|
||||
public void successfullyLoadsConditionKeys() {
|
||||
Model model = Model.assembler()
|
||||
.addImport(getClass().getResource("successful-condition-keys.smithy"))
|
||||
.discoverModels(getClass().getClassLoader())
|
||||
.assemble()
|
||||
.unwrap();
|
||||
ShapeId service = ShapeId.from("smithy.example#MyService");
|
||||
|
||||
ConditionKeysIndex index = model.getKnowledge(ConditionKeysIndex.class);
|
||||
assertThat(index.getConditionKeyNames(service), containsInAnyOrder(
|
||||
"aws:accountId", "foo:baz", "myservice:Resource1Id1", "myservice:Resource2Id2"));
|
||||
assertThat(index.getConditionKeyNames(service, ShapeId.from("smithy.example#Operation1")),
|
||||
containsInAnyOrder("aws:accountId", "foo:baz"));
|
||||
assertThat(index.getConditionKeyNames(service, ShapeId.from("smithy.example#Resource1")),
|
||||
containsInAnyOrder("aws:accountId", "foo:baz", "myservice:Resource1Id1"));
|
||||
// Note that ID1 is not duplicated but rather reused on the child operation.
|
||||
assertThat(index.getConditionKeyNames(service, ShapeId.from("smithy.example#Resource2")),
|
||||
containsInAnyOrder("aws:accountId", "foo:baz",
|
||||
"myservice:Resource1Id1", "myservice:Resource2Id2"));
|
||||
// Note that this operation does bind all identifiers, so it includes both ID1 and 2.
|
||||
assertThat(index.getConditionKeyNames(service, ShapeId.from("smithy.example#GetResource2")),
|
||||
containsInAnyOrder("aws:accountId", "foo:baz",
|
||||
"myservice:Resource1Id1", "myservice:Resource2Id2"));
|
||||
// Note that this operation does not bind all identifiers, so it does not include ID2.
|
||||
assertThat(index.getConditionKeyNames(service, ShapeId.from("smithy.example#ListResource2")),
|
||||
containsInAnyOrder("aws:accountId", "foo:baz", "myservice:Resource1Id1"));
|
||||
|
||||
// Defined context keys are assembled from the names and mapped with the definitions.
|
||||
assertThat(index.getDefinedConditionKeys(service).get("myservice:Resource1Id1").getDocumentation(),
|
||||
not(Optional.empty()));
|
||||
assertThat(index.getDefinedConditionKeys(service, ShapeId.from("smithy.example#GetResource2")).keySet(),
|
||||
containsInAnyOrder("foo:baz", "myservice:Resource1Id1", "myservice:Resource2Id2"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void detectsUnkownConditionKeys() {
|
||||
var result = Model.assembler()
|
||||
.addImport(getClass().getResource("invalid-condition-keys.smithy"))
|
||||
.discoverModels(getClass().getClassLoader())
|
||||
.assemble();
|
||||
|
||||
assertTrue(result.isBroken());
|
||||
assertThat(result.getValidationEvents(Severity.ERROR).stream()
|
||||
.map(ValidationEvent::getEventId)
|
||||
.collect(Collectors.toSet()),
|
||||
contains("ConditionKeys"));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* Copyright 2019 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.smithy.aws.traits.iam;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.containsInAnyOrder;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import software.amazon.smithy.model.Model;
|
||||
import software.amazon.smithy.model.shapes.Shape;
|
||||
import software.amazon.smithy.model.shapes.ShapeId;
|
||||
|
||||
public class RequiredActionsTraitTest {
|
||||
@Test
|
||||
public void loadsFromModel() {
|
||||
Model result = Model.assembler()
|
||||
.discoverModels(getClass().getClassLoader())
|
||||
.addImport(getClass().getResource("required-actions.smithy"))
|
||||
.assemble()
|
||||
.unwrap();
|
||||
|
||||
Shape myOperation = result.getShapeIndex().getShape(ShapeId.from("smithy.example#MyOperation")).get();
|
||||
|
||||
assertTrue(myOperation.hasTrait(RequiredActionsTrait.class));
|
||||
assertThat(myOperation.getTrait(RequiredActionsTrait.class).get().getValues(), containsInAnyOrder(
|
||||
"iam:PassRole", "ec2:RunInstances"));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
[ERROR] (AuthorizersTrait) ns.foo#SomeService / [5, 22]: Invalid `aws.apigateway#authorizers` entry `another` does not match one of the `authentication` schemes defined on the service: [`aws.v4`]
|
||||
[ERROR] (AuthorizersTrait) ns.foo#SomeService / [5, 22]: Invalid `aws.apigateway#authorizers` entry `invalid` does not match one of the `authentication` schemes defined on the service: [`aws.v4`]
|
|
@ -0,0 +1,33 @@
|
|||
{
|
||||
"smithy": "1.0",
|
||||
"ns.foo": {
|
||||
"shapes": {
|
||||
"SomeService": {
|
||||
"type": "service",
|
||||
"version": "2018-03-17",
|
||||
"authentication": {"aws.v4": {}},
|
||||
"aws.apigateway#authorizers": {
|
||||
"invalid": {
|
||||
"clientType": "awsSigV4",
|
||||
"type": "request",
|
||||
"uri": "arn:foo:baz"
|
||||
},
|
||||
"another": {
|
||||
"clientType": "awsSigV4",
|
||||
"type": "token",
|
||||
"uri": "arn:foo:baz"
|
||||
},
|
||||
"aws.v4": {
|
||||
"clientType": "awsSigV4",
|
||||
"type": "request",
|
||||
"uri": "arn:foo:baz",
|
||||
"credentials": "arn:foo:bar",
|
||||
"identitySource": "mapping.expression",
|
||||
"identityValidationExpression": "[A-Z]+",
|
||||
"resultTtlInSeconds": 100
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"smithy": "1.0",
|
||||
"ns.foo": {
|
||||
"shapes": {
|
||||
"A": {
|
||||
"type": "string",
|
||||
"aws.api#data": "account"
|
||||
},
|
||||
"B": {
|
||||
"type": "list",
|
||||
"member": {
|
||||
"target": "BMember"
|
||||
},
|
||||
"aws.api#data": "tagging"
|
||||
},
|
||||
"BMember": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
$version:1.0
|
||||
namespace smithy.example
|
||||
|
||||
@aws.api#service(sdkId: "My")
|
||||
@aws.iam#defineConditionKeys("foo:baz": {type: "String", documentation: "Foo baz"})
|
||||
service MyService {
|
||||
version: "2019-02-20",
|
||||
operations: [Operation]
|
||||
}
|
||||
|
||||
@aws.iam#conditionKeys(["foo:qux"])
|
||||
operation Operation()
|
|
@ -0,0 +1,5 @@
|
|||
$version:1.0
|
||||
namespace smithy.example
|
||||
|
||||
@aws.iam#requiredActions(["iam:PassRole", "ec2:RunInstances"])
|
||||
operation MyOperation()
|
|
@ -0,0 +1,58 @@
|
|||
$version:1.0
|
||||
namespace smithy.example
|
||||
|
||||
@aws.api#service(sdkId: "My")
|
||||
@aws.iam#defineConditionKeys("foo:baz": {type: "String", documentation: "Foo baz"})
|
||||
service MyService {
|
||||
version: "2019-02-20",
|
||||
operations: [Operation1],
|
||||
resources: [Resource1]
|
||||
}
|
||||
|
||||
@aws.iam#conditionKeys(["aws:accountId", "foo:baz"])
|
||||
operation Operation1()
|
||||
|
||||
@aws.iam#conditionKeys(["aws:accountId", "foo:baz"])
|
||||
@aws.iam#inferConditionKeys
|
||||
resource Resource1 {
|
||||
identifiers: {
|
||||
id1: ArnString,
|
||||
},
|
||||
resources: [Resource2]
|
||||
}
|
||||
|
||||
@aws.iam#inferConditionKeys
|
||||
resource Resource2 {
|
||||
identifiers: {
|
||||
id1: ArnString,
|
||||
id2: String,
|
||||
},
|
||||
read: GetResource2,
|
||||
list: ListResource2,
|
||||
}
|
||||
|
||||
@instanceOperation
|
||||
@readonly
|
||||
operation GetResource2(GetResource2Input)
|
||||
|
||||
structure GetResource2Input {
|
||||
@required
|
||||
id1: ArnString,
|
||||
|
||||
@required
|
||||
id2: String
|
||||
}
|
||||
|
||||
@readonly
|
||||
@collectionOperation
|
||||
operation ListResource2(ListResource2Input) -> ListResource2Output
|
||||
|
||||
structure ListResource2Input {
|
||||
@required
|
||||
id1: ArnString,
|
||||
}
|
||||
|
||||
structure ListResource2Output {}
|
||||
|
||||
@aws.api#arnReference(type: "ec2:Instance")
|
||||
string ArnString
|
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"smithy": "1.0",
|
||||
"ns.foo": {
|
||||
"shapes": {
|
||||
"SomeService": {
|
||||
"type": "service",
|
||||
"version": "2018-03-17",
|
||||
"aws.api#service": {
|
||||
"sdkId": "Some Value",
|
||||
"arnNamespace": "service",
|
||||
"cloudFormationName": "SomeService"
|
||||
},
|
||||
"resources": [
|
||||
"A"
|
||||
]
|
||||
},
|
||||
"A": {
|
||||
"type": "resource",
|
||||
"resources": [
|
||||
"B"
|
||||
]
|
||||
},
|
||||
"B": {
|
||||
"type": "resource",
|
||||
"resources": [
|
||||
"A"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"smithy": "1.0",
|
||||
"ns.foo": {
|
||||
"shapes": {
|
||||
"SomeService": {
|
||||
"type": "service",
|
||||
"version": "2018-03-17",
|
||||
"aws.api#service": {
|
||||
"sdkId": "Some Value"
|
||||
},
|
||||
"resources": [
|
||||
"A"
|
||||
]
|
||||
},
|
||||
"A": {
|
||||
"type": "resource",
|
||||
"aws.api#arn": {
|
||||
"template": "{;;;}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
{
|
||||
"smithy": "1.0",
|
||||
"ns.foo": {
|
||||
"shapes": {
|
||||
"SomeService": {
|
||||
"type": "service",
|
||||
"version": "2018-03-17",
|
||||
"aws.api#service": {
|
||||
"sdkId": "Some Value",
|
||||
"arnNamespace": "service",
|
||||
"cloudFormationName": "SomeService"
|
||||
},
|
||||
"resources": [
|
||||
"A"
|
||||
]
|
||||
},
|
||||
"A": {
|
||||
"type": "resource",
|
||||
"identifiers": {
|
||||
"aid": "smithy.api#String"
|
||||
},
|
||||
"aws.api#arn": {
|
||||
"template": "a"
|
||||
},
|
||||
"resources": [
|
||||
"B"
|
||||
]
|
||||
},
|
||||
"B": {
|
||||
"type": "resource",
|
||||
"identifiers": {
|
||||
"aid": "smithy.api#String",
|
||||
"bid": "smithy.api#String"
|
||||
},
|
||||
"aws.api#arn": {
|
||||
"template": "a\/b"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
namespace smithy.example
|
||||
|
||||
@aws.api#requiredActions(["iam:PassRole", "ec2:RunInstances"])
|
||||
operation MyOperation()
|
|
@ -0,0 +1,96 @@
|
|||
{
|
||||
"smithy": "1.0",
|
||||
"ns.foo": {
|
||||
"shapes": {
|
||||
"NonAwsService": {
|
||||
"type": "service",
|
||||
"version": "2018-03-17"
|
||||
},
|
||||
"EmptyAwsService": {
|
||||
"type": "service",
|
||||
"version": "2018-03-17",
|
||||
"aws.api#service": {
|
||||
"sdkId": "Something Empty"
|
||||
}
|
||||
},
|
||||
"SomeService": {
|
||||
"type": "service",
|
||||
"version": "2018-03-17",
|
||||
"aws.api#service": {
|
||||
"sdkServiceId": "Some Value",
|
||||
"arnNamespace": "service",
|
||||
"cloudFormationName": "SomeService"
|
||||
},
|
||||
"resources": [
|
||||
"SomeResource",
|
||||
"RootArnResource",
|
||||
"AbsoluteResource"
|
||||
]
|
||||
},
|
||||
"RootArnResource": {
|
||||
"type": "resource",
|
||||
"aws.api#arn": {
|
||||
"noRegion": true,
|
||||
"noAccount": true,
|
||||
"template": "rootArnResource"
|
||||
}
|
||||
},
|
||||
"SomeResource": {
|
||||
"type": "resource",
|
||||
"identifiers": {
|
||||
"someId": "SomeResourceId"
|
||||
},
|
||||
"aws.api#arn": {
|
||||
"template": "someresource\/{someId}"
|
||||
},
|
||||
"resources": [
|
||||
"ChildResource"
|
||||
]
|
||||
},
|
||||
"ChildResource": {
|
||||
"type": "resource",
|
||||
"identifiers": {
|
||||
"someId": "SomeResourceId",
|
||||
"childId": "ChildResourceId"
|
||||
},
|
||||
"aws.api#arn": {
|
||||
"template": "someresource\/{someId}\/{childId}"
|
||||
},
|
||||
"resources": [
|
||||
"AnotherChild"
|
||||
]
|
||||
},
|
||||
"AnotherChild": {
|
||||
"type": "resource",
|
||||
"identifiers": {
|
||||
"someId": "SomeResourceId",
|
||||
"childId": "ChildResourceId"
|
||||
}
|
||||
},
|
||||
"AbsoluteResource": {
|
||||
"type": "resource",
|
||||
"identifiers": {
|
||||
"arn": "AbsoluteResourceArn"
|
||||
},
|
||||
"aws.api#arn": {
|
||||
"template": "{arn}",
|
||||
"absolute": true
|
||||
}
|
||||
},
|
||||
"AbsoluteResourceArn": {
|
||||
"type": "string",
|
||||
"aws.api#arnReference": {
|
||||
"type": "AWS::SomeService::AbsoluteResource",
|
||||
"service": "ns.foo#SomeService",
|
||||
"resource": "ns.foo#AbsoluteResource"
|
||||
}
|
||||
},
|
||||
"SomeResourceId": {
|
||||
"type": "string"
|
||||
},
|
||||
"ChildResourceId": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
{
|
||||
"smithy": "1.0",
|
||||
"ns.foo": {
|
||||
"shapes": {
|
||||
"SomeService": {
|
||||
"type": "service",
|
||||
"version": "2018-03-17",
|
||||
"aws.api#service": {
|
||||
"sdkId": "Some Value",
|
||||
"arnNamespace": "service",
|
||||
"cloudFormationName": "SomeService"
|
||||
},
|
||||
"resources": [
|
||||
"A"
|
||||
]
|
||||
},
|
||||
"A": {
|
||||
"type": "resource",
|
||||
"identifiers": {
|
||||
"aid": "smithy.api#String"
|
||||
},
|
||||
"aws.api#arn": {
|
||||
"template": "a\/{aid}\/{InvalidId}\/{InvalidId2}"
|
||||
},
|
||||
"resources": [
|
||||
"B"
|
||||
]
|
||||
},
|
||||
"B": {
|
||||
"type": "resource",
|
||||
"identifiers": {
|
||||
"aid": "smithy.api#String",
|
||||
"bid": "smithy.api#String"
|
||||
},
|
||||
"aws.api#arn": {
|
||||
"template": "a\/{aid}\/{InvalidId}\/{InvalidId2}\/b\/{bid}\/{AnotherInvalid}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"smithy": "1.0",
|
||||
|
||||
"ns.foo": {
|
||||
"shapes": {
|
||||
"Unsigned1": {
|
||||
"type": "operation",
|
||||
"aws.api#unsignedPayload": []
|
||||
},
|
||||
"Unsigned2": {
|
||||
"type": "operation",
|
||||
"aws.api#unsignedPayload": ["aws.v4"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,186 @@
|
|||
/*
|
||||
* Copyright 2019 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.
|
||||
*/
|
||||
|
||||
plugins {
|
||||
`java-library`
|
||||
`maven-publish`
|
||||
checkstyle
|
||||
jacoco
|
||||
id("com.github.spotbugs") version "1.6.10"
|
||||
id("org.javamodularity.moduleplugin") version "1.4.0"
|
||||
}
|
||||
|
||||
// Set a global group ID and version on each project. This version might
|
||||
// need to be overridden is a project ever needs to be version bumped out
|
||||
// of band with the rest of the projects.
|
||||
allprojects {
|
||||
group = "software.amazon.smithy"
|
||||
version = "0.1.0"
|
||||
}
|
||||
|
||||
subprojects {
|
||||
/*
|
||||
* Java
|
||||
* ====================================================
|
||||
*
|
||||
* By default, build each subproject as a java library.
|
||||
* We can add if-statements around this plugin to change
|
||||
* how specific subprojects are built (for example, if
|
||||
* we build Sphinx subprojects with Gradle).
|
||||
*/
|
||||
apply(plugin = "java-library")
|
||||
|
||||
if (plugins.hasPlugin("java")) {
|
||||
java {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
}
|
||||
|
||||
tasks.withType(JavaCompile::class) {
|
||||
options.encoding = "UTF-8"
|
||||
}
|
||||
|
||||
// Use Junit5's test runner.
|
||||
tasks.withType<Test> {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
// Apply junit 5 and hamcrest test dependencies to all java projects.
|
||||
dependencies {
|
||||
testCompile("org.junit.jupiter:junit-jupiter-api:5.4.0")
|
||||
testRuntime("org.junit.jupiter:junit-jupiter-engine:5.4.0")
|
||||
testCompile("org.junit.jupiter:junit-jupiter-params:5.4.0")
|
||||
testCompile("org.hamcrest:hamcrest:2.1")
|
||||
}
|
||||
|
||||
// Reusable license copySpec
|
||||
val licenseSpec = copySpec {
|
||||
from("${project.rootDir}/LICENSE.txt")
|
||||
from("${project.rootDir}/NOTICE.txt")
|
||||
}
|
||||
|
||||
// Set up tasks that build source and javadoc jars.
|
||||
tasks.register<Jar>("sourcesJar") {
|
||||
metaInf.with(licenseSpec)
|
||||
from(sourceSets.main.get().allJava)
|
||||
archiveClassifier.set("sources")
|
||||
}
|
||||
|
||||
tasks.register<Jar>("javadocJar") {
|
||||
metaInf.with(licenseSpec)
|
||||
from(tasks.javadoc)
|
||||
archiveClassifier.set("javadoc")
|
||||
}
|
||||
|
||||
// Configure jars to include license related info
|
||||
tasks.jar {
|
||||
metaInf.with(licenseSpec)
|
||||
}
|
||||
|
||||
// Always run javadoc after build.
|
||||
tasks["build"].finalizedBy(tasks["javadoc"])
|
||||
}
|
||||
|
||||
/*
|
||||
* Java Modules
|
||||
* ====================================================
|
||||
*
|
||||
* Build using Java modules.
|
||||
*/
|
||||
if (plugins.hasPlugin("java")) {
|
||||
apply(plugin = "org.javamodularity.moduleplugin")
|
||||
}
|
||||
|
||||
/*
|
||||
* Maven
|
||||
* ====================================================
|
||||
*
|
||||
* Publish to Maven central.
|
||||
*/
|
||||
if (plugins.hasPlugin("java")) {
|
||||
apply(plugin = "maven-publish")
|
||||
|
||||
repositories {
|
||||
mavenLocal()
|
||||
maven {
|
||||
url = uri("http://repo.maven.apache.org/maven2")
|
||||
}
|
||||
}
|
||||
|
||||
publishing {
|
||||
publications {
|
||||
create<MavenPublication>("mavenJava") {
|
||||
from(components["java"])
|
||||
|
||||
// Ship the source and javadoc jars.
|
||||
artifact(tasks["sourcesJar"])
|
||||
artifact(tasks["javadocJar"])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* CheckStyle
|
||||
* ====================================================
|
||||
*
|
||||
* Apply CheckStyle to source files but not tests.
|
||||
*/
|
||||
if (plugins.hasPlugin("java")) {
|
||||
apply(plugin = "checkstyle")
|
||||
|
||||
tasks["checkstyleTest"].enabled = false
|
||||
}
|
||||
|
||||
/*
|
||||
* Code coverage
|
||||
* ====================================================
|
||||
*
|
||||
* Create code coverage reports after running tests.
|
||||
*/
|
||||
if (plugins.hasPlugin("java")) {
|
||||
apply(plugin = "jacoco")
|
||||
|
||||
// Always run the jacoco test report after testing.
|
||||
tasks["test"].finalizedBy(tasks["jacocoTestReport"])
|
||||
|
||||
// Configure jacoco to generate an HTML report.
|
||||
tasks.jacocoTestReport {
|
||||
reports {
|
||||
xml.isEnabled = false
|
||||
csv.isEnabled = false
|
||||
html.destination = file("$buildDir/reports/jacoco")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Spotbugs
|
||||
* ====================================================
|
||||
*
|
||||
* Run spotbugs against source files and configure suppressions.
|
||||
*/
|
||||
if (plugins.hasPlugin("java")) {
|
||||
apply(plugin = "com.github.spotbugs")
|
||||
|
||||
// We don't need to lint tests.
|
||||
tasks["spotbugsTest"].enabled = false
|
||||
|
||||
// Configure the bug filter for spotbugs.
|
||||
tasks.withType(com.github.spotbugs.SpotBugsTask::class) {
|
||||
effort = "max"
|
||||
excludeFilterConfig = project.resources.text.fromFile("${project.rootDir}/config/spotbugs/filter.xml")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
# Smithy Codegen
|
||||
|
||||
Code generation framework for generating clients, servers, documentation,
|
||||
and other artifacts for various languages from Smithy models.
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* Copyright 2019 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.
|
||||
*/
|
||||
|
||||
dependencies {
|
||||
api(project(":smithy-model"))
|
||||
api(project(":smithy-build"))
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* Copyright 2019 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.
|
||||
*/
|
||||
|
||||
module software.amazon.smithy.codegen.core {
|
||||
requires java.logging;
|
||||
requires software.amazon.smithy.model;
|
||||
requires software.amazon.smithy.build;
|
||||
|
||||
exports software.amazon.smithy.codegen.core;
|
||||
}
|
|
@ -0,0 +1,452 @@
|
|||
/*
|
||||
* Copyright 2019 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.smithy.codegen.core;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.Deque;
|
||||
import java.util.Formatter;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Helper class for generating code inside of a string.
|
||||
*
|
||||
* <p>A CodeWriter should be used for more advanced code generation than
|
||||
* what is possible inside of templates. A CodeWriter can be used to write
|
||||
* basically any kind of code, including whitespace sensitive and brace-based.
|
||||
* Inserting and closing braces is not handled automatically by this class.
|
||||
*
|
||||
* <p>The CodeWriter can maintain a stack of transformation state, including
|
||||
* the character used for newlines, the text used to indent, a prefix to add
|
||||
* before each line, the number of times to indent, whether or not whitespace
|
||||
* is trimmed from the end of newlines, and whether or not N number of
|
||||
* newlines are combined into a single newline. State can be pushed onto the
|
||||
* stack using {@link #pushState} which copies the current state. Mutations
|
||||
* can then be made to the top-most state of the CodeWriter and do not affect
|
||||
* previous states. The previous transformation state of the CodeWriter can
|
||||
* later be restored using {@link #popState}.
|
||||
*
|
||||
* <p>The following example writes out some Python code:
|
||||
*
|
||||
* <pre>
|
||||
* {@code
|
||||
* CodeWriter writer = CodeWriter.createDefault();
|
||||
* writer.write("def Foo(str):")
|
||||
* .indent()
|
||||
* .write("print str")
|
||||
* String code = writer.toString();
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* The CodeWriter is stateful, and a prefix can be added before each line.
|
||||
* This is useful for doing things like create javadoc strings:
|
||||
*
|
||||
* <pre>
|
||||
* {@code
|
||||
* CodeWriter writer = CodeWriter.createDefault();
|
||||
* writer.write("/**")
|
||||
* .setNewlinePrefix(" * ")
|
||||
* .write("This is some docs.")
|
||||
* .write("And more docs.\n\n\n")
|
||||
* .write("Foo.")
|
||||
* .setNewlinePrefix("")
|
||||
* .write(" *\/");
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* The above example outputs:
|
||||
*
|
||||
* <pre>
|
||||
* {@code
|
||||
* /**
|
||||
* * This is some docs.
|
||||
* * And more docs.
|
||||
* *
|
||||
* * Foo.
|
||||
* *\/
|
||||
*
|
||||
* ^ Minus this escape character
|
||||
* }
|
||||
* </pre>
|
||||
*/
|
||||
public final class CodeWriter {
|
||||
private final StringBuilder builder = new StringBuilder();
|
||||
private final Deque<State> states = new ArrayDeque<>();
|
||||
private State currentState;
|
||||
private boolean trailingNewline;
|
||||
private boolean pendingNewline;
|
||||
private boolean onBlankLine = true;
|
||||
private int blankLineCount;
|
||||
|
||||
/**
|
||||
* Creates a new CodeWriter that uses "\n" for a newline, four spaces
|
||||
* for indentation, does not strip trailing whitespace, does not flatten
|
||||
* multiple successive blank lines into a single blank line, and adds no
|
||||
* trailing new line.
|
||||
*/
|
||||
public CodeWriter() {
|
||||
states.push(new State());
|
||||
currentState = states.getFirst();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a default instance of a CodeWriter that uses "\n" for newlines,
|
||||
* flattens multiple successive blank lines into a single blank line,
|
||||
* and adds a trailing new line if needed when converting the CodeWriter
|
||||
* to a string.
|
||||
*
|
||||
* @return Returns the created and configured CodeWriter.
|
||||
*/
|
||||
public static CodeWriter createDefault() {
|
||||
return new CodeWriter()
|
||||
.setNewline("\n")
|
||||
.setIndentText(" ")
|
||||
.trimTrailingSpaces()
|
||||
.trimBlankLines()
|
||||
.insertTrailingNewline();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the contents of the generated code.
|
||||
*
|
||||
* <p>The result will have an appended newline if the CodeWriter is
|
||||
* configured to always append a newline. A newline is only appended
|
||||
* in these cases if the result does not already end with a newline.
|
||||
*
|
||||
* @return Returns the generated code.
|
||||
*/
|
||||
public String toString() {
|
||||
String result = builder.toString();
|
||||
// Insert a new line if one is pending or if trailing new lines are to be
|
||||
// added and the content doesn't already end with a newline.
|
||||
return (pendingNewline || (trailingNewline && !result.endsWith(currentState.newline)))
|
||||
? result + currentState.newline
|
||||
: result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies and pushes the current state to the state stack.
|
||||
*
|
||||
* <p>This method is used to prepare for a corresponding {@link #popState}
|
||||
* operation later. It stores the current state of the CodeWriter into a
|
||||
* stack and keeps it active. After pushing, mutations can be made to the
|
||||
* state of the CodeWriter without affecting the previous state on the
|
||||
* stack. Changes to the state of the CodeWriter can be undone by using
|
||||
* {@link #popState()}, which returns the CodeWriter state to the state
|
||||
* it was in before calling {@code pushState}.
|
||||
*
|
||||
* @return Returns the code writer.
|
||||
*/
|
||||
public CodeWriter pushState() {
|
||||
State copiedState = new State(currentState);
|
||||
states.push(copiedState);
|
||||
currentState = copiedState;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pops the current CodeWriter state from the state stack.
|
||||
*
|
||||
* <p>This method is used to reverse a previous {@link #pushState}
|
||||
* operation. It configures the current CodeWriter state to what it was
|
||||
* before the last preceding {@code pushState} call.
|
||||
*
|
||||
* @return Returns the CodeWriter.
|
||||
* @throws CodegenException if there a no states to pop.
|
||||
*/
|
||||
public CodeWriter popState() {
|
||||
if (states.size() == 1) {
|
||||
throw new CodegenException("Cannot pop CodeWriter state because at the root state");
|
||||
}
|
||||
|
||||
states.pop();
|
||||
currentState = states.getFirst();
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the character that represents newlines ("\n" is the default).
|
||||
*
|
||||
* @param newline Newline character to use.
|
||||
* @return Returns the CodeWriter.
|
||||
*/
|
||||
public CodeWriter setNewline(String newline) {
|
||||
currentState.newline = newline;
|
||||
currentState.newlineRegexQuoted = Pattern.quote(newline);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a prefix to prepend to every line after a new line is added
|
||||
* (except for an inserted trailing newline).
|
||||
*
|
||||
* @param newlinePrefix Newline prefix to use.
|
||||
* @return Returns the CodeWriter.
|
||||
*/
|
||||
public CodeWriter setNewlinePrefix(String newlinePrefix) {
|
||||
currentState.newlinePrefix = newlinePrefix;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the text used for indentation (defaults to four spaces).
|
||||
*
|
||||
* @param indentText Indentation text.
|
||||
* @return Returns the CodeWriter.
|
||||
*/
|
||||
public CodeWriter setIndentText(String indentText) {
|
||||
currentState.indentText = indentText;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables the trimming of trailing spaces on a line.
|
||||
*
|
||||
* @return Returns the CodeWriter.
|
||||
*/
|
||||
public CodeWriter trimTrailingSpaces() {
|
||||
return trimTrailingSpaces(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures if trailing spaces on a line are removed.
|
||||
*
|
||||
* @param trimTrailingSpaces Set to true to trim trailing spaces.
|
||||
* @return Returns the CodeWriter.
|
||||
*/
|
||||
public CodeWriter trimTrailingSpaces(boolean trimTrailingSpaces) {
|
||||
currentState.trimTrailingSpaces = trimTrailingSpaces;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that no more than one blank line occurs in succession.
|
||||
*
|
||||
* @return Returns the CodeWriter.
|
||||
*/
|
||||
public CodeWriter trimBlankLines() {
|
||||
return trimBlankLines(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that no more than the given number of newlines can occur
|
||||
* in succession, removing consecutive newlines that exceed the given
|
||||
* threshold.
|
||||
*
|
||||
* @param trimBlankLines Number of allowed consecutive newlines. Set to
|
||||
* -1 to perform no trimming. Set to 0 to allow no blank lines. Set to
|
||||
* 1 or more to allow for no more than N consecutive blank lines.
|
||||
* @return Returns the CodeWriter.
|
||||
*/
|
||||
public CodeWriter trimBlankLines(int trimBlankLines) {
|
||||
currentState.trimBlankLines = trimBlankLines;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the CodeWriter to always append a newline at the end of
|
||||
* the text if one is not already present.
|
||||
*
|
||||
* <p>This setting is not captured as part of push/popState.
|
||||
*
|
||||
* @return Returns the CodeWriter.
|
||||
*/
|
||||
public CodeWriter insertTrailingNewline() {
|
||||
return insertTrailingNewline(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the CodeWriter to always append a newline at the end of
|
||||
* the text if one is not already present.
|
||||
*
|
||||
* <p>This setting is not captured as part of push/popState.
|
||||
*
|
||||
* @param trailingNewline Set to true to append a trailing new line.
|
||||
*
|
||||
* @return Returns the CodeWriter.
|
||||
*/
|
||||
public CodeWriter insertTrailingNewline(boolean trailingNewline) {
|
||||
this.trailingNewline = trailingNewline;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indents all text one level.
|
||||
*
|
||||
* @return Returns the CodeWriter.
|
||||
*/
|
||||
public CodeWriter indent() {
|
||||
return indent(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indents all text a specific number of levels.
|
||||
*
|
||||
* @param levels Number of levels to indent.
|
||||
* @return Returns the CodeWriter.
|
||||
*/
|
||||
public CodeWriter indent(int levels) {
|
||||
currentState.indentation += levels;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes one level of indentation from all lines.
|
||||
*
|
||||
* @return Returns the CodeWriter.
|
||||
*/
|
||||
public CodeWriter dedent() {
|
||||
return dedent(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a specific number of indentations from all lines.
|
||||
*
|
||||
* <p>Set to -1 to dedent back to 0 (root).
|
||||
*
|
||||
* @param levels Number of levels to remove.
|
||||
* @return Returns the CodeWriter.
|
||||
*/
|
||||
public CodeWriter dedent(int levels) {
|
||||
if (levels == -1) {
|
||||
currentState.indentation = 0;
|
||||
} else if (levels < 1 || currentState.indentation - levels < 0) {
|
||||
throw new CodegenException(String.format("Cannot dedent CodeWriter %d levels", levels));
|
||||
} else {
|
||||
currentState.indentation -= levels;
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes text to the CodeWriter and appends a newline.
|
||||
*
|
||||
* <p>The provided text is automatically formatted using a
|
||||
* {@link Formatter} and variadic arguments.
|
||||
*
|
||||
* @param content Content to write.
|
||||
* @param args String {@link Formatter} arguments to use for formatting.
|
||||
* @return Returns the CodeWriter.
|
||||
*/
|
||||
public CodeWriter write(Object content, Object... args) {
|
||||
writeInline(String.format(String.valueOf(content), args));
|
||||
pendingNewline = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes text to the CodeWriter and does not append a newline.
|
||||
*
|
||||
* @param content Content to write.
|
||||
* @return Returns the CodeWriter.
|
||||
*/
|
||||
public CodeWriter writeInline(Object content) {
|
||||
String[] lines = content.toString().split(currentState.newlineRegexQuoted, -1);
|
||||
|
||||
// Indent the given text.
|
||||
for (int lineNumber = 0; lineNumber < lines.length; lineNumber++) {
|
||||
String line = lines[lineNumber];
|
||||
|
||||
// Trim newlines if blank line control is enforced.
|
||||
if (pendingNewline
|
||||
&& (currentState.trimBlankLines == -1 || blankLineCount <= currentState.trimBlankLines)) {
|
||||
writeRaw(currentState.newline);
|
||||
pendingNewline = false;
|
||||
onBlankLine = true;
|
||||
}
|
||||
|
||||
// Don't register a pending new line on the last line when writing
|
||||
// inline. This is handled by the write() method.
|
||||
if (lineNumber < lines.length - 1) {
|
||||
pendingNewline = true;
|
||||
}
|
||||
|
||||
//line = newlinePrefix + line;
|
||||
if (currentState.trimTrailingSpaces) {
|
||||
line = stripTrailingSpaces(line);
|
||||
}
|
||||
|
||||
if (!line.isEmpty()) {
|
||||
blankLineCount = 0;
|
||||
|
||||
// Only write the newline prefix on a new line.
|
||||
if (onBlankLine) {
|
||||
writeIndent();
|
||||
writeRaw(currentState.newlinePrefix);
|
||||
}
|
||||
|
||||
writeRaw(line);
|
||||
onBlankLine = false;
|
||||
|
||||
} else {
|
||||
// Track how many blank lines have been seen so that they can
|
||||
// be omitted when necessary.
|
||||
blankLineCount++;
|
||||
|
||||
// If the line was blank, a newline was written, and the newline
|
||||
// prefix is not empty, then write the newline prefix.
|
||||
if (onBlankLine && !currentState.newlinePrefix.isEmpty()) {
|
||||
writeIndent();
|
||||
writeRaw(currentState.newlinePrefix);
|
||||
onBlankLine = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
private void writeIndent() {
|
||||
for (int i = 0; i < currentState.indentation; i++) {
|
||||
writeRaw(currentState.indentText);
|
||||
}
|
||||
}
|
||||
|
||||
private static String stripTrailingSpaces(String text) {
|
||||
return text.replaceAll("\\s+$", "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes raw text that is not processed in any way.
|
||||
*
|
||||
* @param content Content to write.
|
||||
*/
|
||||
private void writeRaw(String content) {
|
||||
builder.append(content);
|
||||
}
|
||||
|
||||
private static final class State {
|
||||
private String newline = "\n";
|
||||
private String newlineRegexQuoted = Pattern.quote("\n");
|
||||
private String indentText = " ";
|
||||
private String newlinePrefix = "";
|
||||
private int indentation;
|
||||
private boolean trimTrailingSpaces;
|
||||
private int trimBlankLines = -1;
|
||||
|
||||
State() {}
|
||||
|
||||
State(State copy) {
|
||||
this.newline = copy.newline;
|
||||
this.newlineRegexQuoted = copy.newlineRegexQuoted;
|
||||
this.indentText = copy.indentText;
|
||||
this.newlinePrefix = copy.newlinePrefix;
|
||||
this.indentation = copy.indentation;
|
||||
this.trimTrailingSpaces = copy.trimTrailingSpaces;
|
||||
this.trimBlankLines = copy.trimBlankLines;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* Copyright 2019 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.smithy.codegen.core;
|
||||
|
||||
import software.amazon.smithy.build.SmithyBuildException;
|
||||
|
||||
/**
|
||||
* Thrown when an error occurs during code generation.
|
||||
*/
|
||||
public class CodegenException extends SmithyBuildException {
|
||||
public CodegenException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public CodegenException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
public CodegenException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,123 @@
|
|||
/*
|
||||
* Copyright 2019 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.smithy.codegen.core;
|
||||
|
||||
import java.io.Writer;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import software.amazon.smithy.model.SmithyBuilder;
|
||||
|
||||
/**
|
||||
* Creates a template engine that always injects default values
|
||||
* into the data model.
|
||||
*
|
||||
* <p>Default values can be overridden per/template by passing in a
|
||||
* different value in the data model when rendering templates.
|
||||
*
|
||||
* <pre>
|
||||
* {@code
|
||||
* TemplateEngine myEngine = createMyTemplateEngine();
|
||||
* TemplateEngine wrappedEngine = DefaultDataTemplateEngine.builder()
|
||||
* .delegate(myEngine)
|
||||
* .put("foo", "baz")
|
||||
* .put("hello", true)
|
||||
* .build();
|
||||
* assert(wrappedEngine.renderString("{{ foo }}") == "baz");
|
||||
* }
|
||||
* </pre>
|
||||
*/
|
||||
public final class DefaultDataTemplateEngine implements TemplateEngine {
|
||||
private final TemplateEngine delegate;
|
||||
private final Map<String, Object> defaultContext;
|
||||
|
||||
public DefaultDataTemplateEngine(Map<String, Object> defaultContext, TemplateEngine delegate) {
|
||||
this.defaultContext = defaultContext;
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(String templatePath, Writer out, Map<String, Object> dataModel) {
|
||||
delegate.write(templatePath, out, merge(dataModel));
|
||||
}
|
||||
|
||||
private Map<String, Object> merge(Map<String, Object> map) {
|
||||
if (map.isEmpty()) {
|
||||
return defaultContext;
|
||||
} else if (defaultContext.isEmpty()) {
|
||||
return map;
|
||||
}
|
||||
|
||||
Map<String, Object> result = new HashMap<>(defaultContext);
|
||||
result.putAll(map);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a new DefaultDataTemplateEngine.
|
||||
*/
|
||||
public static final class Builder {
|
||||
private TemplateEngine delegate;
|
||||
private final Map<String, Object> defaultContext = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Builds the DefaultDataTemplateEngine.
|
||||
*
|
||||
* @return Returns the new template engine.
|
||||
* @throws IllegalStateException if a delegate was not set.
|
||||
*/
|
||||
public DefaultDataTemplateEngine build() {
|
||||
return new DefaultDataTemplateEngine(defaultContext, SmithyBuilder.requiredState("delegate", delegate));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the template engine to wrap and delegate to.
|
||||
*
|
||||
* @param delegate The template engine to wrap.
|
||||
* @return Returns the builder.
|
||||
*/
|
||||
public Builder delegate(TemplateEngine delegate) {
|
||||
this.delegate = delegate;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a specific template variable.
|
||||
*
|
||||
* @param key Key to set.
|
||||
* @param value Value to set.
|
||||
* @return Returns the builder.
|
||||
*/
|
||||
public Builder put(String key, Object value) {
|
||||
this.defaultContext.put(key, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets zero or more template variables from a map of key-value pairs.
|
||||
*
|
||||
* @param map Map of value to merge into the data model.
|
||||
* @return Returns the builder.
|
||||
*/
|
||||
public Builder putAll(Map<String, Object> map) {
|
||||
this.defaultContext.putAll(map);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,134 @@
|
|||
/*
|
||||
* Copyright 2019 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.smithy.codegen.core;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import software.amazon.smithy.model.SmithyBuilder;
|
||||
|
||||
/**
|
||||
* A reserved words implementation that maps known words to other words.
|
||||
*
|
||||
* <p>The following example shows how to use this class to make reserved
|
||||
* words safe for the targeted code:
|
||||
*
|
||||
* <pre>
|
||||
* {@code
|
||||
* ReservedWords reserved = MappedReservedWords.builder()
|
||||
* .put("exception", "apiException")
|
||||
* .put("void", "void_")
|
||||
* .build();
|
||||
* String safeWord = reserved.escape("exception");
|
||||
* System.out.println(safeWord); // outputs "apiException"
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* <p>The detection of reserved words can be made case-insensitive such
|
||||
* that "bar", "BAR", "Bar", etc., can be detected as reserved words.
|
||||
*
|
||||
* <pre>
|
||||
* {@code
|
||||
* ReservedWords reserved = MappedReservedWords.builder()
|
||||
* .put("foo", "Hi")
|
||||
* .putCaseInsensitive("bar", "bam")
|
||||
* .build();
|
||||
*
|
||||
* assert(reserved.escape("foo").equals("Hi"));
|
||||
* assert(reserved.escape("Foo").equals("Foo"));
|
||||
* assert(reserved.escape("BAR").equals("bam"));
|
||||
* }
|
||||
* </pre>
|
||||
*/
|
||||
public final class MappedReservedWords implements ReservedWords {
|
||||
private final Map<String, String> mappings;
|
||||
private final Map<String, String> caseInsensitiveMappings;
|
||||
|
||||
/**
|
||||
* @param mappings Map of reserved word to replacement words.
|
||||
* @param caseInsensitiveMappings Map of case-insensitive reserved word to replacement words.
|
||||
*/
|
||||
public MappedReservedWords(Map<String, String> mappings, Map<String, String> caseInsensitiveMappings) {
|
||||
this.mappings = Map.copyOf(mappings);
|
||||
this.caseInsensitiveMappings = Map.copyOf(caseInsensitiveMappings);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Creates a new Builder.
|
||||
*/
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String escape(String word) {
|
||||
String result = mappings.get(word);
|
||||
if (result == null) {
|
||||
result = caseInsensitiveMappings.get(word.toLowerCase(Locale.US));
|
||||
}
|
||||
|
||||
return result != null ? result : word;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isReserved(String word) {
|
||||
return mappings.containsKey(word) || caseInsensitiveMappings.containsKey(word.toLowerCase(Locale.US));
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder to create a new {@link MappedReservedWords} instance.
|
||||
*/
|
||||
public static final class Builder implements SmithyBuilder<ReservedWords> {
|
||||
private final Map<String, String> mappings = new HashMap<>();
|
||||
private final Map<String, String> caseInsensitiveMappings = new HashMap<>();
|
||||
|
||||
private Builder() {}
|
||||
|
||||
/**
|
||||
* Add a new reserved words.
|
||||
*
|
||||
* @param reservedWord Reserved word to convert.
|
||||
* @param conversion Word to convert to.
|
||||
* @return Returns the builder.
|
||||
*/
|
||||
public Builder put(String reservedWord, String conversion) {
|
||||
mappings.put(reservedWord, conversion);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new case-insensitive reserved words.
|
||||
*
|
||||
* @param reservedWord Case-insensitive reserved word to convert.
|
||||
* @param conversion Word to convert to.
|
||||
* @return Returns the builder.
|
||||
*/
|
||||
public Builder putCaseInsensitive(String reservedWord, String conversion) {
|
||||
caseInsensitiveMappings.put(reservedWord.toLowerCase(Locale.US), conversion);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the reserved words.
|
||||
*
|
||||
* @return Returns the created reserved words implementation.
|
||||
*/
|
||||
@Override
|
||||
public ReservedWords build() {
|
||||
return new MappedReservedWords(mappings, caseInsensitiveMappings);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,184 @@
|
|||
/*
|
||||
* Copyright 2019 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.smithy.codegen.core;
|
||||
|
||||
import java.util.logging.Logger;
|
||||
import software.amazon.smithy.model.SmithyBuilder;
|
||||
import software.amazon.smithy.model.shapes.Shape;
|
||||
|
||||
/**
|
||||
* Decorates a {@link SymbolProvider} by passing values through context
|
||||
* specific {@link ReservedWords} implementations.
|
||||
*
|
||||
* <p>A specific {@code ReservedWords} implementation can be registered
|
||||
* for each kind of symbol provided by the delegated {@code SymbolProvider}.
|
||||
* For example, reserved words can be created that are specific to
|
||||
* class names.
|
||||
*
|
||||
* <p>This motivation behind this class is to allow more general purpose
|
||||
* implementations of {@code SymbolProvider} and {@code ReservedWords} to
|
||||
* be composed.
|
||||
*
|
||||
* <p>A warning is logged each time a symbol is renamed by a reserved words
|
||||
* implementation.
|
||||
*/
|
||||
public final class ReservedWordSymbolProvider implements SymbolProvider {
|
||||
private static final Logger LOGGER = Logger.getLogger(ReservedWordSymbolProvider.class.getName());
|
||||
private static final ReservedWords IDENTITY = ReservedWords.identity();
|
||||
|
||||
private final SymbolProvider delegate;
|
||||
private final ReservedWords filenameReservedWords;
|
||||
private final ReservedWords namespaceReservedWords;
|
||||
private final ReservedWords nameReservedWords;
|
||||
private final ReservedWords memberReservedWords;
|
||||
|
||||
private ReservedWordSymbolProvider(Builder builder) {
|
||||
this.delegate = SmithyBuilder.requiredState("symbolProvider", builder.delegate);
|
||||
this.filenameReservedWords = resolveReserved(builder.filenameReservedWords);
|
||||
this.namespaceReservedWords = resolveReserved(builder.namespaceReservedWords);
|
||||
this.nameReservedWords = resolveReserved(builder.nameReservedWords);
|
||||
this.memberReservedWords = resolveReserved(builder.memberReservedWords);
|
||||
}
|
||||
|
||||
private static ReservedWords resolveReserved(ReservedWords specific) {
|
||||
return specific != null ? specific : IDENTITY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder to create a ReservedWordSymbolProvider instance.
|
||||
*
|
||||
* @return Returns a new builder.
|
||||
*/
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Symbol toSymbol(Shape shape) {
|
||||
Symbol upstream = delegate.toSymbol(shape);
|
||||
return upstream.toBuilder()
|
||||
.name(nameReservedWords.escape(upstream.getName()))
|
||||
.namespace(namespaceReservedWords.escape(upstream.getNamespace()), upstream.getNamespaceDelimiter())
|
||||
.declarationFile(filenameReservedWords.escape(upstream.getDeclarationFile()))
|
||||
.definitionFile(filenameReservedWords.escape(upstream.getDefinitionFile()))
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toMemberName(Shape shape) {
|
||||
return convertWord("member", delegate.toMemberName(shape), memberReservedWords);
|
||||
}
|
||||
|
||||
private static String convertWord(String name, String result, ReservedWords reservedWords) {
|
||||
if (!reservedWords.isReserved(result)) {
|
||||
return result;
|
||||
}
|
||||
|
||||
String newResult = reservedWords.escape(result);
|
||||
LOGGER.warning(() -> String.format(
|
||||
"Reserved word: %s is a reserved word for a %s. Converting to %s",
|
||||
result, name, newResult));
|
||||
return newResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder to build a {@link ReservedWordSymbolProvider}.
|
||||
*/
|
||||
public static final class Builder {
|
||||
private SymbolProvider delegate;
|
||||
private ReservedWords filenameReservedWords;
|
||||
private ReservedWords namespaceReservedWords;
|
||||
private ReservedWords nameReservedWords;
|
||||
private ReservedWords memberReservedWords;
|
||||
|
||||
/**
|
||||
* Builds the provider.
|
||||
*
|
||||
* @return Returns the built provider.
|
||||
*/
|
||||
public SymbolProvider build() {
|
||||
return new ReservedWordSymbolProvider(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the <strong>required</strong> delegate symbol provider.
|
||||
*
|
||||
* @param delegate Symbol provider to delegate to.
|
||||
* @return Returns the builder
|
||||
*/
|
||||
public Builder symbolProvider(SymbolProvider delegate) {
|
||||
this.delegate = delegate;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the reserved word implementation for file names.
|
||||
*
|
||||
* <p>If not provided, file names are not passed through a reserved
|
||||
* words implementation after calling the delegate.
|
||||
*
|
||||
* @param filenameReservedWords Reserved word implementation for namespaces.
|
||||
* @return Returns the builder.
|
||||
*/
|
||||
public Builder filenameReservedWords(ReservedWords filenameReservedWords) {
|
||||
this.filenameReservedWords = filenameReservedWords;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the reserved word implementation for namespace names.
|
||||
*
|
||||
* <p>If not provided, namespace names are not passed through a reserved
|
||||
* words implementation after calling the delegate.
|
||||
*
|
||||
* @param namespaceReservedWords Reserved word implementation for namespaces.
|
||||
* @return Returns the builder.
|
||||
*/
|
||||
public Builder namespaceReservedWords(ReservedWords namespaceReservedWords) {
|
||||
this.namespaceReservedWords = namespaceReservedWords;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the reserved word implementation for names (structures names,
|
||||
* class names, etc.).
|
||||
*
|
||||
* <p>If not provided, names are not passed through a reserved words
|
||||
* implementation after calling the delegate.
|
||||
*
|
||||
* @param nameReservedWords Reserved word implementation for containers.
|
||||
* @return Returns the builder.
|
||||
*/
|
||||
public Builder nameReservedWords(ReservedWords nameReservedWords) {
|
||||
this.nameReservedWords = nameReservedWords;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the reserved word implementation for members.
|
||||
*
|
||||
* <p>If not provided, member names are not passed through a reserved
|
||||
* words implementation after calling the delegate.
|
||||
*
|
||||
* @param memberReservedWords Reserved word implementation for members.
|
||||
* @return Returns the builder.
|
||||
*/
|
||||
public Builder memberReservedWords(ReservedWords memberReservedWords) {
|
||||
this.memberReservedWords = memberReservedWords;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,112 @@
|
|||
/*
|
||||
* Copyright 2019 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.smithy.codegen.core;
|
||||
|
||||
/**
|
||||
* Determines what is reserved and escapes reserved words.
|
||||
*/
|
||||
public interface ReservedWords {
|
||||
/**
|
||||
* Escapes a reserved word.
|
||||
*
|
||||
* @param word Word to escape.
|
||||
* @return Returns the converted value.
|
||||
*/
|
||||
String escape(String word);
|
||||
|
||||
/**
|
||||
* Checks if the given word is reserved.
|
||||
*
|
||||
* @param word Word to check.
|
||||
* @return Returns true if the word is reserved.
|
||||
*/
|
||||
boolean isReserved(String word);
|
||||
|
||||
/**
|
||||
* Creates a reserved word implementation that does not modify words.
|
||||
*
|
||||
* <pre>
|
||||
* {@code
|
||||
* ReservedWords reserved = ReservedWords.identity();
|
||||
* reserved.isReserved("foo"); // always returns false for anything.
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* @return Returns the identity implementation.
|
||||
*/
|
||||
static ReservedWords identity() {
|
||||
return new ReservedWords() {
|
||||
@Override
|
||||
public String escape(String word) {
|
||||
return word;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isReserved(String word) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Composes multiple instance of {@code ReservedWords} into a
|
||||
* single implementation that delegates to them one after the
|
||||
* other.
|
||||
*
|
||||
* <p>Each reserved words implementation is invoked one after
|
||||
* the other until one of them returns true for
|
||||
* {@link ReservedWords#isReserved}.
|
||||
*
|
||||
* <pre>
|
||||
* {@code
|
||||
* ReservedWords a = MappedReservedWords.builder().put("void", "_void").build();
|
||||
* ReservedWords b = MappedReservedWords.builder().put("foo", "_foo").build();
|
||||
* ReservedWords composed = ReservedWords.compose(a, b);
|
||||
* String safeWordA = composed.escape("void");
|
||||
* String safeWordB = composed.escape("foo");
|
||||
* System.out.println(safeWordA + " " + safeWordB); // outputs "_void _foo"
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* @param delegates ReservedWords instances to delegate to.
|
||||
* @return Returns the created {@code ReservedWords} instance.
|
||||
*/
|
||||
static ReservedWords compose(ReservedWords... delegates) {
|
||||
return new ReservedWords() {
|
||||
@Override
|
||||
public String escape(String word) {
|
||||
for (ReservedWords reservedWords : delegates) {
|
||||
if (reservedWords.isReserved(word)) {
|
||||
return reservedWords.escape(word);
|
||||
}
|
||||
}
|
||||
|
||||
return word;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isReserved(String word) {
|
||||
for (ReservedWords reservedWords : delegates) {
|
||||
if (reservedWords.isReserved(word)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,392 @@
|
|||
/*
|
||||
* Copyright 2019 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.smithy.codegen.core;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@SuppressWarnings("checkstyle:HideUtilityClassConstructor")
|
||||
public final class StringUtils {
|
||||
/**
|
||||
* <p>Wraps a single line of text, identifying words by <code>' '</code>.</p>
|
||||
*
|
||||
* <p>New lines will be separated by the system property line separator.
|
||||
* Very long words, such as URLs will <i>not</i> be wrapped.</p>
|
||||
*
|
||||
* <p>Leading spaces on a new line are stripped.
|
||||
* Trailing spaces are not stripped.</p>
|
||||
*
|
||||
* <table border="1">
|
||||
* <caption>Examples</caption>
|
||||
* <tr>
|
||||
* <th>input</th>
|
||||
* <th>wrapLength</th>
|
||||
* <th>result</th>
|
||||
* </tr>
|
||||
* <tr>
|
||||
* <td>null</td>
|
||||
* <td>*</td>
|
||||
* <td>null</td>
|
||||
* </tr>
|
||||
* <tr>
|
||||
* <td>""</td>
|
||||
* <td>*</td>
|
||||
* <td>""</td>
|
||||
* </tr>
|
||||
* <tr>
|
||||
* <td>"Here is one line of text that is going to be wrapped after 20 columns."</td>
|
||||
* <td>20</td>
|
||||
* <td>"Here is one line of\ntext that is going\nto be wrapped after\n20 columns."</td>
|
||||
* </tr>
|
||||
* <tr>
|
||||
* <td>"Click here to jump to the commons website - http://commons.apache.org"</td>
|
||||
* <td>20</td>
|
||||
* <td>"Click here to jump\nto the commons\nwebsite -\nhttp://commons.apache.org"</td>
|
||||
* </tr>
|
||||
* <tr>
|
||||
* <td>"Click here, http://commons.apache.org, to jump to the commons website"</td>
|
||||
* <td>20</td>
|
||||
* <td>"Click here,\nhttp://commons.apache.org,\nto jump to the\ncommons website"</td>
|
||||
* </tr>
|
||||
* </table>
|
||||
*
|
||||
* (assuming that '\n' is the systems line separator)
|
||||
*
|
||||
* @param str the String to be word wrapped, may be null
|
||||
* @param wrapLength the column to wrap the words at, less than 1 is treated as 1
|
||||
* @return a line with newlines inserted, <code>null</code> if null input
|
||||
* @see <a href="https://github.com/apache/commons-text/blob/f0ae79e46e3923562168df9c03023587eafc4d69/src/main/java/org/apache/commons/text/WordUtils.java#L105">Source</a>
|
||||
*/
|
||||
public static String wrap(final String str, final int wrapLength) {
|
||||
return wrap(str, wrapLength, null, false, " ");
|
||||
}
|
||||
|
||||
// https://github.com/apache/commons-text/blob/f0ae79e46e3923562168df9c03023587eafc4d69/src/main/java/org/apache/commons/text/WordUtils.java#L283
|
||||
private static String wrap(final String str,
|
||||
int wrapLength,
|
||||
String newLineStr,
|
||||
final boolean wrapLongWords,
|
||||
String wrapOn
|
||||
) {
|
||||
if (str == null) {
|
||||
return null;
|
||||
}
|
||||
if (newLineStr == null) {
|
||||
newLineStr = System.lineSeparator();
|
||||
}
|
||||
if (wrapLength < 1) {
|
||||
wrapLength = 1;
|
||||
}
|
||||
if (wrapOn.isBlank()) {
|
||||
wrapOn = " ";
|
||||
}
|
||||
final Pattern patternToWrapOn = Pattern.compile(wrapOn);
|
||||
final int inputLineLength = str.length();
|
||||
int offset = 0;
|
||||
final StringBuilder wrappedLine = new StringBuilder(inputLineLength + 32);
|
||||
|
||||
while (offset < inputLineLength) {
|
||||
int spaceToWrapAt = -1;
|
||||
Matcher matcher = patternToWrapOn.matcher(str.substring(
|
||||
offset, Math.min((int) Math.min(Integer.MAX_VALUE, offset + wrapLength + 1L), inputLineLength)));
|
||||
if (matcher.find()) {
|
||||
if (matcher.start() == 0) {
|
||||
offset += matcher.end();
|
||||
continue;
|
||||
}
|
||||
spaceToWrapAt = matcher.start() + offset;
|
||||
}
|
||||
|
||||
// only last line without leading spaces is left
|
||||
if (inputLineLength - offset <= wrapLength) {
|
||||
break;
|
||||
}
|
||||
|
||||
while (matcher.find()) {
|
||||
spaceToWrapAt = matcher.start() + offset;
|
||||
}
|
||||
|
||||
if (spaceToWrapAt >= offset) {
|
||||
// normal case
|
||||
wrappedLine.append(str, offset, spaceToWrapAt);
|
||||
wrappedLine.append(newLineStr);
|
||||
offset = spaceToWrapAt + 1;
|
||||
|
||||
} else {
|
||||
// really long word or URL
|
||||
if (wrapLongWords) {
|
||||
// wrap really long word one line at a time
|
||||
wrappedLine.append(str, offset, wrapLength + offset);
|
||||
wrappedLine.append(newLineStr);
|
||||
offset += wrapLength;
|
||||
} else {
|
||||
// do not wrap really long word, just extend beyond limit
|
||||
matcher = patternToWrapOn.matcher(str.substring(offset + wrapLength));
|
||||
if (matcher.find()) {
|
||||
spaceToWrapAt = matcher.start() + offset + wrapLength;
|
||||
}
|
||||
|
||||
if (spaceToWrapAt >= 0) {
|
||||
wrappedLine.append(str, offset, spaceToWrapAt);
|
||||
wrappedLine.append(newLineStr);
|
||||
offset = spaceToWrapAt + 1;
|
||||
} else {
|
||||
wrappedLine.append(str, offset, str.length());
|
||||
offset = inputLineLength;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Whatever is left in line is short enough to just pass through
|
||||
wrappedLine.append(str, offset, str.length());
|
||||
|
||||
return wrappedLine.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Converts all the delimiter separated words in a String into PascalCase,
|
||||
* that is each word is made up of a titlecase character and then a series of
|
||||
* lowercase characters.</p>
|
||||
*
|
||||
* <p>PacalCase is just like CamelCase, except the first character is an
|
||||
* uppercase letter.
|
||||
*
|
||||
* @param str the String to be converted to PascalCase, may be null
|
||||
* @return camelCase of String, <code>null</code> if null String input
|
||||
*/
|
||||
public static String snakeToPascalCase(String str) {
|
||||
return toCamelCase(str, true, '_');
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Converts all the delimiter separated words in a String into camelCase,
|
||||
* that is each word is made up of a titlecase character and then a series of
|
||||
* lowercase characters.
|
||||
*
|
||||
* <p>The first character is always converted to lowercase.</p>
|
||||
*
|
||||
* @param str the String to be converted to camelCase, may be null
|
||||
* @return camelCase of String, <code>null</code> if null String input
|
||||
*/
|
||||
public static String snakeToCamelCase(String str) {
|
||||
return toCamelCase(str, false, '_');
|
||||
}
|
||||
|
||||
// https://github.com/apache/commons-text/blob/f0ae79e46e3923562168df9c03023587eafc4d69/src/main/java/org/apache/commons/text/CaseUtils.java#L76
|
||||
private static String toCamelCase(String str, final boolean capitalizeFirstLetter, final char... delimiters) {
|
||||
// Begin modification
|
||||
if (str == null || str.isEmpty()) {
|
||||
return str;
|
||||
}
|
||||
str = str.toLowerCase(Locale.US);
|
||||
// End modification
|
||||
final int strLen = str.length();
|
||||
final int[] newCodePoints = new int[strLen];
|
||||
int outOffset = 0;
|
||||
final Set<Integer> delimiterSet = generateDelimiterSet(delimiters);
|
||||
boolean capitalizeNext = false;
|
||||
if (capitalizeFirstLetter) {
|
||||
capitalizeNext = true;
|
||||
}
|
||||
for (int index = 0; index < strLen;) {
|
||||
final int codePoint = str.codePointAt(index);
|
||||
|
||||
if (delimiterSet.contains(codePoint)) {
|
||||
capitalizeNext = true;
|
||||
if (outOffset == 0) {
|
||||
capitalizeNext = false;
|
||||
}
|
||||
index += Character.charCount(codePoint);
|
||||
} else if (capitalizeNext || outOffset == 0 && capitalizeFirstLetter) {
|
||||
final int titleCaseCodePoint = Character.toTitleCase(codePoint);
|
||||
newCodePoints[outOffset++] = titleCaseCodePoint;
|
||||
index += Character.charCount(titleCaseCodePoint);
|
||||
capitalizeNext = false;
|
||||
} else {
|
||||
newCodePoints[outOffset++] = codePoint;
|
||||
index += Character.charCount(codePoint);
|
||||
}
|
||||
}
|
||||
if (outOffset != 0) {
|
||||
return new String(newCodePoints, 0, outOffset);
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
// https://github.com/apache/commons-text/blob/f0ae79e46e3923562168df9c03023587eafc4d69/src/main/java/org/apache/commons/text/WordUtils.java#L887
|
||||
private static Set<Integer> generateDelimiterSet(final char[] delimiters) {
|
||||
final Set<Integer> delimiterHashSet = new HashSet<>();
|
||||
delimiterHashSet.add(Character.codePointAt(new char[]{' '}, 0));
|
||||
if (delimiters == null || delimiters.length == 0) {
|
||||
return delimiterHashSet;
|
||||
}
|
||||
|
||||
for (int index = 0; index < delimiters.length; index++) {
|
||||
delimiterHashSet.add(Character.codePointAt(delimiters, index));
|
||||
}
|
||||
return delimiterHashSet;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a given word to snake_case with all lowercase letters.
|
||||
*
|
||||
* <p>This method was based on Elephant Bird's underscore method.
|
||||
* "-", " ", "\n", "\t", "\r" are replaced with "_".
|
||||
*
|
||||
* <p>Note: this method does not sanitize the string for use as a snake_case
|
||||
* variable in any specific programming language.
|
||||
*
|
||||
* @param word The word to convert.
|
||||
* @return The underscored version of the word
|
||||
* @see <a href="https://github.com/twitter/elephant-bird/blob/master/core/src/main/java/com/twitter/elephantbird/util/Strings.java">Elephant bird</a>
|
||||
* @see <a href="https://github.com/twitter/elephant-bird/blob/master/LICENSE">Elephant bird license</a>
|
||||
*/
|
||||
public static String toSnakeCase(String word) {
|
||||
String firstPattern = "([A-Z]+)([A-Z][a-z])";
|
||||
String secondPattern = "([a-z\\d])([A-Z])";
|
||||
String replacementPattern = "$1_$2";
|
||||
// Replace capital letter with _ plus lowercase letter.
|
||||
word = word.replaceAll(firstPattern, replacementPattern);
|
||||
word = word.replaceAll(secondPattern, replacementPattern);
|
||||
word = word.replaceAll("(\\s|-)", "_");
|
||||
// Begin modification
|
||||
word = word.toLowerCase(Locale.US);
|
||||
// End modification
|
||||
return word;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Capitalizes a String changing the first character to title case as
|
||||
* per {@link Character#toTitleCase(int)}. No other characters are changed.</p>
|
||||
*
|
||||
* <pre>
|
||||
* StringUtils.capitalize(null) = null
|
||||
* StringUtils.capitalize("") = ""
|
||||
* StringUtils.capitalize("cat") = "Cat"
|
||||
* StringUtils.capitalize("cAt") = "CAt"
|
||||
* StringUtils.capitalize("'cat'") = "'cat'"
|
||||
* </pre>
|
||||
*
|
||||
* @param str the String to capitalize, may be null
|
||||
* @return the capitalized String, {@code null} if null String input
|
||||
* @see <a href="https://github.com/apache/commons-lang/blob/c4d0dbcb56b8980b1b3b7c85d00ad6540788c08e/src/main/java/org/apache/commons/lang3/StringUtils.java#L6803">Source</a>
|
||||
* @see #uncapitalize(String)
|
||||
*/
|
||||
public static String capitalize(final String str) {
|
||||
if (str == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int strLen = str.length();
|
||||
if (strLen == 0) {
|
||||
return str;
|
||||
}
|
||||
|
||||
final int firstCodepoint = str.codePointAt(0);
|
||||
final int newCodePoint = Character.toTitleCase(firstCodepoint);
|
||||
if (firstCodepoint == newCodePoint) {
|
||||
// already capitalized
|
||||
return str;
|
||||
}
|
||||
|
||||
final int[] newCodePoints = new int[strLen]; // cannot be longer than the char array
|
||||
int outOffset = 0;
|
||||
newCodePoints[outOffset++] = newCodePoint; // copy the first codepoint
|
||||
for (int inOffset = Character.charCount(firstCodepoint); inOffset < strLen;) {
|
||||
final int codepoint = str.codePointAt(inOffset);
|
||||
newCodePoints[outOffset++] = codepoint; // copy the remaining ones
|
||||
inOffset += Character.charCount(codepoint);
|
||||
}
|
||||
return new String(newCodePoints, 0, outOffset);
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Uncapitalizes a String, changing the first character to lower case as
|
||||
* per {@link Character#toLowerCase(int)}. No other characters are changed.</p>
|
||||
*
|
||||
* <pre>
|
||||
* StringUtils.uncapitalize(null) = null
|
||||
* StringUtils.uncapitalize("") = ""
|
||||
* StringUtils.uncapitalize("cat") = "cat"
|
||||
* StringUtils.uncapitalize("Cat") = "cat"
|
||||
* StringUtils.uncapitalize("CAT") = "cAT"
|
||||
* </pre>
|
||||
*
|
||||
* @param str the String to uncapitalize, may be null
|
||||
* @return the uncapitalized String, {@code null} if null String input
|
||||
* @see <a href="https://github.com/apache/commons-lang/blob/c4d0dbcb56b8980b1b3b7c85d00ad6540788c08e/src/main/java/org/apache/commons/lang3/StringUtils.java#L6848">Source</a>
|
||||
*/
|
||||
public static String uncapitalize(final String str) {
|
||||
if (str == null) {
|
||||
return str;
|
||||
}
|
||||
int strLen = str.length();
|
||||
if (strLen == 0) {
|
||||
return str;
|
||||
}
|
||||
|
||||
final int firstCodepoint = str.codePointAt(0);
|
||||
final int newCodePoint = Character.toLowerCase(firstCodepoint);
|
||||
if (firstCodepoint == newCodePoint) {
|
||||
// already capitalized
|
||||
return str;
|
||||
}
|
||||
|
||||
final int[] newCodePoints = new int[strLen]; // cannot be longer than the char array
|
||||
int outOffset = 0;
|
||||
newCodePoints[outOffset++] = newCodePoint; // copy the first codepoint
|
||||
for (int inOffset = Character.charCount(firstCodepoint); inOffset < strLen;) {
|
||||
final int codepoint = str.codePointAt(inOffset);
|
||||
newCodePoints[outOffset++] = codepoint; // copy the remaining ones
|
||||
inOffset += Character.charCount(codepoint);
|
||||
}
|
||||
return new String(newCodePoints, 0, outOffset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes a given value to convert double quotes (") to (\").
|
||||
*
|
||||
* <p>Any backslash (\) is converted to double blackslashes (\\).
|
||||
*
|
||||
* @param value Value to escape.
|
||||
* @return Returns the escaped value.
|
||||
*/
|
||||
public static String escapeDoubleQuote(String value) {
|
||||
return escapeDelimited(value, "\"");
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes a given value to convert single quotes (') to (\').
|
||||
*
|
||||
* <p>Any backslash (\) is converted to double blackslashes (\\).
|
||||
*
|
||||
* @param value Value to escape.
|
||||
* @return Returns the escaped value.
|
||||
*/
|
||||
public static String escapeSingleQuote(String value) {
|
||||
return escapeDelimited(value, "'");
|
||||
}
|
||||
|
||||
private static String escapeDelimited(String value, String delimiter) {
|
||||
value = value.replace("\\", "\\\\");
|
||||
value = value.replace(delimiter, "\\" + delimiter);
|
||||
return value;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,389 @@
|
|||
/*
|
||||
* Copyright 2019 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.smithy.codegen.core;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import software.amazon.smithy.model.SmithyBuilder;
|
||||
import software.amazon.smithy.model.ToSmithyBuilder;
|
||||
|
||||
/**
|
||||
* A "symbol" is created by a {@link SymbolProvider}, and represents the
|
||||
* qualified name of a type in a target programming language.
|
||||
*
|
||||
* <p>A symbol contains an optional namespace, optional namespace
|
||||
* delimiter, name, a map of additional properties, a declaration file
|
||||
* the determines where the symbol is declared, and a definition file that
|
||||
* determines where a symbol is defined.
|
||||
*
|
||||
* <p>A namespace can be used when the target language supports namespaces.
|
||||
* The provided namespace can be in whatever format is useful to the target
|
||||
* language. A namespace delimiter is injected between the namespace and the
|
||||
* name when creating the fully-qualified symbol name. The "name" is the
|
||||
* unqualified name of the symbol (e.g., "str", or "MyShape").
|
||||
*
|
||||
* <p>Additional properties can be included when it's useful to provide
|
||||
* more information about a symbol. For example, it might be useful to
|
||||
* identify the name of a dependency that needs to be pulled in through a
|
||||
* package manager when a symbol is used.
|
||||
*
|
||||
* <p>The following example shows how a Java type could be made into a symbol:
|
||||
*
|
||||
* <pre>
|
||||
* {@code
|
||||
* Class<Symbol> klass = Symbol.class;
|
||||
* Symbol symbol = Symbol.builder()
|
||||
* .namespace(klass.getPackage().toString(), ".")
|
||||
* .name(klass.getSimpleName())
|
||||
* .build();
|
||||
* System.out.println(symbol);
|
||||
* // ^ outputs "software.amazon.smithy.codegen.Symbol"
|
||||
* System.out.println(symbol.relativize("software.amazon.smithy.codegen");
|
||||
* // ^ outputs "Symbol"
|
||||
* }
|
||||
* </pre>
|
||||
*/
|
||||
public final class Symbol implements ToSmithyBuilder<Symbol> {
|
||||
private final String namespace;
|
||||
private final String namespaceDelimiter;
|
||||
private final String name;
|
||||
private final String definitionFile;
|
||||
private final String declarationFile;
|
||||
private final Map<String, Object> properties;
|
||||
|
||||
private Symbol(Builder builder) {
|
||||
this.namespace = builder.namespace;
|
||||
this.namespaceDelimiter = builder.namespaceDelimiter;
|
||||
this.name = builder.name;
|
||||
this.declarationFile = builder.declarationFile;
|
||||
this.definitionFile = !builder.definitionFile.isEmpty() ? builder.definitionFile : declarationFile;
|
||||
this.properties = Map.copyOf(builder.properties);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new Symbol builder.
|
||||
*
|
||||
* @return Returns the created symbol builder.
|
||||
*/
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides the namespace of the symbol or "" if empty.
|
||||
*
|
||||
* @return Returns the optional namespace.
|
||||
*/
|
||||
public String getNamespace() {
|
||||
return namespace;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides the namespace delimiter of the symbol or "" if empty.
|
||||
*
|
||||
* <p>This delimiter is injected between the namespace and name
|
||||
* when creating the full name.
|
||||
*
|
||||
* @return Returns the optional namespace.
|
||||
*/
|
||||
public String getNamespaceDelimiter() {
|
||||
return namespaceDelimiter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the unqualified name of the symbol, that is, a name with
|
||||
* namespace.
|
||||
*
|
||||
* @return Returns the name of the symbol.
|
||||
*/
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the location/filename in which the symbol is declared.
|
||||
*
|
||||
* <p>Code generators should write the generated code for this
|
||||
* symbol's declaration in a file with the same name that is
|
||||
* returned from this method. Not all languages separate a symbol's
|
||||
* definition from its declaration. This method is useful for things
|
||||
* like like C and C++ header files.
|
||||
*
|
||||
* <p>This method returns an empty string if no value was provided
|
||||
* in the builder.
|
||||
*
|
||||
* @return The name of the file the symbol is declared.
|
||||
*/
|
||||
public String getDeclarationFile() {
|
||||
return declarationFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the location/filename in which the symbol is defined.
|
||||
*
|
||||
* <p>Code generators should write the generated code for a
|
||||
* symbol in a file with the same name that is returned from this
|
||||
* method.
|
||||
*
|
||||
* <p>This method returns an empty string if no value was provided
|
||||
* for either the declaration file or the definition file.
|
||||
*
|
||||
* @return The name of the file the symbol is defined.
|
||||
*/
|
||||
public String getDefinitionFile() {
|
||||
return definitionFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the additional properties of the symbol.
|
||||
*
|
||||
* @return Returns a map of additional property strings.
|
||||
*/
|
||||
public Map<String, Object> getProperties() {
|
||||
return properties;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a specific property if present.
|
||||
*
|
||||
* @param name Property to retrieve.
|
||||
* @return Returns the optionally found property.
|
||||
*/
|
||||
public Optional<Object> getProperty(String name) {
|
||||
return Optional.ofNullable(properties.get(name));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an additional property of a specific type.
|
||||
*
|
||||
* @param name Name of the property to get.
|
||||
* @param type Type of value to expect.
|
||||
* @param <T> Type of value to expect.
|
||||
* @return Returns a map of additional property strings.
|
||||
* @throws IllegalArgumentException if the value is not of the given type.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> Optional<T> getProperty(String name, Class<T> type) {
|
||||
return getProperty(name)
|
||||
.map(value -> {
|
||||
if (!type.isInstance(value)) {
|
||||
throw new IllegalArgumentException(String.format(
|
||||
"Symbol property `%s` of `%s` is not an instance of `%s`",
|
||||
name, this, type.getName()));
|
||||
}
|
||||
return (T) value;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a specific additional property or throws if missing.
|
||||
*
|
||||
* @param name Property to retrieve.
|
||||
* @return Returns the found property.
|
||||
* @throws IllegalArgumentException if the property is not present.
|
||||
*/
|
||||
public Object expectProperty(String name) {
|
||||
return getProperty(name).orElseThrow(() -> new IllegalArgumentException(String.format(
|
||||
"Property `%s` is not part of Symbol, `%s`", name, this)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a specific additional property or throws if missing or if the
|
||||
* property is not an instance of the given type.
|
||||
*
|
||||
* @param name Property to retrieve.
|
||||
* @param type Type of value to expect.
|
||||
* @param <T> Type of value to expect.
|
||||
* @return Returns the found property.
|
||||
* @throws IllegalArgumentException if the property is not present.
|
||||
* @throws IllegalArgumentException if the value is not of the given type.
|
||||
*/
|
||||
public <T> T expectProperty(String name, Class<T> type) {
|
||||
return getProperty(name, type).orElseThrow(() -> new IllegalArgumentException(String.format(
|
||||
"Property `%s` is not part of Symbol, `%s`", name, this)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the full name of the symbol.
|
||||
*
|
||||
* <p>The full name is the concatenation of the namespace,
|
||||
* the namespace delimiter, and the name.
|
||||
*
|
||||
* @return Returns the fully qualified name of the symbol.
|
||||
*/
|
||||
public String getFullName() {
|
||||
return toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a relativized Symbol for the given namespace.
|
||||
*
|
||||
* <p>If this symbol is in the same namespace as the provided namespace,
|
||||
* then only the symbol name is returned. Otherwise, the fully-qualified
|
||||
* symbol is returned.
|
||||
*
|
||||
* @param namespace Namespace to relativize against.
|
||||
* @return Returns the relativized symbol.
|
||||
*/
|
||||
public String relativize(String namespace) {
|
||||
return this.namespace.equals(namespace) ? name : toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder toBuilder() {
|
||||
Builder builder = new Builder();
|
||||
return builder.namespace(namespace, namespaceDelimiter)
|
||||
.name(name)
|
||||
.properties(properties)
|
||||
.definitionFile(definitionFile)
|
||||
.declarationFile(declarationFile);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return namespace.isEmpty() ? name : namespace + namespaceDelimiter + name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (!(o instanceof Symbol)) {
|
||||
return false;
|
||||
}
|
||||
Symbol symbol = (Symbol) o;
|
||||
return Objects.equals(namespace, symbol.namespace)
|
||||
&& Objects.equals(namespaceDelimiter, symbol.namespaceDelimiter)
|
||||
&& Objects.equals(name, symbol.name)
|
||||
&& Objects.equals(properties, symbol.properties)
|
||||
&& Objects.equals(declarationFile, symbol.declarationFile)
|
||||
&& Objects.equals(definitionFile, symbol.definitionFile);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(namespace, namespaceDelimiter, name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a Symbol.
|
||||
*/
|
||||
public static final class Builder implements SmithyBuilder<Symbol> {
|
||||
private String name;
|
||||
private String namespace = "";
|
||||
private String namespaceDelimiter = "";
|
||||
private String definitionFile = "";
|
||||
private String declarationFile = "";
|
||||
private Map<String, Object> properties = new HashMap<>();
|
||||
|
||||
@Override
|
||||
public Symbol build() {
|
||||
return new Symbol(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the unqualified name of the symbol.
|
||||
*
|
||||
* @param name Name to set.
|
||||
* @return Returns the builder.
|
||||
*/
|
||||
public Builder name(String name) {
|
||||
this.name = name;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the namespace and namespace delimiter of the symbol.
|
||||
*
|
||||
* @param namespace Namespace to set.
|
||||
* @param namespaceDelimiter Namespace delimiter to set.
|
||||
* @return Returns the builder.
|
||||
*/
|
||||
public Builder namespace(String namespace, String namespaceDelimiter) {
|
||||
this.namespace = namespace == null ? "" : namespace;
|
||||
this.namespaceDelimiter = namespaceDelimiter == null ? "" : namespaceDelimiter;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a specific custom property.
|
||||
*
|
||||
* @param key Key to set.
|
||||
* @param value Value to set.
|
||||
* @return Returns the builder.
|
||||
*/
|
||||
public Builder putProperty(String key, Object value) {
|
||||
properties.put(key, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a specific custom property.
|
||||
*
|
||||
* @param key Key to remove.
|
||||
* @return Returns the builder.
|
||||
*/
|
||||
public Builder removeProperty(String key) {
|
||||
properties.remove(key);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces all of the custom properties.
|
||||
*
|
||||
* @param properties Custom properties to replace with.
|
||||
* @return Returns the builder.
|
||||
*/
|
||||
public Builder properties(Map<String, Object> properties) {
|
||||
this.properties.clear();
|
||||
this.properties.putAll(properties);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the filename of where this symbol is defined.
|
||||
*
|
||||
* <p>This value defaults to the value provided for {@link #declarationFile}
|
||||
* if not present. One of a {@code definitionFile} or a {@code declarationFile}
|
||||
* must be provided for every Symbol.
|
||||
*
|
||||
* @param definitionFile Filename of where the symbol is defined.
|
||||
* @return Returns the builder.
|
||||
*/
|
||||
public Builder definitionFile(String definitionFile) {
|
||||
this.definitionFile = definitionFile;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the filename of where this symbol is declared.
|
||||
*
|
||||
* <p>This value defaults to the value provided for {@link #definitionFile}
|
||||
* if not present. One of a {@code definitionFile} or a {@code declarationFile}
|
||||
* must be provided for every Symbol.
|
||||
*
|
||||
* @param declarationFile Filename of where the symbol is declared.
|
||||
* @return Returns the builder.
|
||||
*/
|
||||
public Builder declarationFile(String declarationFile) {
|
||||
this.declarationFile = declarationFile;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
* Copyright 2019 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.smithy.codegen.core;
|
||||
|
||||
import software.amazon.smithy.model.shapes.Shape;
|
||||
|
||||
/**
|
||||
* Provides {@link Symbol} objects for shapes.
|
||||
*
|
||||
* <p>Implementations of this interface are used to determine what file a
|
||||
* shape is defined within, what namespace/module/package a shape is defined
|
||||
* in, converts shapes to class/struct/interface names, converts shapes to
|
||||
* function/method names, converts shapes to member/property names, and
|
||||
* creates variable names from strings.
|
||||
*
|
||||
* <p>Method names MUST account for reserved words and the syntax constraints
|
||||
* of the target language. This typically means that implementations will
|
||||
* leverage one or more internal instances of {@link ReservedWords}.
|
||||
*/
|
||||
public interface SymbolProvider {
|
||||
/**
|
||||
* Gets the symbol to define for the given shape.
|
||||
*
|
||||
* <p>A "symbol" represents the qualified name of a type in a target
|
||||
* programming language.
|
||||
*
|
||||
* <ul>
|
||||
* <li>When given a structure, union, resource, or service shape,
|
||||
* this method should provide the namespace and name of the type to
|
||||
* generate.</li>
|
||||
* <li>When given a simple type like a string, number, or timestamp,
|
||||
* this method should return the language-specific type of the
|
||||
* shape.</li>
|
||||
* <li>When given a member shape, this method should return the
|
||||
* language specific type to use as the target of the member.</li>
|
||||
* <li>When given a list, set, or map, this method should return the
|
||||
* language specific type to use for the shape (e.g., a map shape for
|
||||
* a Python code generator might return "dict".</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param shape Shape to get the class name of.
|
||||
* @return Returns the generated class name.
|
||||
*/
|
||||
Symbol toSymbol(Shape shape);
|
||||
|
||||
/**
|
||||
* Converts a shape to a member/property name of a containing
|
||||
* data structure.
|
||||
*
|
||||
* <p>The default implementation will return the member name of
|
||||
* the provided shape ID if the shape ID contains a member. If no
|
||||
* member is present, the name of the shape with the first letter
|
||||
* converted to lowercase is returned. The default implementation may
|
||||
* not work for all use cases and should be overridden as needed.
|
||||
*
|
||||
* @param shape Shape to convert.
|
||||
* @return Returns the converted member name.
|
||||
*/
|
||||
default String toMemberName(Shape shape) {
|
||||
return shape.getId().getMember().orElseGet(() -> StringUtils.uncapitalize(shape.getId().getName()));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* Copyright 2019 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.smithy.codegen.core;
|
||||
|
||||
import java.io.StringWriter;
|
||||
import java.io.Writer;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Abstraction to renderFile templates using a data model.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface TemplateEngine {
|
||||
/**
|
||||
* Writes a template to the given writer.
|
||||
*
|
||||
* @param templatePath Loaded template to render.
|
||||
* @param out Writer to write to.
|
||||
* @param dataModel Data model to apply to the template.
|
||||
*/
|
||||
void write(String templatePath, Writer out, Map<String, Object> dataModel);
|
||||
|
||||
/**
|
||||
* Renders a template loaded from the given path and returns the result.
|
||||
*
|
||||
* @param templatePath Path to a template to load.
|
||||
* @param dataModel Data model to apply to the template.
|
||||
* @return Returns the rendered text of the template.
|
||||
*/
|
||||
default String render(String templatePath, Map<String, Object> dataModel) {
|
||||
StringWriter writer = new StringWriter();
|
||||
write(templatePath, writer, dataModel);
|
||||
return writer.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a template loaded from the given path and returns the result.
|
||||
*
|
||||
* @param templatePath Path to a template to load.
|
||||
* @return Returns the rendered text of the template.
|
||||
*/
|
||||
default String render(String templatePath) {
|
||||
return render(templatePath, Collections.emptyMap());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
//
|
||||
// Copyright 2019 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.
|
||||
//
|
||||
|
||||
--add-reads
|
||||
software.amazon.smithy.codegen.core=org.hamcrest
|
|
@ -0,0 +1,231 @@
|
|||
/*
|
||||
* Copyright 2019 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.smithy.codegen.core;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
public class CodeWriterTest {
|
||||
@Test
|
||||
public void limitsBlankLines() {
|
||||
CodeWriter writer = new CodeWriter().trimBlankLines().trimTrailingSpaces();
|
||||
writer.write("if (%s == \"foo\") {\n\n\n\n", "BAZ")
|
||||
.indent()
|
||||
.write("print(%s)", "BAZ")
|
||||
.dedent()
|
||||
.write("}");
|
||||
|
||||
assertThat(writer.toString(), equalTo("if (BAZ == \"foo\") {\n\n print(BAZ)\n}\n"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doesNotLimitBlankLines() {
|
||||
CodeWriter writer = new CodeWriter().trimTrailingSpaces();
|
||||
writer.write("if (%s == \"foo\") {\n\n\n\n", "BAZ")
|
||||
.indent()
|
||||
.write("print(%s)", "BAZ")
|
||||
.dedent()
|
||||
.write("}");
|
||||
|
||||
assertThat(writer.toString(), equalTo("if (BAZ == \"foo\") {\n\n\n\n\n print(BAZ)\n}\n"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resetsBlankLineCounterWhenContentAppears() {
|
||||
CodeWriter writer = new CodeWriter().trimBlankLines().trimTrailingSpaces();
|
||||
writer.write(".\n.\n.\n\n.\n\n\n.");
|
||||
|
||||
assertThat(writer.toString(), equalTo(".\n.\n.\n\n.\n\n.\n"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void trimsTrailingSpaces() {
|
||||
CodeWriter writer = new CodeWriter().trimBlankLines().trimTrailingSpaces();
|
||||
writer.write("hello ").writeInline("there ");
|
||||
|
||||
assertThat(writer.toString(), equalTo("hello\nthere"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void trimsSpacesAndBlankLines() {
|
||||
CodeWriter writer = new CodeWriter().trimTrailingSpaces().trimBlankLines();
|
||||
writer.write("hello\n\n\nthere, ").writeInline("bud");
|
||||
|
||||
assertThat(writer.toString(), equalTo("hello\n\nthere,\nbud"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void canInsertTrailingNewlines() {
|
||||
CodeWriter writer = new CodeWriter().trimTrailingSpaces().trimBlankLines();
|
||||
writer.write("hello\n\n\nthere, ").writeInline("bud");
|
||||
|
||||
assertThat(writer.toString(), equalTo("hello\n\nthere,\nbud"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void canWriteTextWithNewlinePrefixAndBlankLineTrimming() {
|
||||
CodeWriter writer = CodeWriter.createDefault();
|
||||
writer
|
||||
.write("/**")
|
||||
.setNewlinePrefix(" * ")
|
||||
.write("This is some docs.")
|
||||
.write("And more docs.\n\n\n")
|
||||
.write("Foo.")
|
||||
.setNewlinePrefix("")
|
||||
.write(" */");
|
||||
|
||||
/* Becomes:
|
||||
*
|
||||
*
|
||||
* /**
|
||||
* * This is some docs.
|
||||
* * And more docs.
|
||||
* *
|
||||
* * Foo.
|
||||
* *\/
|
||||
*
|
||||
* ^ Minus this character.
|
||||
*/
|
||||
assertThat(
|
||||
writer.toString(),
|
||||
equalTo("/**\n * This is some docs.\n * And more docs.\n * \n * Foo.\n */\n"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void handlesIndentation() {
|
||||
CodeWriter writer = CodeWriter.createDefault();
|
||||
writer
|
||||
.write("Hi")
|
||||
.indent()
|
||||
.write("A")
|
||||
.indent()
|
||||
.write("B")
|
||||
.dedent()
|
||||
.write("C")
|
||||
.dedent()
|
||||
.write("Fin.");
|
||||
|
||||
assertThat(
|
||||
writer.toString(),
|
||||
equalTo("Hi\n A\n B\n C\nFin.\n"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cannotDedentPastRoot() {
|
||||
Assertions.assertThrows(CodegenException.class, () -> {
|
||||
CodeWriter writer = CodeWriter.createDefault();
|
||||
writer.dedent(10);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void canDedentToRoot() {
|
||||
CodeWriter writer = CodeWriter.createDefault().indent(10).dedent(-1).write("Hi");
|
||||
|
||||
assertThat(writer.toString(), equalTo("Hi\n"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void canIndentDocBlocks() {
|
||||
CodeWriter writer = CodeWriter.createDefault();
|
||||
writer.indent()
|
||||
.write("/**")
|
||||
.setNewlinePrefix(" * ")
|
||||
.write("This is some docs.")
|
||||
.write("And more docs.\n\n\n")
|
||||
.write("Foo.")
|
||||
.setNewlinePrefix("")
|
||||
.write(" */")
|
||||
.dedent();
|
||||
|
||||
/* Becomes:
|
||||
*
|
||||
*
|
||||
* /**
|
||||
* * This is some docs.
|
||||
* * And more docs.
|
||||
* *
|
||||
* * Foo.
|
||||
* *\/
|
||||
*
|
||||
* ^ Minus this character.
|
||||
*/
|
||||
assertThat(
|
||||
writer.toString(),
|
||||
equalTo(" /**\n * This is some docs.\n * And more docs.\n * \n * Foo.\n */\n"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void injectsNewlineWhenNeeded() {
|
||||
CodeWriter writer = CodeWriter.createDefault();
|
||||
writer.writeInline("foo");
|
||||
|
||||
assertThat(writer.toString(), equalTo("foo\n"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doesNotInjectNewlineWhenNotNeeded() {
|
||||
CodeWriter writer = CodeWriter.createDefault();
|
||||
writer.write("foo");
|
||||
|
||||
assertThat(writer.toString(), equalTo("foo\n"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doesNotInjectNewlineWhenNotNeededThroughInline() {
|
||||
CodeWriter writer = CodeWriter.createDefault();
|
||||
writer.writeInline("foo\n");
|
||||
|
||||
assertThat(writer.toString(), equalTo("foo\n"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cannotPopMoreStatesThanExist() {
|
||||
Assertions.assertThrows(CodegenException.class, () -> {
|
||||
CodeWriter.createDefault()
|
||||
.pushState()
|
||||
.popState()
|
||||
.popState();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void canPushAndPopState() {
|
||||
CodeWriter writer = CodeWriter.createDefault();
|
||||
writer
|
||||
.setNewlinePrefix("0: ")
|
||||
.write("Hi")
|
||||
.pushState()
|
||||
.indent(2)
|
||||
.setNewlinePrefix("2: ")
|
||||
.write("there,")
|
||||
.pushState()
|
||||
.indent(2)
|
||||
.setNewlinePrefix("4: ")
|
||||
.write("guy")
|
||||
.popState()
|
||||
.write("Foo")
|
||||
.popState()
|
||||
.write("baz");
|
||||
|
||||
assertThat(
|
||||
writer.toString(),
|
||||
equalTo("0: Hi\n 2: there,\n 4: guy\n 2: Foo\n0: baz\n"));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* Copyright 2019 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.smithy.codegen.core;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
|
||||
import java.io.Writer;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
public class DefaultDataTemplateEngineTest {
|
||||
@Test
|
||||
public void injectsDefaultValues() {
|
||||
DefaultDataTemplateEngine engine = DefaultDataTemplateEngine.builder()
|
||||
.put("foo", "baz")
|
||||
.delegate(new Custom())
|
||||
.build();
|
||||
|
||||
assertThat(engine.render("foo"), equalTo("baz"));
|
||||
assertThat(engine.render("notThere"), equalTo("null"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void canOverrideDefaults() {
|
||||
DefaultDataTemplateEngine engine = DefaultDataTemplateEngine.builder()
|
||||
.put("foo", "baz")
|
||||
.delegate(new Custom())
|
||||
.build();
|
||||
Map<String, Object> dataModel = new HashMap<>();
|
||||
dataModel.put("foo", "qux");
|
||||
|
||||
assertThat(engine.render("foo", dataModel), equalTo("qux"));
|
||||
assertThat(engine.render("notThere", dataModel), equalTo("null"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void canPassInEmptyCustomValues() {
|
||||
DefaultDataTemplateEngine engine = DefaultDataTemplateEngine.builder().delegate(new Custom()).build();
|
||||
Map<String, Object> dataModel = new HashMap<>();
|
||||
dataModel.put("foo", "qux");
|
||||
|
||||
assertThat(engine.render("foo", dataModel), equalTo("qux"));
|
||||
assertThat(engine.render("notThere", dataModel), equalTo("null"));
|
||||
}
|
||||
|
||||
private static final class Custom implements TemplateEngine {
|
||||
@Override
|
||||
public void write(String templatePath, Writer out, Map<String, Object> dataModel) {
|
||||
try {
|
||||
out.write(String.valueOf(dataModel.get(templatePath)));
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* Copyright 2019 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.smithy.codegen.core;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
public class MappedReservedWordsTest {
|
||||
@Test
|
||||
public void mapsOverReservedWords() {
|
||||
ReservedWords reservedWords = MappedReservedWords.builder()
|
||||
.put("foo", "Foo")
|
||||
.put("baz", "Baz")
|
||||
.build();
|
||||
|
||||
assertThat(reservedWords.isReserved("foo"), is(true));
|
||||
assertThat(reservedWords.isReserved("Foo"), is(false));
|
||||
assertThat(reservedWords.isReserved("baz"), is(true));
|
||||
assertThat(reservedWords.isReserved("Baz"), is(false));
|
||||
assertThat(reservedWords.isReserved("qux"), is(false));
|
||||
|
||||
assertThat(reservedWords.escape("foo"), equalTo("Foo"));
|
||||
assertThat(reservedWords.escape("Foo"), equalTo("Foo"));
|
||||
assertThat(reservedWords.escape("baz"), equalTo("Baz"));
|
||||
assertThat(reservedWords.escape("Baz"), equalTo("Baz"));
|
||||
assertThat(reservedWords.escape("qux"), equalTo("qux"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mapsOverReservedWordsCaseInsensitively() {
|
||||
ReservedWords reservedWords = MappedReservedWords.builder()
|
||||
.putCaseInsensitive("foo", "Foo")
|
||||
.putCaseInsensitive("baz", "Baz")
|
||||
.build();
|
||||
|
||||
assertThat(reservedWords.isReserved("foo"), is(true));
|
||||
assertThat(reservedWords.isReserved("Foo"), is(true));
|
||||
assertThat(reservedWords.isReserved("baz"), is(true));
|
||||
assertThat(reservedWords.isReserved("Baz"), is(true));
|
||||
assertThat(reservedWords.isReserved("qux"), is(false));
|
||||
|
||||
assertThat(reservedWords.escape("foo"), equalTo("Foo"));
|
||||
assertThat(reservedWords.escape("Foo"), equalTo("Foo"));
|
||||
assertThat(reservedWords.escape("baz"), equalTo("Baz"));
|
||||
assertThat(reservedWords.escape("Baz"), equalTo("Baz"));
|
||||
assertThat(reservedWords.escape("qux"), equalTo("qux"));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,110 @@
|
|||
/*
|
||||
* Copyright 2019 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.smithy.codegen.core;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import software.amazon.smithy.model.shapes.MemberShape;
|
||||
import software.amazon.smithy.model.shapes.Shape;
|
||||
import software.amazon.smithy.model.shapes.StringShape;
|
||||
|
||||
public class ReservedWordSymbolProviderTest {
|
||||
@Test
|
||||
public void escapesReservedFilenames() {
|
||||
Shape s1 = StringShape.builder().id("foo.bar#Baz").build();
|
||||
Shape s2 = StringShape.builder().id("foo.bar#Bam").build();
|
||||
|
||||
ReservedWords reservedWords = MappedReservedWords.builder().put("/foo/bar/bam", "/rewritten").build();
|
||||
MockProvider delegate = new MockProvider();
|
||||
SymbolProvider provider = ReservedWordSymbolProvider.builder()
|
||||
.symbolProvider(delegate)
|
||||
.filenameReservedWords(reservedWords)
|
||||
.build();
|
||||
delegate.mock = Symbol.builder()
|
||||
.name("foo")
|
||||
.definitionFile("/foo/bar/bam")
|
||||
.declarationFile("/foo/bar/bam")
|
||||
.build();
|
||||
|
||||
assertThat(provider.toSymbol(s1).getDeclarationFile(), equalTo("/rewritten"));
|
||||
assertThat(provider.toSymbol(s2).getDefinitionFile(), equalTo("/rewritten"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void escapesReservedNamespaces() {
|
||||
Shape s1 = StringShape.builder().id("foo.bar#Baz").build();
|
||||
Shape s2 = StringShape.builder().id("foo.baz#Bam").build();
|
||||
|
||||
ReservedWords reservedWords = MappedReservedWords.builder().put("foo.baz", "foo._baz").build();
|
||||
MockProvider delegate = new MockProvider();
|
||||
SymbolProvider provider = ReservedWordSymbolProvider.builder()
|
||||
.symbolProvider(delegate)
|
||||
.namespaceReservedWords(reservedWords)
|
||||
.build();
|
||||
|
||||
delegate.mock = Symbol.builder().namespace("foo.bar", ".").name("Baz").build();
|
||||
assertThat(provider.toSymbol(s1).getNamespace(), equalTo("foo.bar"));
|
||||
|
||||
delegate.mock = Symbol.builder().namespace("foo.baz", ".").name("Bam").build();
|
||||
assertThat(provider.toSymbol(s2).getNamespace(), equalTo("foo._baz"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void escapesReservedNames() {
|
||||
Shape s1 = StringShape.builder().id("foo.bar#Baz").build();
|
||||
Shape s2 = StringShape.builder().id("foo.baz#Bam").build();
|
||||
|
||||
ReservedWords reservedWords = MappedReservedWords.builder().put("Bam", "_Bam").build();
|
||||
MockProvider delegate = new MockProvider();
|
||||
SymbolProvider provider = ReservedWordSymbolProvider.builder()
|
||||
.symbolProvider(delegate)
|
||||
.nameReservedWords(reservedWords)
|
||||
.build();
|
||||
|
||||
delegate.mock = Symbol.builder().namespace("foo.bar", ".").name("Baz").build();
|
||||
assertThat(provider.toSymbol(s1).getName(), equalTo("Baz"));
|
||||
|
||||
delegate.mock = Symbol.builder().namespace("foo.baz", ".").name("Bam").build();
|
||||
assertThat(provider.toSymbol(s2).getName(), equalTo("_Bam"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void escapesReservedMemberNames() {
|
||||
Shape s1 = MemberShape.builder().id("foo.bar#Baz$foo").target("foo.baz#T").build();
|
||||
Shape s2 = MemberShape.builder().id("foo.baz#Baz$baz").target("foo.baz#T").build();
|
||||
|
||||
ReservedWords reservedWords = MappedReservedWords.builder().put("baz", "_baz").build();
|
||||
SymbolProvider delegate = new MockProvider();
|
||||
SymbolProvider provider = ReservedWordSymbolProvider.builder()
|
||||
.symbolProvider(delegate)
|
||||
.memberReservedWords(reservedWords)
|
||||
.build();
|
||||
|
||||
assertThat(provider.toMemberName(s1), equalTo("foo"));
|
||||
assertThat(provider.toMemberName(s2), equalTo("_baz"));
|
||||
}
|
||||
|
||||
private static final class MockProvider implements SymbolProvider {
|
||||
public Symbol mock;
|
||||
|
||||
@Override
|
||||
public Symbol toSymbol(Shape shape) {
|
||||
return mock;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* Copyright 2019 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.smithy.codegen.core;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
public class ReservedWordsTest {
|
||||
@Test
|
||||
public void composesImplementations() {
|
||||
ReservedWords a = MappedReservedWords.builder().put("void", "_void").build();
|
||||
ReservedWords b = MappedReservedWords.builder().put("foo", "_foo").build();
|
||||
ReservedWords composed = ReservedWords.compose(a, b);
|
||||
|
||||
assertThat(composed.isReserved("void"), is(true));
|
||||
assertThat(composed.isReserved("foo"), is(true));
|
||||
assertThat(composed.isReserved("Void"), is(false));
|
||||
|
||||
assertThat(composed.escape("void"), equalTo("_void"));
|
||||
assertThat(composed.escape("foo"), equalTo("_foo"));
|
||||
assertThat(composed.escape("pass"), equalTo("pass"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void identityImplementation() {
|
||||
ReservedWords reservedWords = ReservedWords.identity();
|
||||
|
||||
assertThat(reservedWords.isReserved("void"), is(false));
|
||||
assertThat(reservedWords.escape("void"), equalTo("void"));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,106 @@
|
|||
/*
|
||||
* Copyright 2019 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.smithy.codegen.core;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
public class StringUtilsTest {
|
||||
@Test
|
||||
public void convertsSnakeToLowerCamelCase() {
|
||||
assertThat(StringUtils.snakeToCamelCase("foo_bar"), equalTo("fooBar"));
|
||||
assertThat(StringUtils.snakeToCamelCase("Foo_bar"), equalTo("fooBar"));
|
||||
assertThat(StringUtils.snakeToCamelCase("__foo_bar"), equalTo("fooBar"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void convertsSnakeToPascalCase() {
|
||||
assertThat(StringUtils.snakeToPascalCase("foo_bar"), equalTo("FooBar"));
|
||||
assertThat(StringUtils.snakeToPascalCase("Foo_bar"), equalTo("FooBar"));
|
||||
assertThat(StringUtils.snakeToPascalCase("__foo_bar"), equalTo("FooBar"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void convertsToSnakeCase() {
|
||||
assertThat(StringUtils.toSnakeCase("foo"), equalTo("foo"));
|
||||
assertThat(StringUtils.toSnakeCase(" foo"), equalTo("_foo"));
|
||||
assertThat(StringUtils.toSnakeCase(" fooBar"), equalTo("_foo_bar"));
|
||||
assertThat(StringUtils.toSnakeCase("10-foo"), equalTo("10_foo"));
|
||||
assertThat(StringUtils.toSnakeCase("_foo"), equalTo("_foo"));
|
||||
assertThat(StringUtils.toSnakeCase("FooBar"), equalTo("foo_bar"));
|
||||
assertThat(StringUtils.toSnakeCase("FooAPI"), equalTo("foo_api"));
|
||||
assertThat(StringUtils.toSnakeCase("Ec2Foo"), equalTo("ec2_foo"));
|
||||
assertThat(StringUtils.toSnakeCase("foo_bar"), equalTo("foo_bar"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void uppercaseFirst() {
|
||||
assertThat(StringUtils.capitalize("foo"), equalTo("Foo"));
|
||||
assertThat(StringUtils.capitalize(" foo"), equalTo(" foo"));
|
||||
assertThat(StringUtils.capitalize("10-foo"), equalTo("10-foo"));
|
||||
assertThat(StringUtils.capitalize("_foo"), equalTo("_foo"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void lowercaseFirst() {
|
||||
assertThat(StringUtils.uncapitalize("foo"), equalTo("foo"));
|
||||
assertThat(StringUtils.uncapitalize(" foo"), equalTo(" foo"));
|
||||
assertThat(StringUtils.uncapitalize("10-foo"), equalTo("10-foo"));
|
||||
assertThat(StringUtils.uncapitalize("_foo"), equalTo("_foo"));
|
||||
assertThat(StringUtils.uncapitalize("Foo"), equalTo("foo"));
|
||||
assertThat(StringUtils.uncapitalize(" Foo"), equalTo(" Foo"));
|
||||
assertThat(StringUtils.uncapitalize("10-Foo"), equalTo("10-Foo"));
|
||||
assertThat(StringUtils.uncapitalize("_Foo"), equalTo("_Foo"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void escapesSingleQuotes() {
|
||||
assertThat(StringUtils.escapeSingleQuote("foo"), equalTo("foo"));
|
||||
assertThat(StringUtils.escapeSingleQuote("fo'o"), equalTo("fo\\'o"));
|
||||
assertThat(StringUtils.escapeSingleQuote("fo\\o"), equalTo("fo\\\\o"));
|
||||
assertThat(StringUtils.escapeSingleQuote("\"foo\""), equalTo("\"foo\""));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void escapesDoubleQuotes() {
|
||||
assertThat(StringUtils.escapeDoubleQuote("foo"), equalTo("foo"));
|
||||
assertThat(StringUtils.escapeDoubleQuote("fo'o"), equalTo("fo'o"));
|
||||
assertThat(StringUtils.escapeDoubleQuote("fo\\o"), equalTo("fo\\\\o"));
|
||||
assertThat(StringUtils.escapeDoubleQuote("\"foo\""), equalTo("\\\"foo\\\""));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void capitalizesAndUncapitalizes() {
|
||||
assertThat(StringUtils.capitalize(null), equalTo(null));
|
||||
assertThat(StringUtils.uncapitalize(null), equalTo(null));
|
||||
|
||||
assertThat(StringUtils.capitalize(""), equalTo(""));
|
||||
assertThat(StringUtils.uncapitalize(""), equalTo(""));
|
||||
|
||||
assertThat(StringUtils.capitalize("Foo"), equalTo("Foo"));
|
||||
assertThat(StringUtils.uncapitalize("Foo"), equalTo("foo"));
|
||||
|
||||
assertThat(StringUtils.capitalize("foo"), equalTo("Foo"));
|
||||
assertThat(StringUtils.uncapitalize("foo"), equalTo("foo"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void wrapsText() {
|
||||
assertThat(StringUtils.wrap("hello, there, bud", 6), equalTo(String.format("hello,%nthere,%nbud")));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,101 @@
|
|||
/*
|
||||
* Copyright 2019 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.smithy.codegen.core;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
public class SymbolTest {
|
||||
@Test
|
||||
public void relativizesSymbol() {
|
||||
String ns = "com.foo";
|
||||
Symbol symbol = Symbol.builder()
|
||||
.name("Baz")
|
||||
.namespace(ns, "::")
|
||||
.build();
|
||||
|
||||
assertThat(symbol.relativize(ns), equalTo("Baz"));
|
||||
assertThat(symbol.relativize("com.bam"), equalTo("com.foo::Baz"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getsTypedProperties() {
|
||||
Symbol symbol = Symbol.builder()
|
||||
.name("foo")
|
||||
.putProperty("baz", "bar")
|
||||
.putProperty("bam", 100)
|
||||
.build();
|
||||
|
||||
assertThat(symbol.expectProperty("baz", String.class), equalTo("bar"));
|
||||
assertThat(symbol.expectProperty("bam", Integer.class), equalTo(100));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void throwsIfExpectedPropertyIsNotPresent() {
|
||||
Assertions.assertThrows(IllegalArgumentException.class, () -> {
|
||||
Symbol symbol = Symbol.builder().name("foo").build();
|
||||
|
||||
symbol.expectProperty("baz");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void throwsIfExpectedPropertyIsNotOfSameType() {
|
||||
Assertions.assertThrows(IllegalArgumentException.class, () -> {
|
||||
Symbol symbol = Symbol.builder()
|
||||
.name("foo")
|
||||
.putProperty("bam", 100)
|
||||
.build();
|
||||
|
||||
symbol.expectProperty("bam", String.class);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void returnsDefinitionIfDeclarationPresent() {
|
||||
Symbol symbol = Symbol.builder()
|
||||
.name("foo")
|
||||
.declarationFile("/foo/bar.baz")
|
||||
.build();
|
||||
|
||||
assertThat(symbol.getDefinitionFile(), equalTo("/foo/bar.baz"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void returnsDeclarationIfDefinitionPresent() {
|
||||
Symbol symbol = Symbol.builder()
|
||||
.name("foo")
|
||||
.declarationFile("/foo/bar.baz")
|
||||
.build();
|
||||
|
||||
assertThat(symbol.getDeclarationFile(), equalTo("/foo/bar.baz"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void returnsAppropriateDefinitionAndDeclarationFiles() {
|
||||
Symbol symbol = Symbol.builder()
|
||||
.name("foo")
|
||||
.definitionFile("/foo/bar.baz")
|
||||
.declarationFile("/foo/bar.h")
|
||||
.build();
|
||||
|
||||
assertThat(symbol.getDefinitionFile(), equalTo("/foo/bar.baz"));
|
||||
assertThat(symbol.getDeclarationFile(), equalTo("/foo/bar.h"));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
# FreeMarker template integration for SmithyCodegen
|
||||
|
||||
This package integrates [FreeMarker](https://freemarker.apache.org/)
|
||||
with SmithyCodegen.
|
||||
|
||||
|
||||
## Install
|
||||
|
||||
Maven
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>software.amazon.smithy</groupId>
|
||||
<artifactId>smithy-codegen-freemarker</artifactId>
|
||||
<version>0.1.0</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
Note: this package is pre-installed and provided by the Smithy CLI. Jars
|
||||
dropped in the module path for the CLI do not need to include this
|
||||
dependency.
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
Given the following template,
|
||||
|
||||
```freemarker
|
||||
Hello, ${name}
|
||||
```
|
||||
|
||||
and the following code,
|
||||
|
||||
```java
|
||||
// Create the template engine.
|
||||
var engine = FreeMarkerEngine.builder()
|
||||
.classLoader(getClass().getClassLoader())
|
||||
.build();
|
||||
|
||||
// Create a data model.
|
||||
Map<String, Object> dataModel = new HashMap<>();
|
||||
dataModel.put("name", "Roosevelt");
|
||||
|
||||
// Render a template.
|
||||
var result = engine.render("com/foo/baz/test.ftl", dataModel);
|
||||
```
|
||||
|
||||
the template engine would render:
|
||||
|
||||
```
|
||||
Hello, Roosevelt
|
||||
```
|
||||
|
||||
|
||||
## Data model
|
||||
|
||||
A *data model* is passed to the template engine when rendering templates.
|
||||
The data model is a map of strings to objects that are interpreted by
|
||||
FreeMarker when rendering templates. Smithy will automatically configure
|
||||
FreeMarker to treat Smithy's Node values like normal Java built-in types:
|
||||
|
||||
* An `ObjectNode` is converted to a `Map`.
|
||||
* An `ArrayNode` is converted to a `List`.
|
||||
* A `StringNode` is converted to a `String`.
|
||||
* A `BooleanNode` is converted to a `Boolean`.
|
||||
* A `NumberNode` is converted to a `Number`.
|
||||
* A `NullNode` is converted to a `null`, so use a trailing `?` or `!`
|
||||
in your templates when dealing with a `NullNode`.
|
||||
|
||||
Smithy will configure FreeMarker to understand some additional Java
|
||||
types that were added after FreeMarker was created:
|
||||
|
||||
* An empty `Optional` is automatically converted to `null`.
|
||||
* An `Optional` with a value is automatically unwrapped.
|
||||
* A `Stream` is converted to an `Iterator`.
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Copyright 2019 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.
|
||||
*/
|
||||
|
||||
dependencies {
|
||||
implementation(project(":smithy-model"))
|
||||
api(project(":smithy-codegen-core"))
|
||||
|
||||
implementation("org.freemarker:freemarker:2.3.28")
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue