Skip to content

Commit

Permalink
Add round-trip convertibility of resource schemas (#2445)
Browse files Browse the repository at this point in the history
  • Loading branch information
haydenbaker authored Nov 13, 2024
1 parent 6d296d5 commit ccf6e6a
Show file tree
Hide file tree
Showing 6 changed files with 844 additions and 22 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,14 @@
package software.amazon.smithy.aws.cloudformation.schema.model;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import software.amazon.smithy.jsonschema.Schema;
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;
import software.amazon.smithy.utils.ListUtils;
import software.amazon.smithy.utils.SmithyBuilder;
import software.amazon.smithy.utils.ToSmithyBuilder;

Expand All @@ -32,53 +34,70 @@
* @see <a href="https://github.com/aws-cloudformation/cloudformation-cli/blob/master/src/rpdk/core/data/schema/provider.definition.schema.v1.jsonL74">Resource Type Properties JSON Schema</a>
*/
public final class Property implements ToNode, ToSmithyBuilder<Property> {
private final boolean insertionOrder;
private final List<String> dependencies;
private final Schema schema;
// Other reserved property names in definition but not in the validation
// JSON Schema, so not defined in code:
// * readOnly
// * writeOnly

private Property(Builder builder) {
this.insertionOrder = builder.insertionOrder;
this.dependencies = ListUtils.copyOf(builder.dependencies);
this.schema = builder.schema;
}
Schema.Builder schemaBuilder;

@Override
public Node toNode() {
ObjectNode.Builder builder = schema.toNode().expectObjectNode().toBuilder();
if (builder.schema == null) {
schemaBuilder = Schema.builder();
} else {
schemaBuilder = builder.schema.toBuilder();
}

// Only serialize these properties if set to non-defaults.
if (insertionOrder) {
builder.withMember("insertionOrder", Node.from(insertionOrder));
if (builder.insertionOrder) {
schemaBuilder.putExtension("insertionOrder", Node.from(true));
}
if (!dependencies.isEmpty()) {
builder.withMember("dependencies", Node.fromStrings(dependencies));

if (!builder.dependencies.isEmpty()) {
schemaBuilder.putExtension("dependencies", Node.fromStrings(builder.dependencies));
}

return builder.build();
this.schema = schemaBuilder.build();
}

@Override
public Node toNode() {
return schema.toNode().expectObjectNode();
}

@Override
public Builder toBuilder() {
return builder()
.insertionOrder(insertionOrder)
.dependencies(dependencies)
.schema(schema);
return builder().schema(schema);
}

public static Property fromNode(Node node) {
ObjectNode objectNode = node.expectObjectNode();
Builder builder = builder();
builder.schema(Schema.fromNode(objectNode));

return builder.build();
}

public static Property fromSchema(Schema schema) {
return builder().schema(schema).build();
}

public static Builder builder() {
return new Builder();
}

public boolean isInsertionOrder() {
return insertionOrder;
Optional<Boolean> insertionOrder = schema.getExtension("insertionOrder")
.map(n -> n.toNode().expectBooleanNode().getValue());

return insertionOrder.orElse(false);
}

public List<String> getDependencies() {
return dependencies;
Optional<List<String>> dependencies = schema.getExtension("dependencies")
.map(n -> n.toNode().expectArrayNode().getElementsAs(StringNode::getValue));

return dependencies.orElse(Collections.emptyList());
}

public Schema getSchema() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ public final class ResourceSchema implements ToNode, ToSmithyBuilder<ResourceSch
private final Map<String, Handler> handlers = new TreeMap<>(Comparator.comparing(Handler::getHandlerNameOrder));
private final Map<String, Remote> remotes = new TreeMap<>();
private final Tagging tagging;
private final Schema additionalProperties;

private ResourceSchema(Builder builder) {
typeName = SmithyBuilder.requiredState("typeName", builder.typeName);
Expand All @@ -84,6 +85,7 @@ private ResourceSchema(Builder builder) {
handlers.putAll(builder.handlers);
remotes.putAll(builder.remotes);
tagging = builder.tagging;
additionalProperties = builder.additionalProperties;
}

@Override
Expand Down Expand Up @@ -136,6 +138,9 @@ public Node toNode() {
if (tagging != null) {
builder.withMember("tagging", mapper.serialize(tagging));
}
if (additionalProperties != null) {
builder.withMember("additionalProperties", mapper.serialize(additionalProperties));
}

return builder.build();
}
Expand All @@ -161,6 +166,11 @@ public Builder toBuilder() {
.tagging(tagging);
}

public static ResourceSchema fromNode(Node node) {
NodeMapper mapper = new NodeMapper();
return mapper.deserializeInto(node, ResourceSchema.builder()).build();
}

public static Builder builder() {
return new Builder();
}
Expand Down Expand Up @@ -242,6 +252,7 @@ public static final class Builder implements SmithyBuilder<ResourceSchema> {
private final Map<String, Handler> handlers = new TreeMap<>();
private final Map<String, Remote> remotes = new TreeMap<>();
private Tagging tagging;
private Schema additionalProperties;

private Builder() {}

Expand Down Expand Up @@ -470,5 +481,10 @@ public Builder clearRemotes() {
this.remotes.clear();
return this;
}

public Builder additionalProperties(Schema additionalProperties) {
this.additionalProperties = additionalProperties;
return this;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* Copyright 2020 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.cloudformation.schema.fromsmithy;

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import software.amazon.smithy.aws.cloudformation.schema.model.ResourceSchema;
import software.amazon.smithy.model.node.Node;
import software.amazon.smithy.utils.IoUtils;

import java.io.IOException;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.stream.Collectors;

public class ResourceSchemaTest {

@ParameterizedTest
@MethodSource("resourceSchemaFiles")
public void validateResourceSchemaFromNodeToNode(String resourceSchemaFile) {
String json = IoUtils.readUtf8File(resourceSchemaFile);

Node node = Node.parse(json);
ResourceSchema schemaFromNode = ResourceSchema.fromNode(node);
Node nodeFromSchema = schemaFromNode.toNode();

Node.assertEquals(nodeFromSchema, node);
}

public static List<String> resourceSchemaFiles() {
try {
Path definitionPath = Paths.get(ResourceSchemaTest.class.getResource("aws-sagemaker-domain.cfn.json").toURI());

return Files.walk(Paths.get(definitionPath.getParent().toUri()))
.filter(Files::isRegularFile)
.filter(file -> file.toString().endsWith(".cfn.json"))
.map(Object::toString)
.collect(Collectors.toList());
} catch (IOException | URISyntaxException e) {
throw new RuntimeException(e);
}
}
}
Loading

0 comments on commit ccf6e6a

Please sign in to comment.