From 5d5f3178b92056e7314d1252a26a716e22c8f4ea Mon Sep 17 00:00:00 2001 From: Hayden Baker Date: Thu, 7 Nov 2024 12:38:33 -0800 Subject: [PATCH 1/2] Add round-trip convertibility of resource schemas --- .../cloudformation/schema/model/Property.java | 50 +- .../schema/model/ResourceSchema.java | 11 + .../schema/fromsmithy/ResourceSchemaTest.java | 61 ++ .../fromsmithy/aws-sagemaker-domain.json | 564 ++++++++++++++++++ .../amazon/smithy/jsonschema/Schema.java | 163 +++++ .../model/node/DefaultNodeDeserializers.java | 1 + 6 files changed, 836 insertions(+), 14 deletions(-) create mode 100644 smithy-aws-cloudformation/src/test/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/ResourceSchemaTest.java create mode 100644 smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/aws-sagemaker-domain.json diff --git a/smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/model/Property.java b/smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/model/Property.java index 61ccade1ecd..947ce3ef0ef 100644 --- a/smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/model/Property.java +++ b/smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/model/Property.java @@ -20,8 +20,8 @@ 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; @@ -41,24 +41,30 @@ public final class Property implements ToNode, ToSmithyBuilder { // * 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)); + this.insertionOrder = builder.insertionOrder; + if (this.insertionOrder) { + schemaBuilder.putExtension("insertionOrder", Node.from(true)); } - if (!dependencies.isEmpty()) { - builder.withMember("dependencies", Node.fromStrings(dependencies)); + + this.dependencies = builder.dependencies; + if (!this.dependencies.isEmpty()) { + schemaBuilder.putExtension("dependencies", Node.fromStrings(this.dependencies)); } - return builder.build(); + this.schema = schemaBuilder.build(); + } + + @Override + public Node toNode() { + return schema.toNode().expectObjectNode(); } @Override @@ -69,6 +75,22 @@ public Builder toBuilder() { .schema(schema); } + public static Property fromNode(Node node) { + ObjectNode objectNode = node.expectObjectNode(); + Builder builder = builder(); + + objectNode.getBooleanMember("insertionOrder", builder::insertionOrder); + objectNode.getArrayMember("dependencies", StringNode::getValue, builder::dependencies); + + 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(); } diff --git a/smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/model/ResourceSchema.java b/smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/model/ResourceSchema.java index a006ced3ba0..7ae658ca61c 100644 --- a/smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/model/ResourceSchema.java +++ b/smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/model/ResourceSchema.java @@ -60,6 +60,7 @@ public final class ResourceSchema implements ToNode, ToSmithyBuilder handlers = new TreeMap<>(Comparator.comparing(Handler::getHandlerNameOrder)); private final Map remotes = new TreeMap<>(); private final Tagging tagging; + private final Schema additionalProperties; private ResourceSchema(Builder builder) { typeName = SmithyBuilder.requiredState("typeName", builder.typeName); @@ -84,6 +85,7 @@ private ResourceSchema(Builder builder) { handlers.putAll(builder.handlers); remotes.putAll(builder.remotes); tagging = builder.tagging; + additionalProperties = builder.additionalProperties; } @Override @@ -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(); } @@ -242,6 +247,7 @@ public static final class Builder implements SmithyBuilder { private final Map handlers = new TreeMap<>(); private final Map remotes = new TreeMap<>(); private Tagging tagging; + private Schema additionalProperties; private Builder() {} @@ -470,5 +476,10 @@ public Builder clearRemotes() { this.remotes.clear(); return this; } + + public Builder additionalProperties(Schema additionalProperties) { + this.additionalProperties = additionalProperties; + return this; + } } } diff --git a/smithy-aws-cloudformation/src/test/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/ResourceSchemaTest.java b/smithy-aws-cloudformation/src/test/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/ResourceSchemaTest.java new file mode 100644 index 00000000000..8c87f7b3f46 --- /dev/null +++ b/smithy-aws-cloudformation/src/test/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/ResourceSchemaTest.java @@ -0,0 +1,61 @@ +/* + * 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.model.node.NodeMapper; +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) { + NodeMapper mapper = new NodeMapper(); + String json = IoUtils.readUtf8File(resourceSchemaFile); + + Node node = Node.parse(json); + ResourceSchema schemaFromNode = mapper.deserialize(node, ResourceSchema.class); + Node nodeFromSchema = schemaFromNode.toNode(); + + Node.assertEquals(nodeFromSchema.withDeepSortedKeys(), node.withDeepSortedKeys()); + } + + public static List resourceSchemaFiles() { + try { + Path definitionPath = Paths.get(ResourceSchemaTest.class.getResource("aws-sagemaker-domain.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); + } + } +} diff --git a/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/aws-sagemaker-domain.json b/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/aws-sagemaker-domain.json new file mode 100644 index 00000000000..9581e2d3d3a --- /dev/null +++ b/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/aws-sagemaker-domain.json @@ -0,0 +1,564 @@ +{ + "typeName": "AWS::SageMaker::Domain", + "description": "Resource Type definition for AWS::SageMaker::Domain", + "additionalProperties": false, + "properties": { + "DomainArn": { + "type": "string", + "description": "The Amazon Resource Name (ARN) of the created domain.", + "maxLength": 256, + "pattern": "arn:aws[a-z\\-]*:sagemaker:[a-z0-9\\-]*:[0-9]{12}:domain/.*" + }, + "Url": { + "type": "string", + "description": "The URL to the created domain.", + "maxLength": 1024 + }, + "AppNetworkAccessType": { + "type": "string", + "description": "Specifies the VPC used for non-EFS traffic. The default value is PublicInternetOnly.", + "enum": [ + "PublicInternetOnly", + "VpcOnly" + ] + }, + "AuthMode": { + "type": "string", + "description": "The mode of authentication that members use to access the domain.", + "enum": [ + "SSO", + "IAM" + ] + }, + "DefaultUserSettings": { + "$ref": "#/definitions/UserSettings", + "description": "The default user settings." + }, + "DefaultSpaceSettings": { + "$ref": "#/definitions/DefaultSpaceSettings", + "description": "The default space settings." + }, + "DomainName": { + "type": "string", + "description": "A name for the domain.", + "maxLength": 63, + "pattern": "^[a-zA-Z0-9](-*[a-zA-Z0-9]){0,62}" + }, + "KmsKeyId": { + "type": "string", + "description": "SageMaker uses AWS KMS to encrypt the EFS volume attached to the domain with an AWS managed customer master key (CMK) by default.", + "maxLength": 2048, + "pattern": ".*" + }, + "SubnetIds": { + "type": "array", + "description": "The VPC subnets that Studio uses for communication.", + "uniqueItems": true, + "insertionOrder": false, + "minItems": 1, + "maxItems": 16, + "items": { + "type": "string", + "maxLength": 32, + "pattern": "[-0-9a-zA-Z]+" + } + }, + "Tags": { + "type": "array", + "description": "A list of tags to apply to the user profile.", + "uniqueItems": true, + "insertionOrder": false, + "minItems": 0, + "maxItems": 50, + "items": { + "$ref": "#/definitions/Tag" + } + }, + "VpcId": { + "type": "string", + "description": "The ID of the Amazon Virtual Private Cloud (VPC) that Studio uses for communication.", + "maxLength": 32, + "pattern": "[-0-9a-zA-Z]+" + }, + "DomainId": { + "type": "string", + "description": "The domain name.", + "maxLength": 63, + "pattern": "^d-(-*[a-z0-9])+" + }, + "HomeEfsFileSystemId": { + "type" : "string", + "description" : "The ID of the Amazon Elastic File System (EFS) managed by this Domain.", + "maxLength" : 32 + }, + "SingleSignOnManagedApplicationInstanceId": { + "type" : "string", + "description" : "The SSO managed application instance ID.", + "maxLength" : 256 + }, + "DomainSettings": { + "$ref": "#/definitions/DomainSettings" + }, + "AppSecurityGroupManagement": { + "type": "string", + "description": "The entity that creates and manages the required security groups for inter-app communication in VPCOnly mode. Required when CreateDomain.AppNetworkAccessType is VPCOnly and DomainSettings.RStudioServerProDomainSettings.DomainExecutionRoleArn is provided.", + "enum": [ + "Service", + "Customer" + ] + }, + "SecurityGroupIdForDomainBoundary": { + "type": "string", + "description": "The ID of the security group that authorizes traffic between the RSessionGateway apps and the RStudioServerPro app.", + "maxLength": 32, + "pattern": "[-0-9a-zA-Z]+" + } + }, + "definitions": { + "UserSettings": { + "type": "object", + "description": "A collection of settings that apply to users of Amazon SageMaker Studio. These settings are specified when the CreateUserProfile API is called, and as DefaultUserSettings when the CreateDomain API is called.", + "additionalProperties": false, + "properties": { + "ExecutionRole": { + "type": "string", + "description": "The execution role for the user.", + "minLength": 20, + "maxLength": 2048, + "pattern": "^arn:aws[a-z\\-]*:iam::\\d{12}:role/?[a-zA-Z_0-9+=,.@\\-_/]+$" + }, + "JupyterServerAppSettings": { + "$ref": "#/definitions/JupyterServerAppSettings", + "description": "The Jupyter server's app settings." + }, + "KernelGatewayAppSettings": { + "$ref": "#/definitions/KernelGatewayAppSettings", + "description": "The kernel gateway app settings." + }, + "RStudioServerProAppSettings": { + "$ref": "#/definitions/RStudioServerProAppSettings" + }, + "RSessionAppSettings": { + "$ref": "#/definitions/RSessionAppSettings" + }, + "SecurityGroups": { + "type": "array", + "description": "The security groups for the Amazon Virtual Private Cloud (VPC) that Studio uses for communication.", + "uniqueItems": true, + "insertionOrder": false, + "minItems": 0, + "maxItems": 5, + "items": { + "type": "string", + "maxLength": 32, + "pattern": "[-0-9a-zA-Z]+" + } + }, + "SharingSettings": { + "$ref": "#/definitions/SharingSettings", + "description": "The sharing settings." + } + } + }, + "DefaultSpaceSettings": { + "type": "object", + "description": "A collection of settings that apply to spaces of Amazon SageMaker Studio. These settings are specified when the Create/Update Domain API is called.", + "additionalProperties": false, + "properties": { + "ExecutionRole": { + "type": "string", + "description": "The execution role for the space.", + "minLength": 20, + "maxLength": 2048, + "pattern": "^arn:aws[a-z\\-]*:iam::\\d{12}:role/?[a-zA-Z_0-9+=,.@\\-_/]+$" + }, + "JupyterServerAppSettings": { + "$ref": "#/definitions/JupyterServerAppSettings", + "description": "The Jupyter server's app settings." + }, + "KernelGatewayAppSettings": { + "$ref": "#/definitions/KernelGatewayAppSettings", + "description": "The kernel gateway app settings." + }, + "SecurityGroups": { + "type": "array", + "description": "The security groups for the Amazon Virtual Private Cloud (VPC) that Studio uses for communication.", + "uniqueItems": true, + "insertionOrder": false, + "minItems": 0, + "maxItems": 5, + "items": { + "type": "string", + "maxLength": 32, + "pattern": "[-0-9a-zA-Z]+" + } + } + } + }, + "JupyterServerAppSettings": { + "type": "object", + "description": "The JupyterServer app settings.", + "additionalProperties": false, + "properties": { + "DefaultResourceSpec": { + "$ref": "#/definitions/ResourceSpec" + } + } + }, + "ResourceSpec": { + "type": "object", + "additionalProperties": false, + "properties": { + "InstanceType": { + "type": "string", + "description": "The instance type that the image version runs on.", + "enum": [ + "system", + "ml.t3.micro", + "ml.t3.small", + "ml.t3.medium", + "ml.t3.large", + "ml.t3.xlarge", + "ml.t3.2xlarge", + "ml.m5.large", + "ml.m5.xlarge", + "ml.m5.2xlarge", + "ml.m5.4xlarge", + "ml.m5.8xlarge", + "ml.m5.12xlarge", + "ml.m5.16xlarge", + "ml.m5.24xlarge", + "ml.c5.large", + "ml.c5.xlarge", + "ml.c5.2xlarge", + "ml.c5.4xlarge", + "ml.c5.9xlarge", + "ml.c5.12xlarge", + "ml.c5.18xlarge", + "ml.c5.24xlarge", + "ml.p3.2xlarge", + "ml.p3.8xlarge", + "ml.p3.16xlarge", + "ml.g4dn.xlarge", + "ml.g4dn.2xlarge", + "ml.g4dn.4xlarge", + "ml.g4dn.8xlarge", + "ml.g4dn.12xlarge", + "ml.g4dn.16xlarge", + "ml.r5.large", + "ml.r5.xlarge", + "ml.r5.2xlarge", + "ml.r5.4xlarge", + "ml.r5.8xlarge", + "ml.r5.12xlarge", + "ml.r5.16xlarge", + "ml.r5.24xlarge", + "ml.p3dn.24xlarge", + "ml.m5d.large", + "ml.m5d.xlarge", + "ml.m5d.2xlarge", + "ml.m5d.4xlarge", + "ml.m5d.8xlarge", + "ml.m5d.12xlarge", + "ml.m5d.16xlarge", + "ml.m5d.24xlarge", + "ml.g5.xlarge", + "ml.g5.2xlarge", + "ml.g5.4xlarge", + "ml.g5.8xlarge", + "ml.g5.12xlarge", + "ml.g5.16xlarge", + "ml.g5.24xlarge", + "ml.g5.48xlarge" + ] + }, + "SageMakerImageArn": { + "type": "string", + "description": "The Amazon Resource Name (ARN) of the SageMaker image that the image version belongs to.", + "maxLength": 256, + "pattern": "^arn:aws(-[\\w]+)*:sagemaker:.+:[0-9]{12}:image/[a-z0-9]([-.]?[a-z0-9])*$" + }, + "SageMakerImageVersionArn": { + "type": "string", + "description": "The Amazon Resource Name (ARN) of the image version created on the instance.", + "maxLength": 256, + "pattern": "^arn:aws(-[\\w]+)*:sagemaker:.+:[0-9]{12}:image-version/[a-z0-9]([-.]?[a-z0-9])*/[0-9]+$" + }, + "LifecycleConfigArn": { + "type": "string", + "description": "The Amazon Resource Name (ARN) of the Lifecycle Configuration to attach to the Resource.", + "maxLength": 256, + "pattern": "arn:aws[a-z\\-]*:sagemaker:[a-z0-9\\-]*:[0-9]{12}:studio-lifecycle-config/.*" + } + } + }, + "KernelGatewayAppSettings": { + "type": "object", + "description": "The kernel gateway app settings.", + "additionalProperties": false, + "properties": { + "CustomImages": { + "type": "array", + "description": "A list of custom SageMaker images that are configured to run as a KernelGateway app.", + "uniqueItems": true, + "insertionOrder": false, + "minItems": 0, + "maxItems": 30, + "items": { + "$ref": "#/definitions/CustomImage" + } + }, + "DefaultResourceSpec": { + "$ref": "#/definitions/ResourceSpec", + "description": "The default instance type and the Amazon Resource Name (ARN) of the default SageMaker image used by the KernelGateway app." + } + } + }, + "CustomImage": { + "type": "object", + "description": "A custom SageMaker image.", + "additionalProperties": false, + "properties": { + "AppImageConfigName": { + "type": "string", + "description": "The Name of the AppImageConfig.", + "maxLength": 63, + "pattern": "^[a-zA-Z0-9](-*[a-zA-Z0-9]){0,62}" + }, + "ImageName": { + "type": "string", + "description": "The name of the CustomImage. Must be unique to your account.", + "maxLength": 63, + "pattern": "^[a-zA-Z0-9]([-.]?[a-zA-Z0-9]){0,62}$" + }, + "ImageVersionNumber": { + "type": "integer", + "description": "The version number of the CustomImage.", + "minimum": 0 + } + }, + "required": [ + "AppImageConfigName", + "ImageName" + ] + }, + "SharingSettings": { + "type": "object", + "description": "Specifies options when sharing an Amazon SageMaker Studio notebook. These settings are specified as part of DefaultUserSettings when the CreateDomain API is called, and as part of UserSettings when the CreateUserProfile API is called.", + "additionalProperties": false, + "properties": { + "NotebookOutputOption": { + "type": "string", + "description": "Whether to include the notebook cell output when sharing the notebook. The default is Disabled.", + "enum": [ + "Allowed", + "Disabled" + ] + }, + "S3KmsKeyId": { + "type": "string", + "description": "When NotebookOutputOption is Allowed, the AWS Key Management Service (KMS) encryption key ID used to encrypt the notebook cell output in the Amazon S3 bucket.", + "maxLength": 2048, + "pattern": ".*" + }, + "S3OutputPath": { + "type": "string", + "description": "When NotebookOutputOption is Allowed, the Amazon S3 bucket used to store the shared notebook snapshots.", + "maxLength": 1024, + "pattern": "^(https|s3)://([^/]+)/?(.*)$" + } + } + }, + "Tag": { + "type": "object", + "additionalProperties": false, + "properties": { + "Value": { + "type": "string", + "minLength": 1, + "maxLength": 128 + }, + "Key": { + "type": "string", + "minLength": 1, + "maxLength": 128 + } + }, + "required": [ + "Key", + "Value" + ] + }, + "DomainSettings": { + "type": "object", + "description":"A collection of Domain settings.", + "additionalProperties": false, + "properties": { + "SecurityGroupIds": { + "type": "array", + "description": "The security groups for the Amazon Virtual Private Cloud that the Domain uses for communication between Domain-level apps and user apps.", + "uniqueItems": true, + "insertionOrder": false, + "minItems": 1, + "maxItems": 3, + "items": { + "type": "string", + "maxLength": 32, + "pattern": "[-0-9a-zA-Z]+" + } + }, + "RStudioServerProDomainSettings": { + "$ref": "#/definitions/RStudioServerProDomainSettings" + } + } + }, + "RStudioServerProDomainSettings": { + "type": "object", + "description": "A collection of settings that update the current configuration for the RStudioServerPro Domain-level app.", + "additionalProperties": false, + "properties": { + "DomainExecutionRoleArn": { + "type": "string", + "description": "The ARN of the execution role for the RStudioServerPro Domain-level app.", + "minLength": 20, + "maxLength": 2048, + "pattern": "^arn:aws[a-z\\-]*:iam::\\d{12}:role/?[a-zA-Z_0-9+=,.@\\-_/]+$" + }, + "RStudioConnectUrl": { + "type": "string", + "description": "A URL pointing to an RStudio Connect server.", + "pattern": "^(https:|http:|www\\.)\\S*" + }, + "RStudioPackageManagerUrl": { + "type": "string", + "description": "A URL pointing to an RStudio Package Manager server.", + "pattern": "^(https:|http:|www\\.)\\S*" + }, + "DefaultResourceSpec": { + "$ref": "#/definitions/ResourceSpec" + } + }, + "required":["DomainExecutionRoleArn"] + }, + "RSessionAppSettings": { + "type": "object", + "description": "A collection of settings that apply to an RSessionGateway app.", + "additionalProperties": false, + "properties": { + "CustomImages": { + "type": "array", + "description": "A list of custom SageMaker images that are configured to run as a KernelGateway app.", + "insertionOrder": false, + "uniqueItems": true, + "minItems": 0, + "maxItems": 30, + "items": { + "$ref": "#/definitions/CustomImage" + } + }, + "DefaultResourceSpec": { + "$ref": "#/definitions/ResourceSpec" + } + } + }, + "RStudioServerProAppSettings": { + "type": "object", + "description": "A collection of settings that configure user interaction with the RStudioServerPro app.", + "additionalProperties": false, + "properties": { + "AccessStatus": { + "type": "string", + "description": "Indicates whether the current user has access to the RStudioServerPro app.", + "enum": [ + "ENABLED", + "DISABLED" + ] + }, + "UserGroup": { + "type": "string", + "description": "The level of permissions that the user has within the RStudioServerPro app. This value defaults to User. The Admin value allows the user access to the RStudio Administrative Dashboard.", + "enum": [ + "R_STUDIO_ADMIN", + "R_STUDIO_USER" + ] + } + } + } + }, + "required": [ + "AuthMode", + "DefaultUserSettings", + "DomainName", + "SubnetIds", + "VpcId" + ], + "createOnlyProperties": [ + "/properties/AppNetworkAccessType", + "/properties/AuthMode", + "/properties/DomainName", + "/properties/DomainSettings/RStudioServerProDomainSettings/DefaultResourceSpec", + "/properties/KmsKeyId", + "/properties/SubnetIds", + "/properties/VpcId", + "/properties/Tags" + ], + "writeOnlyProperties": [ + "/properties/Tags" + ], + "primaryIdentifier": [ + "/properties/DomainId" + ], + "readOnlyProperties": [ + "/properties/DomainArn", + "/properties/Url", + "/properties/DomainId", + "/properties/HomeEfsFileSystemId", + "/properties/SecurityGroupIdForDomainBoundary", + "/properties/SingleSignOnManagedApplicationInstanceId" + ], + "handlers": { + "create": { + "permissions": [ + "sagemaker:CreateApp", + "sagemaker:CreateDomain", + "sagemaker:DescribeDomain", + "sagemaker:DescribeImage", + "sagemaker:DescribeImageVersion", + "iam:CreateServiceLinkedRole", + "iam:PassRole", + "efs:CreateFileSystem", + "kms:CreateGrant", + "kms:Decrypt", + "kms:DescribeKey", + "kms:GenerateDataKeyWithoutPlainText" + ] + }, + "read": { + "permissions": [ + "sagemaker:DescribeDomain" + ] + }, + "update": { + "permissions": [ + "sagemaker:CreateApp", + "sagemaker:UpdateDomain", + "sagemaker:DescribeDomain", + "sagemaker:DescribeImage", + "sagemaker:DescribeImageVersion", + "iam:PassRole" + ] + }, + "delete": { + "permissions": [ + "sagemaker:DeleteApp", + "sagemaker:DeleteDomain", + "sagemaker:DescribeDomain" + ] + }, + "list": { + "permissions": [ + "sagemaker:ListDomains" + ] + } + } +} \ No newline at end of file diff --git a/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/Schema.java b/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/Schema.java index 7d820b970cd..f051d0dd50c 100644 --- a/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/Schema.java +++ b/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/Schema.java @@ -27,8 +27,10 @@ import java.util.Optional; import java.util.logging.Logger; import software.amazon.smithy.model.node.ArrayNode; +import software.amazon.smithy.model.node.BooleanNode; 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.MapUtils; @@ -60,6 +62,9 @@ public final class Schema implements ToNode, ToSmithyBuilder { private static final Logger LOGGER = Logger.getLogger(Schema.class.getName()); + // A schema can be an object OR a boolean - when this value is set (true or false), the schema is represented + // as a "trivial boolean schema". + private final Boolean trivial; private final String ref; private final String type; private final Collection enumValues; @@ -112,6 +117,11 @@ public final class Schema implements ToNode, ToSmithyBuilder { private Node asNode; private Schema(Builder builder) { + trivial = builder.trivial; + if (trivial != null) { + asNode = Node.from(trivial); + } + ref = builder.ref; type = builder.type; enumValues = Collections.unmodifiableCollection(builder.enumValues); @@ -577,10 +587,25 @@ public int hashCode() { return Objects.hash(ref, type, properties, items); } + public static Schema fromNode(Node node) { + if (node.isBooleanNode()) { + BooleanNode booleanNode = node.expectBooleanNode(); + return new Schema.Builder().trivial(booleanNode.getValue()).build(); + } + + ObjectNode objectNode = node.expectObjectNode(); + Schema.Builder builder = builder(); + objectNode.getMembers().forEach((key, val) -> builder.applyNode(key.getValue(), val)); + + return builder.build(); + } + /** * Abstract class used to build Schema components. */ public static final class Builder implements SmithyBuilder { + private Boolean trivial; + private String ref; private String type; private Collection enumValues = ListUtils.of(); @@ -637,6 +662,11 @@ public Schema build() { return new Schema(this); } + public Builder trivial(Boolean trivial) { + this.trivial = trivial; + return this; + } + public Builder ref(String ref) { this.ref = ref; return this; @@ -970,5 +1000,138 @@ public Builder disableProperty(String propertyName) { return this; } } + + Builder applyNode(String key, Node node) { + switch (key) { + case "$ref": + this.ref(node.expectStringNode().getValue()); + break; + case "type": + this.type(node.expectStringNode().getValue()); + break; + case "enum": + this.enumValues(node.expectArrayNode().getElementsAs(StringNode::getValue)); + break; + case "intEnum": + this.intEnumValues( + node.expectArrayNode().getElementsAs((e) -> e.expectNumberNode().getValue().intValue())); + break; + case "const": + this.constValue(node); + break; + case "default": + this.defaultValue(node); + break; + case "multipleOf": + this.multipleOf(node.expectNumberNode().getValue()); + break; + case "maximum": + this.maximum(node.expectNumberNode().getValue()); + break; + case "exclusiveMaximum": + this.exclusiveMaximum(node.expectNumberNode().getValue()); + break; + case "minimum": + this.minimum(node.expectNumberNode().getValue()); + break; + case "exclusiveMinimum": + this.exclusiveMinimum(node.expectNumberNode().getValue()); + break; + case "maxLength": + this.maxLength(node.expectNumberNode().getValue().longValue()); + break; + case "minLength": + this.minLength(node.expectNumberNode().getValue().longValue()); + break; + case "pattern": + this.pattern(node.expectStringNode().getValue()); + break; + case "items": + this.items(Schema.fromNode(node)); + break; + case "maxItems": + this.maxItems(node.expectNumberNode().getValue().intValue()); + break; + case "minItems": + this.minItems(node.expectNumberNode().getValue().intValue()); + break; + case "uniqueItems": + this.uniqueItems(node.expectBooleanNode().getValue()); + break; + case "maxProperties": + this.maxProperties(node.expectNumberNode().getValue().intValue()); + break; + case "minProperties": + this.minProperties(node.expectNumberNode().getValue().intValue()); + break; + case "required": + this.required(node.expectArrayNode().getElementsAs(StringNode::getValue)); + break; + case "properties": + node.expectObjectNode() + .getMembers() + .forEach((k, v) -> this.putProperty(k.getValue(), Schema.fromNode(v))); + break; + case "additionalProperties": + this.additionalProperties(Schema.fromNode(node)); + break; + case "propertyNames": + this.propertyNames(Schema.fromNode(node)); + break; + case "patternProperties": + node.expectObjectNode() + .getMembers() + .forEach((k, v) -> this.putPatternProperty(k.getValue(), Schema.fromNode(v))); + break; + case "allOf": + this.allOf(node.expectArrayNode().getElementsAs(Schema::fromNode)); + break; + case "anyOf": + this.anyOf(node.expectArrayNode().getElementsAs(Schema::fromNode)); + break; + case "oneOf": + this.oneOf(node.expectArrayNode().getElementsAs(Schema::fromNode)); + break; + case "not": + this.not(Schema.fromNode(node)); + break; + case "title": + this.title(node.expectStringNode().getValue()); + break; + case "description": + this.description(node.expectStringNode().getValue()); + break; + case "format": + this.format(node.expectStringNode().getValue()); + break; + case "readOnly": + this.readOnly(node.expectBooleanNode().getValue()); + break; + case "writeOnly": + this.writeOnly(node.expectBooleanNode().getValue()); + break; + case "comment": + this.comment(node.expectStringNode().getValue()); + break; + case "examples": + this.examples(node); + break; + case "deprecated": + this.deprecated(node.expectBooleanNode().getValue()); + break; + case "contentEncoding": + this.contentEncoding(node.expectStringNode().getValue()); + break; + case "contentMediaType": + this.contentMediaType(node.expectStringNode().getValue()); + break; + default: + LOGGER.fine("Unknown property will be added to extensions: " + key); + this.putExtension(key, node); + break; + } + + return this; + } } } diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/node/DefaultNodeDeserializers.java b/smithy-model/src/main/java/software/amazon/smithy/model/node/DefaultNodeDeserializers.java index 553213f9515..cda85e0aec1 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/node/DefaultNodeDeserializers.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/node/DefaultNodeDeserializers.java @@ -126,6 +126,7 @@ static Class classFromType(Type type) { private static final Map> NUMBER_MAPPERS = new HashMap<>(); static { + NUMBER_MAPPERS.put(Number.class, n -> n); NUMBER_MAPPERS.put(Object.class, n -> n); NUMBER_MAPPERS.put(Byte.class, Number::byteValue); NUMBER_MAPPERS.put(byte.class, Number::byteValue); From e0ad91832411584faac627324f2a30531eb846b4 Mon Sep 17 00:00:00 2001 From: Hayden Baker Date: Wed, 13 Nov 2024 08:11:06 -0800 Subject: [PATCH 2/2] Address comments --- .../cloudformation/schema/model/Property.java | 31 +++++++++---------- .../schema/model/ResourceSchema.java | 5 +++ .../schema/fromsmithy/ResourceSchemaTest.java | 8 ++--- ...ain.json => aws-sagemaker-domain.cfn.json} | 26 ++++++++-------- 4 files changed, 35 insertions(+), 35 deletions(-) rename smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/{aws-sagemaker-domain.json => aws-sagemaker-domain.cfn.json} (98%) diff --git a/smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/model/Property.java b/smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/model/Property.java index 947ce3ef0ef..919f9f62dd8 100644 --- a/smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/model/Property.java +++ b/smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/model/Property.java @@ -16,7 +16,9 @@ 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; @@ -32,8 +34,6 @@ * @see Resource Type Properties JSON Schema */ public final class Property implements ToNode, ToSmithyBuilder { - private final boolean insertionOrder; - private final List dependencies; private final Schema schema; // Other reserved property names in definition but not in the validation // JSON Schema, so not defined in code: @@ -49,14 +49,12 @@ private Property(Builder builder) { schemaBuilder = builder.schema.toBuilder(); } - this.insertionOrder = builder.insertionOrder; - if (this.insertionOrder) { + if (builder.insertionOrder) { schemaBuilder.putExtension("insertionOrder", Node.from(true)); } - this.dependencies = builder.dependencies; - if (!this.dependencies.isEmpty()) { - schemaBuilder.putExtension("dependencies", Node.fromStrings(this.dependencies)); + if (!builder.dependencies.isEmpty()) { + schemaBuilder.putExtension("dependencies", Node.fromStrings(builder.dependencies)); } this.schema = schemaBuilder.build(); @@ -69,19 +67,12 @@ public Node toNode() { @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(); - - objectNode.getBooleanMember("insertionOrder", builder::insertionOrder); - objectNode.getArrayMember("dependencies", StringNode::getValue, builder::dependencies); - builder.schema(Schema.fromNode(objectNode)); return builder.build(); @@ -96,11 +87,17 @@ public static Builder builder() { } public boolean isInsertionOrder() { - return insertionOrder; + Optional insertionOrder = schema.getExtension("insertionOrder") + .map(n -> n.toNode().expectBooleanNode().getValue()); + + return insertionOrder.orElse(false); } public List getDependencies() { - return dependencies; + Optional> dependencies = schema.getExtension("dependencies") + .map(n -> n.toNode().expectArrayNode().getElementsAs(StringNode::getValue)); + + return dependencies.orElse(Collections.emptyList()); } public Schema getSchema() { diff --git a/smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/model/ResourceSchema.java b/smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/model/ResourceSchema.java index 7ae658ca61c..5268dfb98f9 100644 --- a/smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/model/ResourceSchema.java +++ b/smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/model/ResourceSchema.java @@ -166,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(); } diff --git a/smithy-aws-cloudformation/src/test/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/ResourceSchemaTest.java b/smithy-aws-cloudformation/src/test/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/ResourceSchemaTest.java index 8c87f7b3f46..ce5259be83c 100644 --- a/smithy-aws-cloudformation/src/test/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/ResourceSchemaTest.java +++ b/smithy-aws-cloudformation/src/test/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/ResourceSchemaTest.java @@ -19,7 +19,6 @@ 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.model.node.NodeMapper; import software.amazon.smithy.utils.IoUtils; import java.io.IOException; @@ -35,19 +34,18 @@ public class ResourceSchemaTest { @ParameterizedTest @MethodSource("resourceSchemaFiles") public void validateResourceSchemaFromNodeToNode(String resourceSchemaFile) { - NodeMapper mapper = new NodeMapper(); String json = IoUtils.readUtf8File(resourceSchemaFile); Node node = Node.parse(json); - ResourceSchema schemaFromNode = mapper.deserialize(node, ResourceSchema.class); + ResourceSchema schemaFromNode = ResourceSchema.fromNode(node); Node nodeFromSchema = schemaFromNode.toNode(); - Node.assertEquals(nodeFromSchema.withDeepSortedKeys(), node.withDeepSortedKeys()); + Node.assertEquals(nodeFromSchema, node); } public static List resourceSchemaFiles() { try { - Path definitionPath = Paths.get(ResourceSchemaTest.class.getResource("aws-sagemaker-domain.json").toURI()); + Path definitionPath = Paths.get(ResourceSchemaTest.class.getResource("aws-sagemaker-domain.cfn.json").toURI()); return Files.walk(Paths.get(definitionPath.getParent().toUri())) .filter(Files::isRegularFile) diff --git a/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/aws-sagemaker-domain.json b/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/aws-sagemaker-domain.cfn.json similarity index 98% rename from smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/aws-sagemaker-domain.json rename to smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/aws-sagemaker-domain.cfn.json index 9581e2d3d3a..cdb1e347fc9 100644 --- a/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/aws-sagemaker-domain.json +++ b/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/aws-sagemaker-domain.cfn.json @@ -499,8 +499,8 @@ "/properties/DomainSettings/RStudioServerProDomainSettings/DefaultResourceSpec", "/properties/KmsKeyId", "/properties/SubnetIds", - "/properties/VpcId", - "/properties/Tags" + "/properties/Tags", + "/properties/VpcId" ], "writeOnlyProperties": [ "/properties/Tags" @@ -510,27 +510,27 @@ ], "readOnlyProperties": [ "/properties/DomainArn", - "/properties/Url", "/properties/DomainId", "/properties/HomeEfsFileSystemId", "/properties/SecurityGroupIdForDomainBoundary", - "/properties/SingleSignOnManagedApplicationInstanceId" + "/properties/SingleSignOnManagedApplicationInstanceId", + "/properties/Url" ], "handlers": { "create": { "permissions": [ - "sagemaker:CreateApp", - "sagemaker:CreateDomain", - "sagemaker:DescribeDomain", - "sagemaker:DescribeImage", - "sagemaker:DescribeImageVersion", + "efs:CreateFileSystem", "iam:CreateServiceLinkedRole", "iam:PassRole", - "efs:CreateFileSystem", "kms:CreateGrant", "kms:Decrypt", "kms:DescribeKey", - "kms:GenerateDataKeyWithoutPlainText" + "kms:GenerateDataKeyWithoutPlainText", + "sagemaker:CreateApp", + "sagemaker:CreateDomain", + "sagemaker:DescribeDomain", + "sagemaker:DescribeImage", + "sagemaker:DescribeImageVersion" ] }, "read": { @@ -540,12 +540,12 @@ }, "update": { "permissions": [ + "iam:PassRole", "sagemaker:CreateApp", - "sagemaker:UpdateDomain", "sagemaker:DescribeDomain", "sagemaker:DescribeImage", "sagemaker:DescribeImageVersion", - "iam:PassRole" + "sagemaker:UpdateDomain" ] }, "delete": {