diff --git a/Examples/SAM/Deploy.swift b/Examples/SAM/Deploy.swift index ecb9aa9..637cdf7 100644 --- a/Examples/SAM/Deploy.swift +++ b/Examples/SAM/Deploy.swift @@ -2,111 +2,108 @@ import AWSLambdaDeploymentDescriptor // example of a shared resource let sharedQueue = Queue( - logicalName: "SharedQueue", - physicalName: "swift-lambda-shared-queue") + logicalName: "SharedQueue", + physicalName: "swift-lambda-shared-queue" +) // example of common environment variables let sharedEnvironmentVariables = ["LOG_LEVEL": "debug"] let validEfsArn = - "arn:aws:elasticfilesystem:eu-central-1:012345678901:access-point/fsap-abcdef01234567890" + "arn:aws:elasticfilesystem:eu-central-1:012345678901:access-point/fsap-abcdef01234567890" // the deployment descriptor DeploymentDescriptor { - - // an optional description - "Description of this deployment descriptor" - - // Create a lambda function exposed through a REST API - Function(name: "HttpApiLambda") { - // an optional description - "Description of this function" + "Description of this deployment descriptor" + + // Create a lambda function exposed through a REST API + Function(name: "HttpApiLambda") { + // an optional description + "Description of this function" + + EventSources { + // example of a catch all api + HttpApi() + + // example of an API for a specific HTTP verb and path + // HttpApi(method: .GET, path: "/test") + } + + EnvironmentVariables { + [ + "NAME1": "VALUE1", + "NAME2": "VALUE2", + ] + + // shared environment variables declared upfront + sharedEnvironmentVariables + } + } - EventSources { + // Example Function modifiers: + + // .autoPublishAlias() + // .ephemeralStorage(2048) + // .eventInvoke(onSuccess: "arn:aws:sqs:eu-central-1:012345678901:lambda-test", + // onFailure: "arn:aws:lambda:eu-central-1:012345678901:lambda-test", + // maximumEventAgeInSeconds: 600, + // maximumRetryAttempts: 3) + // .fileSystem(validEfsArn, mountPoint: "/mnt/path1") + // .fileSystem(validEfsArn, mountPoint: "/mnt/path2") + + // Create a Lambda function exposed through an URL + // you can invoke it with a signed request, for example + // curl --aws-sigv4 "aws:amz:eu-central-1:lambda" \ + // --user $AWS_ACCESS_KEY_ID:$AWS_SECRET_ACCESS_KEY \ + // -H 'content-type: application/json' \ + // -d '{ "example": "test" }' \ + // "$FUNCTION_URL?param1=value1¶m2=value2" + Function(name: "UrlLambda") { + "A Lambda function that is directly exposed as an URL, with IAM authentication" + } + .urlConfig(authType: .iam) - // example of a catch all api - HttpApi() + // Create a Lambda function triggered by messages on SQS + Function(name: "SQSLambda", architecture: .arm64) { + EventSources { + // this will reference an existing queue by its Arn + // Sqs("arn:aws:sqs:eu-central-1:012345678901:swift-lambda-shared-queue") - // example of an API for a specific HTTP verb and path - // HttpApi(method: .GET, path: "/test") + // // this will create a new queue resource + Sqs("swift-lambda-queue-name") - } + // // this will create a new queue resource, with control over physical queue name + // Sqs() + // .queue(logicalName: "LambdaQueueResource", physicalName: "swift-lambda-queue-resource") - EnvironmentVariables { - [ - "NAME1": "VALUE1", - "NAME2": "VALUE2", - ] + // // this references a shared queue resource created at the top of this deployment descriptor + // // the queue resource will be created automatically, you do not need to add `sharedQueue` as a resource + // Sqs(sharedQueue) + } - // shared environment variables declared upfront - sharedEnvironmentVariables - } - } - - // Example Function modifiers: - - // .autoPublishAlias() - // .ephemeralStorage(2048) - // .eventInvoke(onSuccess: "arn:aws:sqs:eu-central-1:012345678901:lambda-test", - // onFailure: "arn:aws:lambda:eu-central-1:012345678901:lambda-test", - // maximumEventAgeInSeconds: 600, - // maximumRetryAttempts: 3) - // .fileSystem(validEfsArn, mountPoint: "/mnt/path1") - // .fileSystem(validEfsArn, mountPoint: "/mnt/path2") - - // Create a Lambda function exposed through an URL - // you can invoke it with a signed request, for example - // curl --aws-sigv4 "aws:amz:eu-central-1:lambda" \ - // --user $AWS_ACCESS_KEY_ID:$AWS_SECRET_ACCESS_KEY \ - // -H 'content-type: application/json' \ - // -d '{ "example": "test" }' \ - // "$FUNCTION_URL?param1=value1¶m2=value2" - Function(name: "UrlLambda") { - "A Lambda function that is directly exposed as an URL, with IAM authentication" - } - .urlConfig(authType: .iam) - - // Create a Lambda function triggered by messages on SQS - Function(name: "SQSLambda", architecture: .arm64) { - - EventSources { - - // this will reference an existing queue by its Arn - // Sqs("arn:aws:sqs:eu-central-1:012345678901:swift-lambda-shared-queue") - - // // this will create a new queue resource - Sqs("swift-lambda-queue-name") - - // // this will create a new queue resource, with control over physical queue name - // Sqs() - // .queue(logicalName: "LambdaQueueResource", physicalName: "swift-lambda-queue-resource") - - // // this references a shared queue resource created at the top of this deployment descriptor - // // the queue resource will be created automatically, you do not need to add `sharedQueue` as a resource - // Sqs(sharedQueue) + EnvironmentVariables { + sharedEnvironmentVariables + } } - EnvironmentVariables { - sharedEnvironmentVariables - } - } - - // - // Additional resources - // - // Create a SQS queue - Queue( - logicalName: "TopLevelQueueResource", - physicalName: "swift-lambda-top-level-queue") - - // Create a DynamoDB table - Table( - logicalName: "SwiftLambdaTable", - physicalName: "swift-lambda-table", - primaryKeyName: "id", - primaryKeyType: "String") - - // example modifiers - // .provisionedThroughput(readCapacityUnits: 10, writeCapacityUnits: 99) + // + // Additional resources + // + // Create a SQS queue + Queue( + logicalName: "TopLevelQueueResource", + physicalName: "swift-lambda-top-level-queue" + ) + + // Create a DynamoDB table + Table( + logicalName: "SwiftLambdaTable", + physicalName: "swift-lambda-table", + primaryKeyName: "id", + primaryKeyType: "String" + ) + + // example modifiers + // .provisionedThroughput(readCapacityUnits: 10, writeCapacityUnits: 99) } diff --git a/Examples/SAM/HttpApiLambda/Lambda.swift b/Examples/SAM/HttpApiLambda/Lambda.swift index e120fa8..6255ec7 100644 --- a/Examples/SAM/HttpApiLambda/Lambda.swift +++ b/Examples/SAM/HttpApiLambda/Lambda.swift @@ -21,12 +21,11 @@ struct HttpApiLambda: LambdaHandler { init() {} init(context: LambdaInitializationContext) async throws { context.logger.info( - "Log Level env var : \(ProcessInfo.processInfo.environment["LOG_LEVEL"] ?? "info" )") + "Log Level env var : \(ProcessInfo.processInfo.environment["LOG_LEVEL"] ?? "info")") } // the return value must be either APIGatewayV2Response or any Encodable struct func handle(_ event: APIGatewayV2Request, context: AWSLambdaRuntimeCore.LambdaContext) async throws -> APIGatewayV2Response { - var header = HTTPHeaders() do { context.logger.debug("HTTP API Message received") @@ -46,7 +45,6 @@ struct HttpApiLambda: LambdaHandler { // when the input event is malformed, this function is not even called header["content-type"] = "text/plain" return APIGatewayV2Response(statusCode: .badRequest, headers: header, body: "\(error.localizedDescription)") - } } } diff --git a/Examples/SAM/Package.swift b/Examples/SAM/Package.swift index caca1c4..7467c7e 100644 --- a/Examples/SAM/Package.swift +++ b/Examples/SAM/Package.swift @@ -18,57 +18,57 @@ import class Foundation.ProcessInfo // needed for CI to test the local version o import PackageDescription let package = Package( - name: "swift-aws-lambda-runtime-example", - platforms: [ - .macOS(.v12) - ], - products: [ - .executable(name: "HttpApiLambda", targets: ["HttpApiLambda"]), - .executable(name: "SQSLambda", targets: ["SQSLambda"]), - .executable(name: "UrlLambda", targets: ["UrlLambda"]) - ], - dependencies: [ - .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", branch: "main"), - .package(url: "https://github.com/swift-server/swift-aws-lambda-events.git", branch: "main") - ], - targets: [ - .executableTarget( - name: "HttpApiLambda", - dependencies: [ - .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"), - .product(name: "AWSLambdaEvents", package: "swift-aws-lambda-events") - ], - path: "./HttpApiLambda" - ), - .executableTarget( - name: "UrlLambda", - dependencies: [ - .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"), - .product(name: "AWSLambdaEvents", package: "swift-aws-lambda-events") - ], - path: "./UrlLambda" - ), - .executableTarget( - name: "SQSLambda", - dependencies: [ - .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"), - .product(name: "AWSLambdaEvents", package: "swift-aws-lambda-events") - ], - path: "./SQSLambda" - ), - .testTarget( - name: "LambdaTests", - dependencies: [ - "HttpApiLambda", "SQSLambda", - .product(name: "AWSLambdaTesting", package: "swift-aws-lambda-runtime"), - ], - // testing data - resources: [ - .process("data/apiv2.json"), - .process("data/sqs.json") - ] - ) - ] + name: "swift-aws-lambda-runtime-example", + platforms: [ + .macOS(.v12), + ], + products: [ + .executable(name: "HttpApiLambda", targets: ["HttpApiLambda"]), + .executable(name: "SQSLambda", targets: ["SQSLambda"]), + .executable(name: "UrlLambda", targets: ["UrlLambda"]), + ], + dependencies: [ + .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", branch: "main"), + .package(url: "https://github.com/swift-server/swift-aws-lambda-events.git", branch: "main"), + ], + targets: [ + .executableTarget( + name: "HttpApiLambda", + dependencies: [ + .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"), + .product(name: "AWSLambdaEvents", package: "swift-aws-lambda-events"), + ], + path: "./HttpApiLambda" + ), + .executableTarget( + name: "UrlLambda", + dependencies: [ + .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"), + .product(name: "AWSLambdaEvents", package: "swift-aws-lambda-events"), + ], + path: "./UrlLambda" + ), + .executableTarget( + name: "SQSLambda", + dependencies: [ + .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"), + .product(name: "AWSLambdaEvents", package: "swift-aws-lambda-events"), + ], + path: "./SQSLambda" + ), + .testTarget( + name: "LambdaTests", + dependencies: [ + "HttpApiLambda", "SQSLambda", + .product(name: "AWSLambdaTesting", package: "swift-aws-lambda-runtime"), + ], + // testing data + resources: [ + .process("data/apiv2.json"), + .process("data/sqs.json"), + ] + ), + ] ) // for CI to test the local version of the library @@ -76,6 +76,6 @@ if ProcessInfo.processInfo.environment["LAMBDA_USE_LOCAL_DEPS"] != nil { package.dependencies = [ .package(name: "swift-aws-lambda-runtime", path: "../.."), // .package(url: "../../../swift-aws-lambda-runtime", branch: "sebsto/use_local_deps"), // to have the LAMBDA_USE_LOCAL_DEPS env var on plugin archive (temp until https://github.com/swift-server/swift-aws-lambda-runtime/pull/325 is merged) - .package(url: "https://github.com/swift-server/swift-aws-lambda-events.git", branch: "main") + .package(url: "https://github.com/swift-server/swift-aws-lambda-events.git", branch: "main"), ] -} \ No newline at end of file +} diff --git a/Examples/SAM/SQSLambda/Lambda.swift b/Examples/SAM/SQSLambda/Lambda.swift index 6e39a41..0bf5aec 100644 --- a/Examples/SAM/SQSLambda/Lambda.swift +++ b/Examples/SAM/SQSLambda/Lambda.swift @@ -24,12 +24,11 @@ struct SQSLambda: LambdaHandler { init() {} init(context: LambdaInitializationContext) async throws { context.logger.info( - "Log Level env var : \(ProcessInfo.processInfo.environment["LOG_LEVEL"] ?? "info" )") + "Log Level env var : \(ProcessInfo.processInfo.environment["LOG_LEVEL"] ?? "info")") } func handle(_ event: Event, context: AWSLambdaRuntimeCore.LambdaContext) async throws -> Output { - - context.logger.info("Log Level env var : \(ProcessInfo.processInfo.environment["LOG_LEVEL"] ?? "not defined" )" ) + context.logger.info("Log Level env var : \(ProcessInfo.processInfo.environment["LOG_LEVEL"] ?? "not defined")") context.logger.debug("SQS Message received, with \(event.records.count) record") for msg in event.records { diff --git a/Examples/SAM/Tests/LambdaTests/HttpApiLambdaTest.swift b/Examples/SAM/Tests/LambdaTests/HttpApiLambdaTest.swift index 590df2d..4efcd68 100644 --- a/Examples/SAM/Tests/LambdaTests/HttpApiLambdaTest.swift +++ b/Examples/SAM/Tests/LambdaTests/HttpApiLambdaTest.swift @@ -15,32 +15,30 @@ import AWSLambdaEvents import AWSLambdaRuntime import AWSLambdaTesting -import XCTest @testable import HttpApiLambda +import XCTest class HttpApiLambdaTests: LambdaTest { - func testHttpAPiLambda() async throws { + // given + let eventData = try self.loadTestData(file: .apiGatewayV2) + let event = try JSONDecoder().decode(APIGatewayV2Request.self, from: eventData) - // given - let eventData = try self.loadTestData(file: .apiGatewayV2) - let event = try JSONDecoder().decode(APIGatewayV2Request.self, from: eventData) - - do { - // when - let result = try await Lambda.test(HttpApiLambda.self, with: event) + do { + // when + let result = try await Lambda.test(HttpApiLambda.self, with: event) - // then - XCTAssertEqual(result.statusCode.code, 200) - XCTAssertNotNil(result.headers) - if let headers = result.headers { - XCTAssertNotNil(headers["content-type"]) - if let contentType = headers["content-type"] { - XCTAssertTrue(contentType == "application/json") - } + // then + XCTAssertEqual(result.statusCode.code, 200) + XCTAssertNotNil(result.headers) + if let headers = result.headers { + XCTAssertNotNil(headers["content-type"]) + if let contentType = headers["content-type"] { + XCTAssertTrue(contentType == "application/json") } - } catch { - XCTFail("Lambda invocation should not throw error : \(error)") } + } catch { + XCTFail("Lambda invocation should not throw error : \(error)") } + } } diff --git a/Examples/SAM/Tests/LambdaTests/LambdaTest.swift b/Examples/SAM/Tests/LambdaTests/LambdaTest.swift index dff1ce5..b6cf749 100644 --- a/Examples/SAM/Tests/LambdaTests/LambdaTest.swift +++ b/Examples/SAM/Tests/LambdaTests/LambdaTest.swift @@ -3,7 +3,7 @@ import XCTest enum TestData: String { case apiGatewayV2 = "apiv2" - case sqs = "sqs" + case sqs } class LambdaTest: XCTestCase { @@ -17,6 +17,6 @@ class LambdaTest: XCTestCase { // load a test file added as a resource to the executable bundle func loadTestData(file: TestData) throws -> Data { // load list from file - return try Data(contentsOf: urlForTestData(file: file)) + try Data(contentsOf: self.urlForTestData(file: file)) } } diff --git a/Examples/SAM/Tests/LambdaTests/SQSLambdaTest.swift b/Examples/SAM/Tests/LambdaTests/SQSLambdaTest.swift index e28b407..361d466 100644 --- a/Examples/SAM/Tests/LambdaTests/SQSLambdaTest.swift +++ b/Examples/SAM/Tests/LambdaTests/SQSLambdaTest.swift @@ -15,26 +15,23 @@ import AWSLambdaEvents import AWSLambdaRuntime import AWSLambdaTesting -import XCTest @testable import SQSLambda +import XCTest class SQSLambdaTests: LambdaTest { - func testSQSLambda() async throws { + // given + let eventData = try self.loadTestData(file: .sqs) + let event = try JSONDecoder().decode(SQSEvent.self, from: eventData) - // given - let eventData = try self.loadTestData(file: .sqs) - let event = try JSONDecoder().decode(SQSEvent.self, from: eventData) - - // when - do { - try await Lambda.test(SQSLambda.self, with: event) - } catch { - XCTFail("Lambda invocation should not throw error : \(error)") - } - - // then - // SQS Lambda returns Void - + // when + do { + try await Lambda.test(SQSLambda.self, with: event) + } catch { + XCTFail("Lambda invocation should not throw error : \(error)") } + + // then + // SQS Lambda returns Void + } } diff --git a/Examples/SAM/UrlLambda/Lambda.swift b/Examples/SAM/UrlLambda/Lambda.swift index 7587ce7..2385af0 100644 --- a/Examples/SAM/UrlLambda/Lambda.swift +++ b/Examples/SAM/UrlLambda/Lambda.swift @@ -18,38 +18,36 @@ import Foundation @main struct UrlLambda: LambdaHandler { - init() {} - init(context: LambdaInitializationContext) async throws { - context.logger.info( - "Log Level env var : \(ProcessInfo.processInfo.environment["LOG_LEVEL"] ?? "info" )") - } - - // the return value must be either APIGatewayV2Response or any Encodable struct - func handle(_ event: FunctionURLRequest, context: AWSLambdaRuntimeCore.LambdaContext) async throws - -> FunctionURLResponse - { - - var header = HTTPHeaders() - do { - context.logger.debug("HTTP API Message received") - - header["content-type"] = "application/json" - - // echo the request in the response - let data = try JSONEncoder().encode(event) - let response = String(decoding: data, as: UTF8.self) - - // if you want control on the status code and headers, return an APIGatewayV2Response - // otherwise, just return any Encodable struct, the runtime will wrap it for you - return FunctionURLResponse(statusCode: .ok, headers: header, body: response) - - } catch { - // should never happen as the decoding was made by the runtime - // when the input event is malformed, this function is not even called - header["content-type"] = "text/plain" - return FunctionURLResponse( - statusCode: .badRequest, headers: header, body: "\(error.localizedDescription)") + init() {} + init(context: LambdaInitializationContext) async throws { + context.logger.info( + "Log Level env var : \(ProcessInfo.processInfo.environment["LOG_LEVEL"] ?? "info")") + } + // the return value must be either APIGatewayV2Response or any Encodable struct + func handle(_ event: FunctionURLRequest, context: AWSLambdaRuntimeCore.LambdaContext) async throws + -> FunctionURLResponse { + var header = HTTPHeaders() + do { + context.logger.debug("HTTP API Message received") + + header["content-type"] = "application/json" + + // echo the request in the response + let data = try JSONEncoder().encode(event) + let response = String(decoding: data, as: UTF8.self) + + // if you want control on the status code and headers, return an APIGatewayV2Response + // otherwise, just return any Encodable struct, the runtime will wrap it for you + return FunctionURLResponse(statusCode: .ok, headers: header, body: response) + + } catch { + // should never happen as the decoding was made by the runtime + // when the input event is malformed, this function is not even called + header["content-type"] = "text/plain" + return FunctionURLResponse( + statusCode: .badRequest, headers: header, body: "\(error.localizedDescription)" + ) + } } - } } diff --git a/Package.swift b/Package.swift index 487804b..8b27de6 100644 --- a/Package.swift +++ b/Package.swift @@ -5,26 +5,59 @@ import PackageDescription let package = Package( name: "swift-aws-lambda-sam-dsl", platforms: [ - .macOS(.v14), + .macOS(.v12), ], products: [ // SwiftPM plugin to deploy a SAM Lambda function .plugin(name: "AWSLambdaDeployer", targets: ["AWSLambdaDeployer"]), + .executable(name: "DeploymentDescriptorGeneratorExecutable", + targets: ["DeploymentDescriptorGeneratorExecutable"]), + + // SwiftPM plugin to generate a SAM deployment descriptor + .plugin(name: "AWSLambdaDescriptorGenerator", targets: ["AWSLambdaDescriptorGenerator"]), + // Shared Library to generate a SAM deployment descriptor .library(name: "AWSLambdaDeploymentDescriptor", type: .dynamic, targets: ["AWSLambdaDeploymentDescriptor"]), ], dependencies: [ + .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.0.0"), + .package(url: "https://github.com/apple/swift-log.git", .upToNextMajor(from: "1.4.2")), + .package(url: "https://github.com/hummingbird-project/hummingbird-mustache.git", from: "1.0.3"), + .package(url: "https://github.com/apple/swift-syntax.git", from: "510.0.2"), + .package(url: "https://github.com/apple/swift-openapi-generator", from: "1.0.0"), + .package(url: "https://github.com/apple/swift-openapi-runtime", from: "1.0.0"), + .package(url: "https://github.com/apple/swift-openapi-urlsession", from: "1.0.0"), ], targets: [ .target( name: "AWSLambdaDeploymentDescriptor", path: "Sources/AWSLambdaDeploymentDescriptor" ), - // SAM Deployment Descriptor Generator - .target( - name: "AWSLambdaDeploymentDescriptorGenerator", - path: "Sources/AWSLambdaDeploymentDescriptorGenerator" + .executableTarget( + name: "DeploymentDescriptorGeneratorExecutable", + dependencies: [ + .byName(name: "AWSLambdaDeploymentDescriptorGenerator"), + .product(name: "ArgumentParser", package: "swift-argument-parser"), + .product(name: "Logging", package: "swift-log"), + ] + ), + .target(name: "AWSLambdaDeploymentDescriptorGenerator", dependencies: [ + .product(name: "Logging", package: "swift-log"), + .product(name: "ArgumentParser", package: "swift-argument-parser"), + .product(name: "HummingbirdMustache", package: "hummingbird-mustache"), + .product(name: "SwiftSyntax", package: "swift-syntax"), + .product(name: "SwiftSyntaxBuilder", package: "swift-syntax"), + .product(name: "OpenAPIRuntime", package: "swift-openapi-runtime"), + .product(name: "OpenAPIURLSession", package: "swift-openapi-urlsession"), + ], + resources: [ + .process("Resources/SamTranslatorSchema.json"), + .process("Resources/TypeSchemaTranslator.json"), + .process("Resources/openapi.yaml"), + .process("Resources/openapi-generator-config.yaml"), + ], + plugins: [.plugin(name: "OpenAPIGenerator", package: "swift-openapi-generator")] ), .plugin( name: "AWSLambdaDeployer", @@ -36,6 +69,12 @@ let package = Package( // permissions: [.writeToPackageDirectory(reason: "This plugin generates a SAM template to describe your deployment")] ) ), + + .plugin( + name: "AWSLambdaDescriptorGenerator", + capability: .buildTool(), + dependencies: ["DeploymentDescriptorGeneratorExecutable"] + ), .testTarget( name: "AWSLambdaDeploymentDescriptorTests", dependencies: [ @@ -50,7 +89,9 @@ let package = Package( // https://stackoverflow.com/questions/47177036/use-resources-in-unit-tests-with-swift-package-manager resources: [ .copy("Resources/SimpleJSONSchema.json"), - .copy("Resources/SAMJSONSchema.json")] + .copy("Resources/SAMJSONSchema.json"), + .copy("Resources/TypeSchemaTranslator.json"), + ] ), ] ) diff --git a/Plugins/AWSLambdaDeployer/Plugin.swift b/Plugins/AWSLambdaDeployer/Plugin.swift index 8ec8f52..0838da3 100644 --- a/Plugins/AWSLambdaDeployer/Plugin.swift +++ b/Plugins/AWSLambdaDeployer/Plugin.swift @@ -19,40 +19,39 @@ import PackagePlugin @main struct AWSLambdaDeployer: CommandPlugin { func performCommand(context: PackagePlugin.PluginContext, arguments: [String]) async throws { - let configuration = try Configuration(context: context, arguments: arguments) if configuration.help { - displayHelpMessage() + self.displayHelpMessage() return } - + // gather file paths let samDeploymentDescriptorFilePath = "\(context.package.directory)/template.yaml" - + let swiftExecutablePath = try self.findExecutable(context: context, executableName: "swift", helpMessage: "Is Swift or Xcode installed? (https://www.swift.org/getting-started)", verboseLogging: configuration.verboseLogging) - + let samExecutablePath = try self.findExecutable(context: context, executableName: "sam", helpMessage: "SAM command line is required. (brew tap aws/tap && brew install aws-sam-cli)", verboseLogging: configuration.verboseLogging) - + let shellExecutablePath = try self.findExecutable(context: context, executableName: "sh", helpMessage: "The default shell (/bin/sh) is required to run this plugin", verboseLogging: configuration.verboseLogging) - + let awsRegion = try self.getDefaultAWSRegion(context: context, regionFromCommandLine: configuration.region, verboseLogging: configuration.verboseLogging) - + // build the shared lib to compile the deployment descriptor try self.compileSharedLibrary(projectDirectory: context.package.directory, - buildConfiguration: configuration.buildConfiguration, - swiftExecutable: swiftExecutablePath, - verboseLogging: configuration.verboseLogging) + buildConfiguration: configuration.buildConfiguration, + swiftExecutable: swiftExecutablePath, + verboseLogging: configuration.verboseLogging) // generate the deployment descriptor try self.generateDeploymentDescriptor(projectDirectory: context.package.directory, @@ -63,8 +62,7 @@ struct AWSLambdaDeployer: CommandPlugin { archivePath: configuration.archiveDirectory, force: configuration.force, verboseLogging: configuration.verboseLogging) - - + // check if there is a samconfig.toml file. // when there is no file, generate one with default values and values collected from params try self.checkOrCreateSAMConfigFile(projetDirectory: context.package.directory, @@ -73,12 +71,11 @@ struct AWSLambdaDeployer: CommandPlugin { stackName: configuration.stackName, force: configuration.force, verboseLogging: configuration.verboseLogging) - + // validate the template try self.validate(samExecutablePath: samExecutablePath, samDeploymentDescriptorFilePath: samDeploymentDescriptorFilePath, verboseLogging: configuration.verboseLogging) - // deploy the functions if !configuration.noDeploy { @@ -86,12 +83,12 @@ struct AWSLambdaDeployer: CommandPlugin { buildConfiguration: configuration.buildConfiguration, verboseLogging: configuration.verboseLogging) } - + // list endpoints if !configuration.noList { let output = try self.listEndpoints(samExecutablePath: samExecutablePath, samDeploymentDescriptorFilePath: samDeploymentDescriptorFilePath, - stackName : configuration.stackName, + stackName: configuration.stackName, verboseLogging: configuration.verboseLogging) print(output) } @@ -105,15 +102,14 @@ struct AWSLambdaDeployer: CommandPlugin { print("Compile shared library") print("-------------------------------------------------------------------------") - let cmd = [ "swift", "build", - "-c", buildConfiguration.rawValue, - "--product", "AWSLambdaDeploymentDescriptor"] + let cmd = ["swift", "build", + "-c", buildConfiguration.rawValue, + "--product", "AWSLambdaDeploymentDescriptor"] try Utils.execute(executable: swiftExecutable, arguments: Array(cmd.dropFirst()), customWorkingDirectory: projectDirectory, logLevel: verboseLogging ? .debug : .silent) - } private func generateDeploymentDescriptor(projectDirectory: Path, @@ -127,7 +123,7 @@ struct AWSLambdaDeployer: CommandPlugin { print("-------------------------------------------------------------------------") print("Generating SAM deployment descriptor") print("-------------------------------------------------------------------------") - + // // Build and run the Deploy.swift package description // this generates the SAM deployment descriptor @@ -135,39 +131,39 @@ struct AWSLambdaDeployer: CommandPlugin { let deploymentDescriptorFileName = "Deploy.swift" let deploymentDescriptorFilePath = "\(projectDirectory)/\(deploymentDescriptorFileName)" let sharedLibraryName = "AWSLambdaDeploymentDescriptor" // provided by the swift lambda runtime - + // Check if Deploy.swift exists. Stop when it does not exist. guard FileManager.default.fileExists(atPath: deploymentDescriptorFilePath) else { print("`Deploy.Swift` file not found in directory \(projectDirectory)") throw DeployerPluginError.deployswiftDoesNotExist } - + do { var cmd = [ "\"\(swiftExecutable.string)\"", "-L \(projectDirectory)/.build/\(buildConfiguration)/", "-I \(projectDirectory)/.build/\(buildConfiguration)/", "-l\(sharedLibraryName)", - "\"\(deploymentDescriptorFilePath)\"" + "\"\(deploymentDescriptorFilePath)\"", ] if let archive = archivePath { cmd = cmd + ["--archive-path", archive] } let helperCmd = cmd.joined(separator: " \\\n") - + if verboseLogging { print("-------------------------------------------------------------------------") print("Swift compile and run Deploy.swift") print("-------------------------------------------------------------------------") print("Swift command:\n\n\(helperCmd)\n") } - + // create and execute a plugin helper to run the "swift" command let helperFilePath = "\(FileManager.default.temporaryDirectory.path)/compile.sh" FileManager.default.createFile(atPath: helperFilePath, contents: helperCmd.data(using: .utf8), attributes: [.posixPermissions: 0o755]) - defer { try? FileManager.default.removeItem(atPath: helperFilePath) } + defer { try? FileManager.default.removeItem(atPath: helperFilePath) } // running the swift command directly from the plugin does not work 🤷‍♂️ // the below launches a bash shell script that will launch the `swift` command @@ -175,27 +171,26 @@ struct AWSLambdaDeployer: CommandPlugin { executable: shellExecutable, arguments: ["-c", helperFilePath], customWorkingDirectory: projectDirectory, - logLevel: verboseLogging ? .debug : .silent) - // let samDeploymentDescriptor = try Utils.execute( - // executable: swiftExecutable, - // arguments: Array(cmd.dropFirst()), - // customWorkingDirectory: projectDirectory, - // logLevel: verboseLogging ? .debug : .silent) - + logLevel: verboseLogging ? .debug : .silent + ) + // let samDeploymentDescriptor = try Utils.execute( + // executable: swiftExecutable, + // arguments: Array(cmd.dropFirst()), + // customWorkingDirectory: projectDirectory, + // logLevel: verboseLogging ? .debug : .silent) + // write the generated SAM deployment descriptor to disk if FileManager.default.fileExists(atPath: samDeploymentDescriptorFilePath) && !force { - print("SAM deployment descriptor already exists at") print("\(samDeploymentDescriptorFilePath)") print("use --force option to overwrite it.") - + } else { - FileManager.default.createFile(atPath: samDeploymentDescriptorFilePath, contents: samDeploymentDescriptor.data(using: .utf8)) verboseLogging ? print("Writing file at \(samDeploymentDescriptorFilePath)") : nil } - + } catch let error as DeployerPluginError { print("Error while compiling Deploy.swift") print(error) @@ -205,20 +200,18 @@ struct AWSLambdaDeployer: CommandPlugin { print("Unexpected error : \(error)") throw DeployerPluginError.error(error) } - } - + private func findExecutable(context: PluginContext, executableName: String, helpMessage: String, verboseLogging: Bool) throws -> Path { - guard let executable = try? context.tool(named: executableName) else { print("Can not find `\(executableName)` executable.") print(helpMessage) throw DeployerPluginError.toolNotFound(executableName) } - + if verboseLogging { print("-------------------------------------------------------------------------") print("\(executableName) executable : \(executable.path)") @@ -226,23 +219,23 @@ struct AWSLambdaDeployer: CommandPlugin { } return executable.path } - + private func validate(samExecutablePath: Path, samDeploymentDescriptorFilePath: String, verboseLogging: Bool) throws { - print("-------------------------------------------------------------------------") print("Validating SAM deployment descriptor") print("-------------------------------------------------------------------------") - + do { try Utils.execute( executable: samExecutablePath, arguments: ["validate", "-t", samDeploymentDescriptorFilePath, "--lint"], - logLevel: verboseLogging ? .debug : .silent) - + logLevel: verboseLogging ? .debug : .silent + ) + } catch let error as DeployerPluginError { print("Error while validating the SAM template.") print(error) @@ -253,56 +246,51 @@ struct AWSLambdaDeployer: CommandPlugin { throw DeployerPluginError.error(error) } } - + private func checkOrCreateSAMConfigFile(projetDirectory: Path, buildConfiguration: PackageManager.BuildConfiguration, region: String, stackName: String, force: Bool, verboseLogging: Bool) throws { - let samConfigFilePath = "\(projetDirectory)/samconfig.toml" // the default value for SAM let samConfigTemplate = """ -version = 0.1 -[\(buildConfiguration)] -[\(buildConfiguration).deploy] -[\(buildConfiguration).deploy.parameters] -stack_name = "\(stackName)" -region = "\(region)" -capabilities = "CAPABILITY_IAM" -image_repositories = [] -""" - if FileManager.default.fileExists(atPath: samConfigFilePath) && !force { - + version = 0.1 + [\(buildConfiguration)] + [\(buildConfiguration).deploy] + [\(buildConfiguration).deploy.parameters] + stack_name = "\(stackName)" + region = "\(region)" + capabilities = "CAPABILITY_IAM" + image_repositories = [] + """ + if FileManager.default.fileExists(atPath: samConfigFilePath) && !force { print("SAM configuration file already exists at") print("\(samConfigFilePath)") print("use --force option to overwrite it.") - + } else { - // when SAM config does not exist, create it, it will allow function developers to customize and reuse it FileManager.default.createFile(atPath: samConfigFilePath, contents: samConfigTemplate.data(using: .utf8)) verboseLogging ? print("Writing file at \(samConfigFilePath)") : nil - } } - + private func deploy(samExecutablePath: Path, buildConfiguration: PackageManager.BuildConfiguration, verboseLogging: Bool) throws { - print("-------------------------------------------------------------------------") print("Deploying AWS Lambda function") print("-------------------------------------------------------------------------") do { - try Utils.execute( executable: samExecutablePath, arguments: ["deploy", "--config-env", buildConfiguration.rawValue, "--resolve-s3"], - logLevel: verboseLogging ? .debug : .silent) + logLevel: verboseLogging ? .debug : .silent + ) } catch let error as DeployerPluginError { print("Error while deploying the SAM template.") print(error) @@ -322,30 +310,28 @@ image_repositories = [] throw DeployerPluginError.error(error) } } - + private func listEndpoints(samExecutablePath: Path, samDeploymentDescriptorFilePath: String, stackName: String, - verboseLogging: Bool) throws -> String { - + verboseLogging: Bool) throws -> String { print("-------------------------------------------------------------------------") print("Listing AWS endpoints") print("-------------------------------------------------------------------------") do { - return try Utils.execute( executable: samExecutablePath, arguments: ["list", "endpoints", "-t", samDeploymentDescriptorFilePath, "--stack-name", stackName, "--output", "json"], - logLevel: verboseLogging ? .debug : .silent) + logLevel: verboseLogging ? .debug : .silent + ) } catch { print("Unexpected error : \(error)") throw DeployerPluginError.error(error) } } - /// provides a region name where to deploy /// first check for the region provided as a command line param to the plugin @@ -354,16 +340,15 @@ image_repositories = [] private func getDefaultAWSRegion(context: PluginContext, regionFromCommandLine: String?, verboseLogging: Bool) throws -> String { - let helpMsg = """ - Search order : 1. [--region] plugin parameter, - 2. AWS_DEFAULT_REGION environment variable, - 3. [default] profile from AWS CLI (~/.aws/config) -""" + Search order : 1. [--region] plugin parameter, + 2. AWS_DEFAULT_REGION environment variable, + 3. [default] profile from AWS CLI (~/.aws/config) + """ // first check the --region plugin command line var result: String? = regionFromCommandLine - + guard result == nil else { print("AWS Region : \(result!) (from command line)") return result! @@ -380,11 +365,10 @@ image_repositories = [] // third, check from AWS CLI configuration when it is available // aws cli is optional. It is used as last resort to identify the default AWS Region - if let awsCLIPath = try? self.findExecutable(context: context, - executableName: "aws", - helpMessage: "aws command line is used to find default AWS region. (brew install awscli)", - verboseLogging: verboseLogging) { - + if let awsCLIPath = try? self.findExecutable(context: context, + executableName: "aws", + helpMessage: "aws command line is used to find default AWS region. (brew install awscli)", + verboseLogging: verboseLogging) { let userDir = FileManager.default.homeDirectoryForCurrentUser.path if FileManager.default.fileExists(atPath: "\(userDir)/.aws/config") { // aws --profile default configure get region @@ -394,14 +378,15 @@ image_repositories = [] arguments: ["--profile", "default", "configure", "get", "region"], - logLevel: verboseLogging ? .debug : .silent) - + logLevel: verboseLogging ? .debug : .silent + ) + result?.removeLast() // remove trailing newline char } catch { print("Unexpected error : \(error)") throw DeployerPluginError.error(error) } - + guard result == nil else { print("AWS Region : \(result!) (from AWS CLI configuration)") return result! @@ -413,42 +398,42 @@ image_repositories = [] throw DeployerPluginError.noRegionFound(helpMsg) } - + private func displayHelpMessage() { print(""" -OVERVIEW: A swift plugin to deploy your Lambda function on your AWS account. - -REQUIREMENTS: To use this plugin, you must have an AWS account and have `sam` installed. - You can install sam with the following command: - (brew tap aws/tap && brew install aws-sam-cli) - -USAGE: swift package --disable-sandbox deploy [--help] [--verbose] - [--archive-path ] - [--configuration ] - [--force] [--nodeploy] [--nolist] - [--region ] - [--stack-name ] - -OPTIONS: - --verbose Produce verbose output for debugging. - --archive-path - The path where the archive plugin created the ZIP archive. - Must be aligned with the value passed to archive --output-path plugin. - (default: .build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager) - --configuration - Build for a specific configuration. - Must be aligned with what was used to build and package. - Valid values: [ debug, release ] (default: release) - --force Overwrites existing SAM deployment descriptor. - --nodeploy Generates the YAML deployment descriptor, but do not deploy. - --nolist Do not list endpoints. - --stack-name - The name of the CloudFormation stack when deploying. - (default: the project name) - --region The AWS region to deploy to. - (default: the region of AWS CLI's default profile) - --help Show help information. -""") + OVERVIEW: A swift plugin to deploy your Lambda function on your AWS account. + + REQUIREMENTS: To use this plugin, you must have an AWS account and have `sam` installed. + You can install sam with the following command: + (brew tap aws/tap && brew install aws-sam-cli) + + USAGE: swift package --disable-sandbox deploy [--help] [--verbose] + [--archive-path ] + [--configuration ] + [--force] [--nodeploy] [--nolist] + [--region ] + [--stack-name ] + + OPTIONS: + --verbose Produce verbose output for debugging. + --archive-path + The path where the archive plugin created the ZIP archive. + Must be aligned with the value passed to archive --output-path plugin. + (default: .build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager) + --configuration + Build for a specific configuration. + Must be aligned with what was used to build and package. + Valid values: [ debug, release ] (default: release) + --force Overwrites existing SAM deployment descriptor. + --nodeploy Generates the YAML deployment descriptor, but do not deploy. + --nolist Do not list endpoints. + --stack-name + The name of the CloudFormation stack when deploying. + (default: the project name) + --region The AWS region to deploy to. + (default: the region of AWS CLI's default profile) + --help Show help information. + """) } } @@ -462,16 +447,15 @@ private struct Configuration: CustomStringConvertible { public let archiveDirectory: String? public let stackName: String public let region: String? - + private let context: PluginContext - + public init( context: PluginContext, arguments: [String] ) throws { - self.context = context // keep a reference for self.description - + // extract command line arguments var argumentExtractor = ArgumentExtractor(arguments) let nodeployArgument = argumentExtractor.extractFlag(named: "nodeploy") > 0 @@ -483,22 +467,22 @@ private struct Configuration: CustomStringConvertible { let stackNameArgument = argumentExtractor.extractOption(named: "stackname") let regionArgument = argumentExtractor.extractOption(named: "region") let helpArgument = argumentExtractor.extractFlag(named: "help") > 0 - + // help required ? self.help = helpArgument - + // force overwrite the SAM deployment descriptor when it already exists self.force = forceArgument - + // define deployment option self.noDeploy = nodeployArgument - + // define control on list endpoints after a deployment self.noList = noListArgument - + // define logging verbosity self.verboseLogging = verboseArgument - + // define build configuration, defaults to debug if let buildConfigurationName = configurationArgument.first { guard @@ -511,11 +495,11 @@ private struct Configuration: CustomStringConvertible { } else { self.buildConfiguration = .release } - + // use a default archive directory when none are given if let archiveDirectory = archiveDirectoryArgument.first { self.archiveDirectory = archiveDirectory - + // check if archive directory exists var isDirectory: ObjCBool = false guard FileManager.default.fileExists(atPath: archiveDirectory, isDirectory: &isDirectory), isDirectory.boolValue else { @@ -525,20 +509,20 @@ private struct Configuration: CustomStringConvertible { } else { self.archiveDirectory = nil } - + // infer or consume stack name if let stackName = stackNameArgument.first { self.stackName = stackName } else { self.stackName = context.package.displayName } - + if let region = regionArgument.first { self.region = region } else { self.region = nil } - + if self.verboseLogging { print("-------------------------------------------------------------------------") print("configuration") @@ -546,22 +530,22 @@ private struct Configuration: CustomStringConvertible { print(self) } } - + var description: String { - """ - { - verbose: \(self.verboseLogging) - force: \(self.force) - noDeploy: \(self.noDeploy) - noList: \(self.noList) - buildConfiguration: \(self.buildConfiguration) - archiveDirectory: \(self.archiveDirectory ?? "none provided on command line") - stackName: \(self.stackName) - region: \(self.region ?? "none provided on command line") - Plugin directory: \(self.context.pluginWorkDirectory) - Project directory: \(self.context.package.directory) - } - """ + """ + { + verbose: \(self.verboseLogging) + force: \(self.force) + noDeploy: \(self.noDeploy) + noList: \(self.noList) + buildConfiguration: \(self.buildConfiguration) + archiveDirectory: \(self.archiveDirectory ?? "none provided on command line") + stackName: \(self.stackName) + region: \(self.region ?? "none provided on command line") + Plugin directory: \(self.context.pluginWorkDirectory) + Project directory: \(self.context.package.directory) + } + """ } } @@ -571,7 +555,7 @@ private enum DeployerPluginError: Error, CustomStringConvertible { case deployswiftDoesNotExist case noRegionFound(String) case error(Error) - + var description: String { switch self { case .invalidArgument(let description): @@ -587,4 +571,3 @@ private enum DeployerPluginError: Error, CustomStringConvertible { } } } - diff --git a/Plugins/AWSLambdaDeployer/PluginUtils.swift b/Plugins/AWSLambdaDeployer/PluginUtils.swift index 4ebe2d1..d1506f7 100644 --- a/Plugins/AWSLambdaDeployer/PluginUtils.swift +++ b/Plugins/AWSLambdaDeployer/PluginUtils.swift @@ -98,6 +98,7 @@ struct Utils { } } } + enum ProcessLogLevel: Comparable { case silent case output(outputIndent: Int) diff --git a/Plugins/AWSLambdaDescriptorGenerator/Plugin.swift b/Plugins/AWSLambdaDescriptorGenerator/Plugin.swift new file mode 100644 index 0000000..a2d3380 --- /dev/null +++ b/Plugins/AWSLambdaDescriptorGenerator/Plugin.swift @@ -0,0 +1,26 @@ + +import Foundation +import PackagePlugin + +@main struct AWSLambdaDescriptorGenerator: BuildToolPlugin { + func createBuildCommands( + context: PluginContext, + target: Target + ) throws -> [Command] { + guard let target = target as? SourceModuleTarget else { + return [] + } + + return try target.sourceFiles(withSuffix: "json").map { samSchema in + let base = samSchema.path.stem + let input = samSchema.path + let output = context.pluginWorkDirectory.appending(["\(base).swift"]) + + return try .buildCommand(displayName: "Generating constants for \(base)", + executable: context.tool(named: "DeploymentDescriptorGeneratorExecutable").path, + arguments: [input.string, output.string], + inputFiles: [input], + outputFiles: [output]) + } + } +} diff --git a/Plugins/AWSLambdaDescriptorGenerator/PluginUtils.swift b/Plugins/AWSLambdaDescriptorGenerator/PluginUtils.swift new file mode 100644 index 0000000..e9ab159 --- /dev/null +++ b/Plugins/AWSLambdaDescriptorGenerator/PluginUtils.swift @@ -0,0 +1,130 @@ +// ===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2022 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +// ===----------------------------------------------------------------------===// + +import Dispatch +import Foundation +import PackagePlugin + +struct Utils { + @discardableResult + static func execute( + executable: Path, + arguments: [String], + customWorkingDirectory: Path? = .none, + logLevel: ProcessLogLevel + ) throws -> String { + if logLevel >= .debug { + print("\(executable.string) \(arguments.joined(separator: " "))") + } + + var output = "" + let outputSync = DispatchGroup() + let outputQueue = DispatchQueue(label: "AWSLambdaPackager.output") + let outputHandler = { (data: Data?) in + dispatchPrecondition(condition: .onQueue(outputQueue)) + + outputSync.enter() + defer { outputSync.leave() } + + guard let _output = data.flatMap({ String(data: $0, encoding: .utf8)?.trimmingCharacters(in: CharacterSet(["\n"])) }), !_output.isEmpty else { + return + } + + output += _output + "\n" + + switch logLevel { + case .silent: + break + case .debug(let outputIndent), .output(let outputIndent): + print(String(repeating: " ", count: outputIndent), terminator: "") + print(_output) + fflush(stdout) + } + } + + let pipe = Pipe() + pipe.fileHandleForReading.readabilityHandler = { fileHandle in outputQueue.async { outputHandler(fileHandle.availableData) } } + + let process = Process() + process.standardOutput = pipe + process.standardError = pipe + process.executableURL = URL(fileURLWithPath: executable.string) + process.arguments = arguments + if let workingDirectory = customWorkingDirectory { + process.currentDirectoryURL = URL(fileURLWithPath: workingDirectory.string) + } + process.terminationHandler = { _ in + outputQueue.async { + outputHandler(try? pipe.fileHandleForReading.readToEnd()) + } + } + + try process.run() + process.waitUntilExit() + + // wait for output to be full processed + outputSync.wait() + + if process.terminationStatus != 0 { + // print output on failure and if not already printed + if logLevel < .output { + print(output) + fflush(stdout) + } + throw ProcessError.processFailed([executable.string] + arguments, process.terminationStatus, output) + } + + return output + } + + enum ProcessError: Error, CustomStringConvertible { + case processFailed([String], Int32, String) + + var description: String { + switch self { + case .processFailed(let arguments, let code, _): + return "\(arguments.joined(separator: " ")) failed with code \(code)" + } + } + } + + enum ProcessLogLevel: Comparable { + case silent + case output(outputIndent: Int) + case debug(outputIndent: Int) + + var naturalOrder: Int { + switch self { + case .silent: + return 0 + case .output: + return 1 + case .debug: + return 2 + } + } + + static var output: Self { + .output(outputIndent: 2) + } + + static var debug: Self { + .debug(outputIndent: 2) + } + + static func < (lhs: ProcessLogLevel, rhs: ProcessLogLevel) -> Bool { + lhs.naturalOrder < rhs.naturalOrder + } + } +} diff --git a/Sources/AWSLambdaDeploymentDescriptor/DeploymentDescriptor.swift b/Sources/AWSLambdaDeploymentDescriptor/DeploymentDescriptor.swift index 697e68a..5e315ec 100644 --- a/Sources/AWSLambdaDeploymentDescriptor/DeploymentDescriptor.swift +++ b/Sources/AWSLambdaDeploymentDescriptor/DeploymentDescriptor.swift @@ -21,37 +21,36 @@ import Foundation // currently limited to the properties I needed for the examples. // An immediate TODO if this code is accepted is to add more properties and more struct public struct SAMDeploymentDescriptor: Encodable { + let templateVersion: String = "2010-09-09" + let transform: String = "AWS::Serverless-2016-10-31" + let description: String + var resources: [String: Resource] = [:] + + public init( + description: String, + resources: [Resource] = [] + ) { + self.description = description + + // extract resources names for serialization + for res in resources { + self.resources[res.name] = res + } + } - let templateVersion: String = "2010-09-09" - let transform: String = "AWS::Serverless-2016-10-31" - let description: String - var resources: [String: Resource] = [:] - - public init( - description: String, - resources: [Resource] = [] - ) { - self.description = description - - // extract resources names for serialization - for res in resources { - self.resources[res.name] = res - } - } - - enum CodingKeys: String, CodingKey { - case templateVersion = "AWSTemplateFormatVersion" - case transform - case description - case resources - } + enum CodingKeys: String, CodingKey { + case templateVersion = "AWSTemplateFormatVersion" + case transform + case description + case resources + } } public protocol SAMResource: Encodable, Equatable {} public protocol SAMResourceType: Encodable, Equatable {} public protocol SAMResourceProperties: Encodable {} -// public protocol Table: SAMResource { +// public protocol Table: SAMResource { // func type() -> String // } // public extension Table { @@ -59,40 +58,39 @@ public protocol SAMResourceProperties: Encodable {} // } public enum ResourceType: String, SAMResourceType { - case function = "AWS::Serverless::Function" - case queue = "AWS::SQS::Queue" - case table = "AWS::Serverless::SimpleTable" + case function = "AWS::Serverless::Function" + case queue = "AWS::SQS::Queue" + case table = "AWS::Serverless::SimpleTable" } public enum EventSourceType: String, SAMResourceType { - case httpApi = "HttpApi" - case sqs = "SQS" + case httpApi = "HttpApi" + case sqs = "SQS" } // generic type to represent either a top-level resource or an event source public struct Resource: SAMResource { + let type: T + let properties: SAMResourceProperties? + let name: String - let type: T - let properties: SAMResourceProperties? - let name: String - - public static func == (lhs: Resource, rhs: Resource) -> Bool { - lhs.type == rhs.type && lhs.name == rhs.name - } + public static func == (lhs: Resource, rhs: Resource) -> Bool { + lhs.type == rhs.type && lhs.name == rhs.name + } - enum CodingKeys: CodingKey { - case type - case properties - } + enum CodingKeys: CodingKey { + case type + case properties + } - // this is to make the compiler happy : Resource now conforms to Encodable - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(self.type, forKey: .type) - if let properties = self.properties { - try container.encode(properties, forKey: .properties) + // this is to make the compiler happy : Resource now conforms to Encodable + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.type, forKey: .type) + if let properties = self.properties { + try container.encode(properties, forKey: .properties) + } } - } } // MARK: Lambda Function resource definition @@ -104,180 +102,184 @@ public struct Resource: SAMResource { -----------------------------------------------------------------------------------------*/ public struct ServerlessFunctionProperties: SAMResourceProperties { + public enum Architectures: String, Encodable, CaseIterable { + case x64 = "x86_64" + case arm64 + + // the default value is the current architecture + public static func defaultArchitecture() -> Architectures { + #if arch(arm64) + return .arm64 + #else // I understand this #else will not always be true. Developers can overwrite the default in Deploy.swift + return .x64 + #endif + } - public enum Architectures: String, Encodable, CaseIterable { - case x64 = "x86_64" - case arm64 = "arm64" - - // the default value is the current architecture - public static func defaultArchitecture() -> Architectures { - #if arch(arm64) - return .arm64 - #else // I understand this #else will not always be true. Developers can overwrite the default in Deploy.swift - return .x64 - #endif + // valid values for error and help message + public static func validValues() -> String { + Architectures.allCases.map(\.rawValue).joined(separator: ", ") + } } - // valid values for error and help message - public static func validValues() -> String { - return Architectures.allCases.map { $0.rawValue }.joined(separator: ", ") - } - } + // https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lambda-function-ephemeralstorage.html + public struct EphemeralStorage: Encodable { + private let validValues = 512 ... 10240 + let size: Int + init?(_ size: Int) { + if self.validValues.contains(size) { + self.size = size + } else { + return nil + } + } - // https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lambda-function-ephemeralstorage.html - public struct EphemeralStorage: Encodable { - private let validValues = 512...10240 - let size: Int - init?(_ size: Int) { - if validValues.contains(size) { - self.size = size - } else { - return nil - } - } - enum CodingKeys: String, CodingKey { - case size = "Size" + enum CodingKeys: String, CodingKey { + case size = "Size" + } } - } - public struct EventInvokeConfiguration: Encodable { - public enum EventInvokeDestinationType: String, Encodable { - case sqs = "SQS" - case sns = "SNS" - case lambda = "Lambda" - case eventBridge = "EventBridge" + public struct EventInvokeConfiguration: Encodable { + public enum EventInvokeDestinationType: String, Encodable { + case sqs = "SQS" + case sns = "SNS" + case lambda = "Lambda" + case eventBridge = "EventBridge" + + public static func destinationType(from arn: Arn?) -> EventInvokeDestinationType? { + guard let service = arn?.service() else { + return nil + } + switch service.lowercased() { + case "sqs": + return .sqs + case "sns": + return .sns + case "lambda": + return .lambda + case "eventbridge": + return .eventBridge + default: + return nil + } + } + + public static func destinationType(from resource: Resource?) + -> EventInvokeDestinationType? { + guard let res = resource else { + return nil + } + switch res.type { + case .queue: + return .sqs + case .function: + return .lambda + default: + return nil + } + } + } - public static func destinationType(from arn: Arn?) -> EventInvokeDestinationType? { - guard let service = arn?.service() else { - return nil + public struct EventInvokeDestination: Encodable { + let destination: Reference? + let type: EventInvokeDestinationType? } - switch service.lowercased() { - case "sqs": - return .sqs - case "sns": - return .sns - case "lambda": - return .lambda - case "eventbridge": - return .eventBridge - default: - return nil + + public struct EventInvokeDestinationConfiguration: Encodable { + let onSuccess: EventInvokeDestination? + let onFailure: EventInvokeDestination? } - } - public static func destinationType(from resource: Resource?) - -> EventInvokeDestinationType? - { - guard let res = resource else { - return nil + + let destinationConfig: EventInvokeDestinationConfiguration? + let maximumEventAgeInSeconds: Int? + let maximumRetryAttempts: Int? + } + + // TODO: add support for reference to other resources of type elasticfilesystem or mountpoint + public struct FileSystemConfig: Encodable { + // regex from + // https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lambda-function-filesystemconfig.html + let validMountPathRegex = #"^/mnt/[a-zA-Z0-9-_.]+$"# + let validArnRegex = + #"arn:aws[a-zA-Z-]*:elasticfilesystem:[a-z]{2}((-gov)|(-iso(b?)))?-[a-z]+-\d{1}:\d{12}:access-point/fsap-[a-f0-9]{17}"# + let reference: Reference + let localMountPath: String + + public init?(arn: String, localMountPath: String) { + guard arn.range(of: self.validArnRegex, options: .regularExpression) != nil, + localMountPath.range(of: self.validMountPathRegex, options: .regularExpression) != nil + else { + return nil + } + + self.reference = .arn(Arn(arn)!) + self.localMountPath = localMountPath } - switch res.type { - case .queue: - return .sqs - case .function: - return .lambda - default: - return nil + + enum CodingKeys: String, CodingKey { + case reference = "Arn" + case localMountPath } - } } - public struct EventInvokeDestination: Encodable { - let destination: Reference? - let type: EventInvokeDestinationType? - } - public struct EventInvokeDestinationConfiguration: Encodable { - let onSuccess: EventInvokeDestination? - let onFailure: EventInvokeDestination? - } - let destinationConfig: EventInvokeDestinationConfiguration? - let maximumEventAgeInSeconds: Int? - let maximumRetryAttempts: Int? - } - //TODO: add support for reference to other resources of type elasticfilesystem or mountpoint - public struct FileSystemConfig: Encodable { + public struct URLConfig: Encodable { + public enum AuthType: String, Encodable { + case iam = "AWS_IAM" + case none = "None" + } - // regex from - // https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lambda-function-filesystemconfig.html - let validMountPathRegex = #"^/mnt/[a-zA-Z0-9-_.]+$"# - let validArnRegex = - #"arn:aws[a-zA-Z-]*:elasticfilesystem:[a-z]{2}((-gov)|(-iso(b?)))?-[a-z]+-\d{1}:\d{12}:access-point/fsap-[a-f0-9]{17}"# - let reference: Reference - let localMountPath: String + public enum InvokeMode: String, Encodable { + case responseStream = "RESPONSE_STREAM" + case buffered = "BUFFERED" + } - public init?(arn: String, localMountPath: String) { + public struct Cors: Encodable { + let allowCredentials: Bool? + let allowHeaders: [String]? + let allowMethods: [String]? + let allowOrigins: [String]? + let exposeHeaders: [String]? + let maxAge: Int? + } - guard arn.range(of: validArnRegex, options: .regularExpression) != nil, - localMountPath.range(of: validMountPathRegex, options: .regularExpression) != nil - else { - return nil - } + let authType: AuthType + let cors: Cors? + let invokeMode: InvokeMode? + } - self.reference = .arn(Arn(arn)!) - self.localMountPath = localMountPath + let architectures: [Architectures] + let handler: String + let runtime: String + let codeUri: String? + var autoPublishAlias: String? + var autoPublishAliasAllProperties: Bool? + var autoPublishCodeSha256: String? + var events: [String: Resource]? + var environment: SAMEnvironmentVariable? + var description: String? + var ephemeralStorage: EphemeralStorage? + var eventInvokeConfig: EventInvokeConfiguration? + var fileSystemConfigs: [FileSystemConfig]? + var functionUrlConfig: URLConfig? + + public init( + codeUri: String?, + architecture: Architectures, + eventSources: [Resource] = [], + environment: SAMEnvironmentVariable? = nil + ) { + self.architectures = [architecture] + self.handler = "Provided" + self.runtime = "provided.al2" // Amazon Linux 2 supports both arm64 and x64 + self.codeUri = codeUri + self.environment = environment + + if !eventSources.isEmpty { + self.events = [:] + for es in eventSources { + self.events![es.name] = es + } + } } - enum CodingKeys: String, CodingKey { - case reference = "Arn" - case localMountPath - } - } - - public struct URLConfig: Encodable { - public enum AuthType: String, Encodable { - case iam = "AWS_IAM" - case none = "None" - } - public enum InvokeMode: String, Encodable { - case responseStream = "RESPONSE_STREAM" - case buffered = "BUFFERED" - } - public struct Cors: Encodable { - let allowCredentials: Bool? - let allowHeaders: [String]? - let allowMethods: [String]? - let allowOrigins: [String]? - let exposeHeaders: [String]? - let maxAge: Int? - } - let authType: AuthType - let cors: Cors? - let invokeMode: InvokeMode? - } - - let architectures: [Architectures] - let handler: String - let runtime: String - let codeUri: String? - var autoPublishAlias: String? - var autoPublishAliasAllProperties: Bool? - var autoPublishCodeSha256: String? - var events: [String: Resource]? - var environment: SAMEnvironmentVariable? - var description: String? - var ephemeralStorage: EphemeralStorage? - var eventInvokeConfig: EventInvokeConfiguration? - var fileSystemConfigs: [FileSystemConfig]? - var functionUrlConfig: URLConfig? - - public init( - codeUri: String?, - architecture: Architectures, - eventSources: [Resource] = [], - environment: SAMEnvironmentVariable? = nil - ) { - - self.architectures = [architecture] - self.handler = "Provided" - self.runtime = "provided.al2" // Amazon Linux 2 supports both arm64 and x64 - self.codeUri = codeUri - self.environment = environment - - if !eventSources.isEmpty { - self.events = [:] - for es in eventSources { - self.events![es.name] = es - } - } - } } /* @@ -286,101 +288,105 @@ public struct ServerlessFunctionProperties: SAMResourceProperties { LOG_LEVEL: debug */ public struct SAMEnvironmentVariable: Encodable { + public var variables: [String: SAMEnvironmentVariableValue] = [:] + public init() {} + public init(_ variables: [String: String]) { + for key in variables.keys { + self.variables[key] = .string(value: variables[key] ?? "") + } + } + + public static var none: SAMEnvironmentVariable { SAMEnvironmentVariable([:]) } + + public static func variable(_ name: String, _ value: String) -> SAMEnvironmentVariable { + SAMEnvironmentVariable([name: value]) + } + + public static func variable(_ variables: [String: String]) -> SAMEnvironmentVariable { + SAMEnvironmentVariable(variables) + } + + public static func variable(_ variables: [[String: String]]) -> SAMEnvironmentVariable { + var mergedDictKeepCurrent: [String: String] = [:] + for dict in variables { + // inspired by https://stackoverflow.com/a/43615143/663360 + mergedDictKeepCurrent = mergedDictKeepCurrent.merging(dict) { current, _ in current } + } + + return SAMEnvironmentVariable(mergedDictKeepCurrent) + } + + public func isEmpty() -> Bool { self.variables.count == 0 } + + public mutating func append(_ key: String, _ value: String) { + self.variables[key] = .string(value: value) + } + + public mutating func append(_ key: String, _ value: [String: String]) { + self.variables[key] = .array(value: value) + } + + public mutating func append(_ key: String, _ value: [String: [String]]) { + self.variables[key] = .dictionary(value: value) + } + + public mutating func append(_ key: String, _ value: Resource) { + self.variables[key] = .array(value: ["Ref": value.name]) + } + + enum CodingKeys: CodingKey { + case variables + } + + public func encode(to encoder: Encoder) throws { + guard !self.isEmpty() else { + return + } + + var container = encoder.container(keyedBy: CodingKeys.self) + var nestedContainer = container.nestedContainer(keyedBy: AnyStringKey.self, forKey: .variables) + + for key in self.variables.keys { + switch self.variables[key] { + case .string(let value): + try? nestedContainer.encode(value, forKey: AnyStringKey(key)) + case .array(let value): + try? nestedContainer.encode(value, forKey: AnyStringKey(key)) + case .dictionary(let value): + try? nestedContainer.encode(value, forKey: AnyStringKey(key)) + case .none: + break + } + } + } - public var variables: [String: SAMEnvironmentVariableValue] = [:] - public init() {} - public init(_ variables: [String: String]) { - for key in variables.keys { - self.variables[key] = .string(value: variables[key] ?? "") - } - } - public static var none: SAMEnvironmentVariable { return SAMEnvironmentVariable([:]) } - - public static func variable(_ name: String, _ value: String) -> SAMEnvironmentVariable { - return SAMEnvironmentVariable([name: value]) - } - public static func variable(_ variables: [String: String]) -> SAMEnvironmentVariable { - return SAMEnvironmentVariable(variables) - } - public static func variable(_ variables: [[String: String]]) -> SAMEnvironmentVariable { - - var mergedDictKeepCurrent: [String: String] = [:] - variables.forEach { dict in - // inspired by https://stackoverflow.com/a/43615143/663360 - mergedDictKeepCurrent = mergedDictKeepCurrent.merging(dict) { (current, _) in current } - } - - return SAMEnvironmentVariable(mergedDictKeepCurrent) - - } - public func isEmpty() -> Bool { return variables.count == 0 } - - public mutating func append(_ key: String, _ value: String) { - variables[key] = .string(value: value) - } - public mutating func append(_ key: String, _ value: [String: String]) { - variables[key] = .array(value: value) - } - public mutating func append(_ key: String, _ value: [String: [String]]) { - variables[key] = .dictionary(value: value) - } - public mutating func append(_ key: String, _ value: Resource) { - variables[key] = .array(value: ["Ref": value.name]) - } - - enum CodingKeys: CodingKey { - case variables - } - - public func encode(to encoder: Encoder) throws { - - guard !self.isEmpty() else { - return - } - - var container = encoder.container(keyedBy: CodingKeys.self) - var nestedContainer = container.nestedContainer(keyedBy: AnyStringKey.self, forKey: .variables) - - for key in variables.keys { - switch variables[key] { - case .string(let value): - try? nestedContainer.encode(value, forKey: AnyStringKey(key)) - case .array(let value): - try? nestedContainer.encode(value, forKey: AnyStringKey(key)) - case .dictionary(let value): - try? nestedContainer.encode(value, forKey: AnyStringKey(key)) - case .none: - break - } - } - } - - public enum SAMEnvironmentVariableValue { - // KEY: VALUE - case string(value: String) - - // KEY: - // Ref: VALUE - case array(value: [String: String]) - - // KEY: - // Fn::GetAtt: - // - VALUE1 - // - VALUE2 - case dictionary(value: [String: [String]]) - } + public enum SAMEnvironmentVariableValue { + // KEY: VALUE + case string(value: String) + + // KEY: + // Ref: VALUE + case array(value: [String: String]) + + // KEY: + // Fn::GetAtt: + // - VALUE1 + // - VALUE2 + case dictionary(value: [String: [String]]) + } } -internal struct AnyStringKey: CodingKey, Hashable, ExpressibleByStringLiteral { - var stringValue: String - init(stringValue: String) { self.stringValue = stringValue } - init(_ stringValue: String) { self.init(stringValue: stringValue) } - var intValue: Int? - init?(intValue: Int) { return nil } - init(stringLiteral value: String) { self.init(value) } +struct AnyStringKey: CodingKey, Hashable, ExpressibleByStringLiteral { + var stringValue: String + init(stringValue: String) { self.stringValue = stringValue } + init(_ stringValue: String) { self.init(stringValue: stringValue) } + var intValue: Int? + init?(intValue: Int) { nil } + init(stringLiteral value: String) { self.init(value) } } // MARK: HTTP API Event definition + /*--------------------------------------------------------------------------------------- HTTP API Event (API Gateway v2) @@ -388,23 +394,25 @@ internal struct AnyStringKey: CodingKey, Hashable, ExpressibleByStringLiteral { -----------------------------------------------------------------------------------------*/ struct HttpApiProperties: SAMResourceProperties, Equatable { - init(method: HttpVerb? = nil, path: String? = nil) { - self.method = method - self.path = path - } - let method: HttpVerb? - let path: String? + init(method: HttpVerb? = nil, path: String? = nil) { + self.method = method + self.path = path + } + + let method: HttpVerb? + let path: String? } public enum HttpVerb: String, Encodable { - case GET - case POST - case PUT - case DELETE - case OPTION + case GET + case POST + case PUT + case DELETE + case OPTION } // MARK: SQS event definition + /*--------------------------------------------------------------------------------------- SQS Event @@ -414,53 +422,53 @@ public enum HttpVerb: String, Encodable { /// Represents SQS queue properties. /// When `queue` name is a shorthand YAML reference to another resource, like `!GetAtt`, it splits the shorthand into proper YAML to make the parser happy public struct SQSEventProperties: SAMResourceProperties, Equatable { + public var reference: Reference + public var batchSize: Int + public var enabled: Bool + + init( + byRef ref: String, + batchSize: Int, + enabled: Bool + ) { + // when the ref is an ARN, leave it as it, otherwise, create a queue resource and pass a reference to it + if let arn = Arn(ref) { + self.reference = .arn(arn) + } else { + let logicalName = Resource.logicalName( + resourceType: "Queue", + resourceName: ref + ) + let queue = Resource( + type: .queue, + properties: SQSResourceProperties(queueName: ref), + name: logicalName + ) + self.reference = .resource(queue) + } + self.batchSize = batchSize + self.enabled = enabled + } + + init( + _ queue: Resource, + batchSize: Int, + enabled: Bool + ) { + self.reference = .resource(queue) + self.batchSize = batchSize + self.enabled = enabled + } - public var reference: Reference - public var batchSize: Int - public var enabled: Bool - - init( - byRef ref: String, - batchSize: Int, - enabled: Bool - ) { - - // when the ref is an ARN, leave it as it, otherwise, create a queue resource and pass a reference to it - if let arn = Arn(ref) { - self.reference = .arn(arn) - } else { - let logicalName = Resource.logicalName( - resourceType: "Queue", - resourceName: ref) - let queue = Resource( - type: .queue, - properties: SQSResourceProperties(queueName: ref), - name: logicalName) - self.reference = .resource(queue) - } - self.batchSize = batchSize - self.enabled = enabled - } - - init( - _ queue: Resource, - batchSize: Int, - enabled: Bool - ) { - - self.reference = .resource(queue) - self.batchSize = batchSize - self.enabled = enabled - } - - enum CodingKeys: String, CodingKey { - case reference = "Queue" - case batchSize - case enabled - } + enum CodingKeys: String, CodingKey { + case reference = "Queue" + case batchSize + case enabled + } } // MARK: SQS queue resource definition + /*--------------------------------------------------------------------------------------- SQS Queue Resource @@ -469,10 +477,11 @@ public struct SQSEventProperties: SAMResourceProperties, Equatable { -----------------------------------------------------------------------------------------*/ public struct SQSResourceProperties: SAMResourceProperties { - public let queueName: String + public let queueName: String } // MARK: Simple DynamoDB table resource definition + /*--------------------------------------------------------------------------------------- Simple DynamoDB Table Resource @@ -481,108 +490,109 @@ public struct SQSResourceProperties: SAMResourceProperties { -----------------------------------------------------------------------------------------*/ public struct SimpleTableProperties: SAMResourceProperties { - let primaryKey: PrimaryKey - let tableName: String - var provisionedThroughput: ProvisionedThroughput? = nil - struct PrimaryKey: Codable { - let name: String - let type: String - } - struct ProvisionedThroughput: Codable { - let readCapacityUnits: Int - let writeCapacityUnits: Int - } + let primaryKey: PrimaryKey + let tableName: String + var provisionedThroughput: ProvisionedThroughput? = nil + struct PrimaryKey: Codable { + let name: String + let type: String + } + + struct ProvisionedThroughput: Codable { + let readCapacityUnits: Int + let writeCapacityUnits: Int + } } // MARK: Utils public struct Arn: Encodable { - public let arn: String - - // Arn regex from https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lambda-eventsourcemapping.html#cfn-lambda-eventsourcemapping-eventsourcearn - private let arnRegex = - #"arn:(aws[a-zA-Z0-9-]*):([a-zA-Z0-9\-]+):([a-z]{2}(-gov)?-[a-z]+-\d{1})?:(\d{12})?:(.*)"# - - public init?(_ arn: String) { - if arn.range(of: arnRegex, options: .regularExpression) != nil { - self.arn = arn - } else { - return nil - } - } - public func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() - try container.encode(self.arn) - } - public func service() -> String? { - var result: String? = nil - - if #available(macOS 13, *) { - let regex = try! Regex(arnRegex) - if let matches = try? regex.wholeMatch(in: self.arn), - matches.count > 3, - let substring = matches[2].substring - { - result = "\(substring)" - } - } else { - let split = self.arn.split(separator: ":") - if split.count > 3 { - result = "\(split[2])" - } - } - - return result - } + public let arn: String + + // Arn regex from https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lambda-eventsourcemapping.html#cfn-lambda-eventsourcemapping-eventsourcearn + private let arnRegex = + #"arn:(aws[a-zA-Z0-9-]*):([a-zA-Z0-9\-]+):([a-z]{2}(-gov)?-[a-z]+-\d{1})?:(\d{12})?:(.*)"# + + public init?(_ arn: String) { + if arn.range(of: self.arnRegex, options: .regularExpression) != nil { + self.arn = arn + } else { + return nil + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(self.arn) + } + + public func service() -> String? { + var result: String? = nil + + if #available(macOS 13, *) { + let regex = try! Regex(arnRegex) + if let matches = try? regex.wholeMatch(in: self.arn), + matches.count > 3, + let substring = matches[2].substring { + result = "\(substring)" + } + } else { + let split = self.arn.split(separator: ":") + if split.count > 3 { + result = "\(split[2])" + } + } + + return result + } } public enum Reference: Encodable, Equatable { - case arn(Arn) - case resource(Resource) - - // if we have an Arn, return the Arn, otherwise pass a reference with GetAtt - // https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-property-function-sqs.html#sam-function-sqs-queue - public func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() - switch self { - case .arn(let arn): - try container.encode(arn) - case .resource(let resource): - var getAttIntrinsicFunction: [String: [String]] = [:] - getAttIntrinsicFunction["Fn::GetAtt"] = [resource.name, "Arn"] - try container.encode(getAttIntrinsicFunction) - } - } - - public static func == (lhs: Reference, rhs: Reference) -> Bool { - switch lhs { - case .arn(let lArn): - if case let .arn(rArn) = rhs { - return lArn.arn == rArn.arn - } else { - return false - } - case .resource(let lResource): - if case let .resource(rResource) = lhs { - return lResource == rResource - } else { - return false - } - } - } + case arn(Arn) + case resource(Resource) + + // if we have an Arn, return the Arn, otherwise pass a reference with GetAtt + // https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-property-function-sqs.html#sam-function-sqs-queue + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + switch self { + case .arn(let arn): + try container.encode(arn) + case .resource(let resource): + var getAttIntrinsicFunction: [String: [String]] = [:] + getAttIntrinsicFunction["Fn::GetAtt"] = [resource.name, "Arn"] + try container.encode(getAttIntrinsicFunction) + } + } + public static func == (lhs: Reference, rhs: Reference) -> Bool { + switch lhs { + case .arn(let lArn): + if case .arn(let rArn) = rhs { + return lArn.arn == rArn.arn + } else { + return false + } + case .resource(let lResource): + if case .resource(let rResource) = lhs { + return lResource == rResource + } else { + return false + } + } + } } extension Resource { - // Transform resourceName : - // remove space - // remove hyphen - // camel case - static func logicalName(resourceType: String, resourceName: String) -> String { - let noSpaceName = resourceName.split(separator: " ").map { $0.capitalized }.joined( - separator: "") - let noHyphenName = noSpaceName.split(separator: "-").map { $0.capitalized }.joined( - separator: "") - return resourceType.capitalized + noHyphenName - } + // Transform resourceName : + // remove space + // remove hyphen + // camel case + static func logicalName(resourceType: String, resourceName: String) -> String { + let noSpaceName = resourceName.split(separator: " ").map(\.capitalized).joined( + separator: "") + let noHyphenName = noSpaceName.split(separator: "-").map(\.capitalized).joined( + separator: "") + return resourceType.capitalized + noHyphenName + } } diff --git a/Sources/AWSLambdaDeploymentDescriptor/DeploymentDescriptorBuilder.swift b/Sources/AWSLambdaDeploymentDescriptor/DeploymentDescriptorBuilder.swift index 2056599..24462b4 100644 --- a/Sources/AWSLambdaDeploymentDescriptor/DeploymentDescriptorBuilder.swift +++ b/Sources/AWSLambdaDeploymentDescriptor/DeploymentDescriptorBuilder.swift @@ -57,11 +57,11 @@ public struct DeploymentDescriptor { _ description: String, _ resources: [Resource]... ) -> (String?, [Resource]) { - return (description, resources.flatMap { $0 }) + (description, resources.flatMap { $0 }) } public static func buildBlock(_ resources: [Resource]...) -> (String?, [Resource]) { - return (nil, resources.flatMap { $0 }) + (nil, resources.flatMap { $0 }) } public static func buildFinalResult(_ function: (String?, [Resource])) -> DeploymentDescriptor { @@ -79,11 +79,10 @@ public struct DeploymentDescriptor { public static func buildExpression(_ expression: any BuilderResource) -> [Resource] { expression.resource() } - } public protocol BuilderResource { - func resource() -> [Resource] + func resource() -> [Resource] } // MARK: Function resource @@ -107,12 +106,12 @@ public struct Function: BuilderResource { properties: ServerlessFunctionProperties ) { self._underlying = Resource( - type: .function, - properties: properties, - name: name - ) + type: .function, + properties: properties, + name: name + ) } - + public init( name: String, architecture: ServerlessFunctionProperties.Architectures = .defaultArchitecture(), @@ -164,7 +163,7 @@ public struct Function: BuilderResource { // this method fails when the package does not exist at path public func resource() -> [Resource] { - let functionResource = [ self._underlying ] + let functionResource = [self._underlying] let additionalQueueResources = self.collectQueueResources() return functionResource + additionalQueueResources @@ -176,7 +175,7 @@ public struct Function: BuilderResource { // 2. the developer supplied value in Function() definition // 3. a default value // func is public for testability - internal static func packagePath(name: String, codeUri: String?) throws -> String { + static func packagePath(name: String, codeUri: String?) throws -> String { // propose a default path unless the --archive-path argument was used // --archive-path argument value must match the value given to the archive plugin --output-path argument var lambdaPackage = @@ -239,20 +238,20 @@ public struct Function: BuilderResource { public static func buildBlock(_ description: String) -> ( String?, EventSources, [String: String] ) { - return (description, EventSources.none, [:]) + (description, EventSources.none, [:]) } public static func buildBlock( _ description: String, _ events: EventSources ) -> (String?, EventSources, [String: String]) { - return (description, events, [:]) + (description, events, [:]) } public static func buildBlock(_ events: EventSources) -> ( String?, EventSources, [String: String] ) { - return (nil, events, [:]) + (nil, events, [:]) } public static func buildBlock( @@ -260,14 +259,14 @@ public struct Function: BuilderResource { _ events: EventSources, _ variables: EnvironmentVariables ) -> (String?, EventSources, [String: String]) { - return (description, events, variables.environmentVariables) + (description, events, variables.environmentVariables) } public static func buildBlock( _ events: EventSources, _ variables: EnvironmentVariables ) -> (String?, EventSources, [String: String]) { - return (nil, events, variables.environmentVariables) + (nil, events, variables.environmentVariables) } public static func buildBlock( @@ -275,14 +274,14 @@ public struct Function: BuilderResource { _ variables: EnvironmentVariables, _ events: EventSources ) -> (String?, EventSources, [String: String]) { - return (description, events, variables.environmentVariables) + (description, events, variables.environmentVariables) } public static func buildBlock( _ variables: EnvironmentVariables, _ events: EventSources ) -> (String?, EventSources, [String: String]) { - return (nil, events, variables.environmentVariables) + (nil, events, variables.environmentVariables) } @available(*, unavailable, message: "Only one EnvironmentVariables block is allowed") @@ -321,7 +320,7 @@ public struct Function: BuilderResource { public func ephemeralStorage(_ size: Int = 512) -> Function { var properties = properties() properties.ephemeralStorage = ServerlessFunctionProperties.EphemeralStorage(size) - return Function(name(), properties: properties) + return Function(self.name(), properties: properties) } private func getDestinations(onSuccess: Arn, onFailure: Arn) @@ -395,7 +394,7 @@ public struct Function: BuilderResource { maximumEventAgeInSeconds: maximumEventAgeInSeconds, maximumRetryAttempts: maximumRetryAttempts ) - return Function(name(), properties: properties) + return Function(self.name(), properties: properties) } // TODO: Add support for references to other resources (SNS, EventBridge) @@ -425,7 +424,7 @@ public struct Function: BuilderResource { maximumEventAgeInSeconds: maximumEventAgeInSeconds, maximumRetryAttempts: maximumRetryAttempts ) - return Function(name(), properties: properties) + return Function(self.name(), properties: properties) } public func fileSystem(_ arn: String, mountPoint: String) -> Function { @@ -441,7 +440,7 @@ public struct Function: BuilderResource { properties.fileSystemConfigs = [newConfig] } } - return Function(name(), properties: properties) + return Function(self.name(), properties: properties) } public func urlConfig( @@ -500,12 +499,13 @@ public struct Function: BuilderResource { ) var properties = properties() properties.functionUrlConfig = urlConfig - return Function(name(), properties: properties) + return Function(self.name(), properties: properties) } private func properties() -> ServerlessFunctionProperties { - self._underlying.properties as! ServerlessFunctionProperties + self._underlying.properties as! ServerlessFunctionProperties } + private func name() -> String { self._underlying.name } } @@ -600,7 +600,7 @@ public struct EventSources { self.eventSources = builder() } - internal func samEventSources() -> [Resource] { + func samEventSources() -> [Resource] { self.eventSources } @@ -644,7 +644,7 @@ public struct HttpApi { self.path = path } - internal func resource() -> Resource { + func resource() -> Resource { var properties: SAMResourceProperties? if self.method != nil || self.path != nil { properties = HttpApiProperties(method: self.method, path: self.path) @@ -700,7 +700,7 @@ public struct Sqs { return Sqs(name: self.name, queue) } - internal func resource() -> Resource { + func resource() -> Resource { var properties: SQSEventProperties! if self.queue != nil { properties = SQSEventProperties( @@ -730,7 +730,7 @@ public struct Sqs { // MARK: Environment Variable public struct EnvironmentVariables { - internal let environmentVariables: [String: String] + let environmentVariables: [String: String] // MARK: EnvironmentVariable DSL code @@ -744,7 +744,7 @@ public struct EnvironmentVariables { // merge an array of dictionaries into a single dictionary. // existing values are preserved var mergedDictKeepCurrent: [String: String] = [:] - variables.forEach { dict in + for dict in variables { mergedDictKeepCurrent = mergedDictKeepCurrent.merging(dict) { current, _ in current } } return mergedDictKeepCurrent @@ -753,7 +753,8 @@ public struct EnvironmentVariables { } // MARK: Queue top level resource -//TODO : do we really need two Queue and Sqs struct ? + +// TODO: do we really need two Queue and Sqs struct ? public struct Queue: BuilderResource { private let _underlying: Resource @@ -767,10 +768,11 @@ public struct Queue: BuilderResource { ) } - public func resource() -> [Resource] { [_underlying] } + public func resource() -> [Resource] { [self._underlying] } } // MARK: Table top level resource + public struct Table: BuilderResource { private let _underlying: Resource private init( @@ -801,7 +803,7 @@ public struct Table: BuilderResource { self.init(logicalName: logicalName, properties: properties) } - public func resource() -> [Resource] { [ self._underlying ] } + public func resource() -> [Resource] { [self._underlying] } public func provisionedThroughput(readCapacityUnits: Int, writeCapacityUnits: Int) -> Table { var properties = self._underlying.properties as! SimpleTableProperties // use as! is safe, it it fails, it is a programming error @@ -819,7 +821,7 @@ public struct Table: BuilderResource { // MARK: Serialization code extension SAMDeploymentDescriptor { - internal func toJSON(pretty: Bool = true) -> String { + func toJSON(pretty: Bool = true) -> String { let encoder = JSONEncoder() encoder.outputFormatting = [.withoutEscapingSlashes] if pretty { @@ -829,7 +831,7 @@ extension SAMDeploymentDescriptor { return String(decoding: jsonData, as: UTF8.self) } - internal func toYAML() -> String { + func toYAML() -> String { let encoder = YAMLEncoder() encoder.keyEncodingStrategy = .camelCase let yamlData = try! encoder.encode(self) diff --git a/Sources/AWSLambdaDeploymentDescriptor/FileDigest.swift b/Sources/AWSLambdaDeploymentDescriptor/FileDigest.swift index 6a63b25..d54fd06 100644 --- a/Sources/AWSLambdaDeploymentDescriptor/FileDigest.swift +++ b/Sources/AWSLambdaDeploymentDescriptor/FileDigest.swift @@ -16,57 +16,55 @@ import CryptoKit import Foundation class FileDigest { + public static func hex(from filePath: String?) -> String? { + guard let fp = filePath else { + return nil + } - public static func hex(from filePath: String?) -> String? { - guard let fp = filePath else { - return nil + return try? FileDigest().update(path: fp).finalize() } - return try? FileDigest().update(path: fp).finalize() - } - - enum InputStreamError: Error { - case createFailed(String) - case readFailed - } + enum InputStreamError: Error { + case createFailed(String) + case readFailed + } - private var digest = SHA256() + private var digest = SHA256() - func update(path: String) throws -> FileDigest { - guard let inputStream = InputStream(fileAtPath: path) else { - throw InputStreamError.createFailed(path) + func update(path: String) throws -> FileDigest { + guard let inputStream = InputStream(fileAtPath: path) else { + throw InputStreamError.createFailed(path) + } + return try self.update(inputStream: inputStream) } - return try update(inputStream: inputStream) - } - private func update(inputStream: InputStream) throws -> FileDigest { - inputStream.open() - defer { - inputStream.close() - } + private func update(inputStream: InputStream) throws -> FileDigest { + inputStream.open() + defer { + inputStream.close() + } - let bufferSize = 4096 - let buffer = UnsafeMutablePointer.allocate(capacity: bufferSize) - var bytesRead = inputStream.read(buffer, maxLength: bufferSize) - while bytesRead > 0 { - self.update(bytes: buffer, length: bytesRead) - bytesRead = inputStream.read(buffer, maxLength: bufferSize) - } - if bytesRead < 0 { - // Stream error occured - throw (inputStream.streamError ?? InputStreamError.readFailed) + let bufferSize = 4096 + let buffer = UnsafeMutablePointer.allocate(capacity: bufferSize) + var bytesRead = inputStream.read(buffer, maxLength: bufferSize) + while bytesRead > 0 { + self.update(bytes: buffer, length: bytesRead) + bytesRead = inputStream.read(buffer, maxLength: bufferSize) + } + if bytesRead < 0 { + // Stream error occured + throw (inputStream.streamError ?? InputStreamError.readFailed) + } + return self } - return self - } - private func update(bytes: UnsafeMutablePointer, length: Int) { - let data = Data(bytes: bytes, count: length) - digest.update(data: data) - } - - func finalize() -> String { - let digest = digest.finalize() - return digest.compactMap { String(format: "%02x", $0) }.joined() - } + private func update(bytes: UnsafeMutablePointer, length: Int) { + let data = Data(bytes: bytes, count: length) + self.digest.update(data: data) + } + func finalize() -> String { + let digest = digest.finalize() + return digest.compactMap { String(format: "%02x", $0) }.joined() + } } diff --git a/Sources/AWSLambdaDeploymentDescriptor/YAMLEncoder.swift b/Sources/AWSLambdaDeploymentDescriptor/YAMLEncoder.swift index 967ecc4..d061437 100644 --- a/Sources/AWSLambdaDeploymentDescriptor/YAMLEncoder.swift +++ b/Sources/AWSLambdaDeploymentDescriptor/YAMLEncoder.swift @@ -30,1102 +30,1100 @@ extension Dictionary: _YAMLStringDictionaryEncodableMarker where Key == String, /// `YAMLEncoder` facilitates the encoding of `Encodable` values into YAML. open class YAMLEncoder { - // MARK: Options + // MARK: Options - /// The formatting of the output YAML data. - public struct OutputFormatting: OptionSet { - /// The format's default value. - public let rawValue: UInt + /// The formatting of the output YAML data. + public struct OutputFormatting: OptionSet { + /// The format's default value. + public let rawValue: UInt - /// Creates an OutputFormatting value with the given raw value. - public init(rawValue: UInt) { - self.rawValue = rawValue + /// Creates an OutputFormatting value with the given raw value. + public init(rawValue: UInt) { + self.rawValue = rawValue + } + + /// Produce human-readable YAML with indented output. + // public static let prettyPrinted = OutputFormatting(rawValue: 1 << 0) + + /// Produce JSON with dictionary keys sorted in lexicographic order. + public static let sortedKeys = OutputFormatting(rawValue: 1 << 1) + + /// By default slashes get escaped ("/" → "\/", "http://apple.com/" → "http:\/\/apple.com\/") + /// for security reasons, allowing outputted YAML to be safely embedded within HTML/XML. + /// In contexts where this escaping is unnecessary, the YAML is known to not be embedded, + /// or is intended only for display, this option avoids this escaping. + public static let withoutEscapingSlashes = OutputFormatting(rawValue: 1 << 3) } - /// Produce human-readable YAML with indented output. - // public static let prettyPrinted = OutputFormatting(rawValue: 1 << 0) + /// The strategy to use for encoding `Date` values. + public enum DateEncodingStrategy { + /// Defer to `Date` for choosing an encoding. This is the default strategy. + case deferredToDate - /// Produce JSON with dictionary keys sorted in lexicographic order. - public static let sortedKeys = OutputFormatting(rawValue: 1 << 1) + /// Encode the `Date` as a UNIX timestamp (as a YAML number). + case secondsSince1970 - /// By default slashes get escaped ("/" → "\/", "http://apple.com/" → "http:\/\/apple.com\/") - /// for security reasons, allowing outputted YAML to be safely embedded within HTML/XML. - /// In contexts where this escaping is unnecessary, the YAML is known to not be embedded, - /// or is intended only for display, this option avoids this escaping. - public static let withoutEscapingSlashes = OutputFormatting(rawValue: 1 << 3) - } + /// Encode the `Date` as UNIX millisecond timestamp (as a YAML number). + case millisecondsSince1970 - /// The strategy to use for encoding `Date` values. - public enum DateEncodingStrategy { - /// Defer to `Date` for choosing an encoding. This is the default strategy. - case deferredToDate + /// Encode the `Date` as an ISO-8601-formatted string (in RFC 3339 format). + @available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) + case iso8601 - /// Encode the `Date` as a UNIX timestamp (as a YAML number). - case secondsSince1970 + /// Encode the `Date` as a string formatted by the given formatter. + case formatted(DateFormatter) - /// Encode the `Date` as UNIX millisecond timestamp (as a YAML number). - case millisecondsSince1970 + /// Encode the `Date` as a custom value encoded by the given closure. + /// + /// If the closure fails to encode a value into the given encoder, the encoder will encode an empty automatic container in its place. + case custom((Date, Encoder) throws -> Void) + } - /// Encode the `Date` as an ISO-8601-formatted string (in RFC 3339 format). - @available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) - case iso8601 + /// The strategy to use for encoding `Data` values. + public enum DataEncodingStrategy { + /// Defer to `Data` for choosing an encoding. + case deferredToData - /// Encode the `Date` as a string formatted by the given formatter. - case formatted(DateFormatter) + /// Encoded the `Data` as a Base64-encoded string. This is the default strategy. + case base64 - /// Encode the `Date` as a custom value encoded by the given closure. - /// - /// If the closure fails to encode a value into the given encoder, the encoder will encode an empty automatic container in its place. - case custom((Date, Encoder) throws -> Void) - } + /// Encode the `Data` as a custom value encoded by the given closure. + /// + /// If the closure fails to encode a value into the given encoder, the encoder will encode an empty automatic container in its place. + case custom((Data, Encoder) throws -> Void) + } - /// The strategy to use for encoding `Data` values. - public enum DataEncodingStrategy { - /// Defer to `Data` for choosing an encoding. - case deferredToData + /// The strategy to use for non-YAML-conforming floating-point values (IEEE 754 infinity and NaN). + public enum NonConformingFloatEncodingStrategy { + /// Throw upon encountering non-conforming values. This is the default strategy. + case `throw` - /// Encoded the `Data` as a Base64-encoded string. This is the default strategy. - case base64 + /// Encode the values using the given representation strings. + case convertToString(positiveInfinity: String, negativeInfinity: String, nan: String) + } - /// Encode the `Data` as a custom value encoded by the given closure. - /// - /// If the closure fails to encode a value into the given encoder, the encoder will encode an empty automatic container in its place. - case custom((Data, Encoder) throws -> Void) - } - - /// The strategy to use for non-YAML-conforming floating-point values (IEEE 754 infinity and NaN). - public enum NonConformingFloatEncodingStrategy { - /// Throw upon encountering non-conforming values. This is the default strategy. - case `throw` - - /// Encode the values using the given representation strings. - case convertToString(positiveInfinity: String, negativeInfinity: String, nan: String) - } - - /// The strategy to use for automatically changing the value of keys before encoding. - public enum KeyEncodingStrategy { - /// Use the keys specified by each type. This is the default strategy. - case useDefaultKeys - - /// convert keyname to camel case. - /// for example myMaxValue becomes MyMaxValue - case camelCase - - fileprivate static func _convertToCamelCase(_ stringKey: String) -> String { - return stringKey.prefix(1).capitalized + stringKey.dropFirst() - } - } - - /// The output format to produce. Defaults to `withoutEscapingSlashes` for YAML. - open var outputFormatting: OutputFormatting = [OutputFormatting.withoutEscapingSlashes] - - /// The strategy to use in encoding dates. Defaults to `.deferredToDate`. - open var dateEncodingStrategy: DateEncodingStrategy = .deferredToDate - - /// The strategy to use in encoding binary data. Defaults to `.base64`. - open var dataEncodingStrategy: DataEncodingStrategy = .base64 - - /// The strategy to use in encoding non-conforming numbers. Defaults to `.throw`. - open var nonConformingFloatEncodingStrategy: NonConformingFloatEncodingStrategy = .throw - - /// The strategy to use for encoding keys. Defaults to `.useDefaultKeys`. - open var keyEncodingStrategy: KeyEncodingStrategy = .useDefaultKeys - - /// Contextual user-provided information for use during encoding. - open var userInfo: [CodingUserInfoKey: Any] = [:] - - /// the number of space characters for a single indent - public static let singleIndent: Int = 3 - - /// Options set on the top-level encoder to pass down the encoding hierarchy. - fileprivate struct _Options { - let dateEncodingStrategy: DateEncodingStrategy - let dataEncodingStrategy: DataEncodingStrategy - let nonConformingFloatEncodingStrategy: NonConformingFloatEncodingStrategy - let keyEncodingStrategy: KeyEncodingStrategy - let userInfo: [CodingUserInfoKey: Any] - } - - /// The options set on the top-level encoder. - fileprivate var options: _Options { - return _Options( - dateEncodingStrategy: dateEncodingStrategy, - dataEncodingStrategy: dataEncodingStrategy, - nonConformingFloatEncodingStrategy: nonConformingFloatEncodingStrategy, - keyEncodingStrategy: keyEncodingStrategy, - userInfo: userInfo) - } - - // MARK: - Constructing a YAML Encoder - - /// Initializes `self` with default strategies. - public init() {} - - // MARK: - Encoding Values - - /// Encodes the given top-level value and returns its YAML representation. - /// - /// - parameter value: The value to encode. - /// - returns: A new `Data` value containing the encoded YAML data. - /// - throws: `EncodingError.invalidValue` if a non-conforming floating-point value is encountered during encoding, and the encoding strategy is `.throw`. - /// - throws: An error if any value throws an error during encoding. - open func encode(_ value: T) throws -> Data { - let value: YAMLValue = try encodeAsYAMLValue(value) - let writer = YAMLValue.Writer(options: self.outputFormatting) - let bytes = writer.writeValue(value) - - return Data(bytes) - } - - func encodeAsYAMLValue(_ value: T) throws -> YAMLValue { - let encoder = YAMLEncoderImpl(options: self.options, codingPath: []) - guard let topLevel = try encoder.wrapEncodable(value, for: nil) else { - throw EncodingError.invalidValue( - value, - EncodingError.Context( - codingPath: [], debugDescription: "Top-level \(T.self) did not encode any values.")) - } - - return topLevel - } -} + /// The strategy to use for automatically changing the value of keys before encoding. + public enum KeyEncodingStrategy { + /// Use the keys specified by each type. This is the default strategy. + case useDefaultKeys -// MARK: - _YAMLEncoder + /// convert keyname to camel case. + /// for example myMaxValue becomes MyMaxValue + case camelCase -private enum YAMLFuture { - case value(YAMLValue) - case encoder(YAMLEncoderImpl) - case nestedArray(RefArray) - case nestedObject(RefObject) + fileprivate static func _convertToCamelCase(_ stringKey: String) -> String { + stringKey.prefix(1).capitalized + stringKey.dropFirst() + } + } + + /// The output format to produce. Defaults to `withoutEscapingSlashes` for YAML. + open var outputFormatting: OutputFormatting = [OutputFormatting.withoutEscapingSlashes] + + /// The strategy to use in encoding dates. Defaults to `.deferredToDate`. + open var dateEncodingStrategy: DateEncodingStrategy = .deferredToDate + + /// The strategy to use in encoding binary data. Defaults to `.base64`. + open var dataEncodingStrategy: DataEncodingStrategy = .base64 + + /// The strategy to use in encoding non-conforming numbers. Defaults to `.throw`. + open var nonConformingFloatEncodingStrategy: NonConformingFloatEncodingStrategy = .throw + + /// The strategy to use for encoding keys. Defaults to `.useDefaultKeys`. + open var keyEncodingStrategy: KeyEncodingStrategy = .useDefaultKeys - class RefArray { - private(set) var array: [YAMLFuture] = [] + /// Contextual user-provided information for use during encoding. + open var userInfo: [CodingUserInfoKey: Any] = [:] - init() { - self.array.reserveCapacity(10) + /// the number of space characters for a single indent + public static let singleIndent: Int = 3 + + /// Options set on the top-level encoder to pass down the encoding hierarchy. + fileprivate struct _Options { + let dateEncodingStrategy: DateEncodingStrategy + let dataEncodingStrategy: DataEncodingStrategy + let nonConformingFloatEncodingStrategy: NonConformingFloatEncodingStrategy + let keyEncodingStrategy: KeyEncodingStrategy + let userInfo: [CodingUserInfoKey: Any] } - @inline(__always) func append(_ element: YAMLValue) { - self.array.append(.value(element)) + /// The options set on the top-level encoder. + fileprivate var options: _Options { + _Options( + dateEncodingStrategy: self.dateEncodingStrategy, + dataEncodingStrategy: self.dataEncodingStrategy, + nonConformingFloatEncodingStrategy: self.nonConformingFloatEncodingStrategy, + keyEncodingStrategy: self.keyEncodingStrategy, + userInfo: self.userInfo + ) } - @inline(__always) func append(_ encoder: YAMLEncoderImpl) { - self.array.append(.encoder(encoder)) + // MARK: - Constructing a YAML Encoder + + /// Initializes `self` with default strategies. + public init() {} + + // MARK: - Encoding Values + + /// Encodes the given top-level value and returns its YAML representation. + /// + /// - parameter value: The value to encode. + /// - returns: A new `Data` value containing the encoded YAML data. + /// - throws: `EncodingError.invalidValue` if a non-conforming floating-point value is encountered during encoding, and the encoding strategy is `.throw`. + /// - throws: An error if any value throws an error during encoding. + open func encode(_ value: T) throws -> Data { + let value: YAMLValue = try encodeAsYAMLValue(value) + let writer = YAMLValue.Writer(options: self.outputFormatting) + let bytes = writer.writeValue(value) + + return Data(bytes) } - @inline(__always) func appendArray() -> RefArray { - let array = RefArray() - self.array.append(.nestedArray(array)) - return array + func encodeAsYAMLValue(_ value: T) throws -> YAMLValue { + let encoder = YAMLEncoderImpl(options: self.options, codingPath: []) + guard let topLevel = try encoder.wrapEncodable(value, for: nil) else { + throw EncodingError.invalidValue( + value, + EncodingError.Context( + codingPath: [], debugDescription: "Top-level \(T.self) did not encode any values." + ) + ) + } + + return topLevel } +} + +// MARK: - _YAMLEncoder + +private enum YAMLFuture { + case value(YAMLValue) + case encoder(YAMLEncoderImpl) + case nestedArray(RefArray) + case nestedObject(RefObject) - @inline(__always) func appendObject() -> RefObject { - let object = RefObject() - self.array.append(.nestedObject(object)) - return object + class RefArray { + private(set) var array: [YAMLFuture] = [] + + init() { + self.array.reserveCapacity(10) + } + + @inline(__always) func append(_ element: YAMLValue) { + self.array.append(.value(element)) + } + + @inline(__always) func append(_ encoder: YAMLEncoderImpl) { + self.array.append(.encoder(encoder)) + } + + @inline(__always) func appendArray() -> RefArray { + let array = RefArray() + self.array.append(.nestedArray(array)) + return array + } + + @inline(__always) func appendObject() -> RefObject { + let object = RefObject() + self.array.append(.nestedObject(object)) + return object + } + + var values: [YAMLValue] { + self.array.map { future -> YAMLValue in + switch future { + case .value(let value): + return value + case .nestedArray(let array): + return .array(array.values) + case .nestedObject(let object): + return .object(object.values) + case .encoder(let encoder): + return encoder.value ?? .object([:]) + } + } + } } - var values: [YAMLValue] { - self.array.map { (future) -> YAMLValue in - switch future { - case .value(let value): - return value - case .nestedArray(let array): - return .array(array.values) - case .nestedObject(let object): - return .object(object.values) - case .encoder(let encoder): - return encoder.value ?? .object([:]) + class RefObject { + private(set) var dict: [String: YAMLFuture] = [:] + + init() { + self.dict.reserveCapacity(20) + } + + @inline(__always) func set(_ value: YAMLValue, for key: String) { + self.dict[key] = .value(value) + } + + @inline(__always) func setArray(for key: String) -> RefArray { + switch self.dict[key] { + case .encoder: + preconditionFailure("For key \"\(key)\" an encoder has already been created.") + case .nestedObject: + preconditionFailure("For key \"\(key)\" a keyed container has already been created.") + case .nestedArray(let array): + return array + case .none, .value: + let array = RefArray() + self.dict[key] = .nestedArray(array) + return array + } + } + + @inline(__always) func setObject(for key: String) -> RefObject { + switch self.dict[key] { + case .encoder: + preconditionFailure("For key \"\(key)\" an encoder has already been created.") + case .nestedObject(let object): + return object + case .nestedArray: + preconditionFailure("For key \"\(key)\" a unkeyed container has already been created.") + case .none, .value: + let object = RefObject() + self.dict[key] = .nestedObject(object) + return object + } + } + + @inline(__always) func set(_ encoder: YAMLEncoderImpl, for key: String) { + switch self.dict[key] { + case .encoder: + preconditionFailure("For key \"\(key)\" an encoder has already been created.") + case .nestedObject: + preconditionFailure("For key \"\(key)\" a keyed container has already been created.") + case .nestedArray: + preconditionFailure("For key \"\(key)\" a unkeyed container has already been created.") + case .none, .value: + self.dict[key] = .encoder(encoder) + } } - } - } - } - - class RefObject { - private(set) var dict: [String: YAMLFuture] = [:] - - init() { - self.dict.reserveCapacity(20) - } - - @inline(__always) func set(_ value: YAMLValue, for key: String) { - self.dict[key] = .value(value) - } - - @inline(__always) func setArray(for key: String) -> RefArray { - switch self.dict[key] { - case .encoder: - preconditionFailure("For key \"\(key)\" an encoder has already been created.") - case .nestedObject: - preconditionFailure("For key \"\(key)\" a keyed container has already been created.") - case .nestedArray(let array): - return array - case .none, .value: - let array = RefArray() - dict[key] = .nestedArray(array) - return array - } - } - - @inline(__always) func setObject(for key: String) -> RefObject { - switch self.dict[key] { - case .encoder: - preconditionFailure("For key \"\(key)\" an encoder has already been created.") - case .nestedObject(let object): - return object - case .nestedArray: - preconditionFailure("For key \"\(key)\" a unkeyed container has already been created.") - case .none, .value: - let object = RefObject() - dict[key] = .nestedObject(object) - return object - } - } - - @inline(__always) func set(_ encoder: YAMLEncoderImpl, for key: String) { - switch self.dict[key] { - case .encoder: - preconditionFailure("For key \"\(key)\" an encoder has already been created.") - case .nestedObject: - preconditionFailure("For key \"\(key)\" a keyed container has already been created.") - case .nestedArray: - preconditionFailure("For key \"\(key)\" a unkeyed container has already been created.") - case .none, .value: - dict[key] = .encoder(encoder) - } - } - - var values: [String: YAMLValue] { - self.dict.mapValues { (future) -> YAMLValue in - switch future { - case .value(let value): - return value - case .nestedArray(let array): - return .array(array.values) - case .nestedObject(let object): - return .object(object.values) - case .encoder(let encoder): - return encoder.value ?? .object([:]) + + var values: [String: YAMLValue] { + self.dict.mapValues { future -> YAMLValue in + switch future { + case .value(let value): + return value + case .nestedArray(let array): + return .array(array.values) + case .nestedObject(let object): + return .object(object.values) + case .encoder(let encoder): + return encoder.value ?? .object([:]) + } + } } - } } - } } private class YAMLEncoderImpl { - let options: YAMLEncoder._Options - let codingPath: [CodingKey] - var userInfo: [CodingUserInfoKey: Any] { - options.userInfo - } - - var singleValue: YAMLValue? - var array: YAMLFuture.RefArray? - var object: YAMLFuture.RefObject? - - var value: YAMLValue? { - if let object = self.object { - return .object(object.values) - } - if let array = self.array { - return .array(array.values) - } - return self.singleValue - } - - init(options: YAMLEncoder._Options, codingPath: [CodingKey]) { - self.options = options - self.codingPath = codingPath - } -} + let options: YAMLEncoder._Options + let codingPath: [CodingKey] + var userInfo: [CodingUserInfoKey: Any] { + self.options.userInfo + } -extension YAMLEncoderImpl: Encoder { - func container(keyedBy _: Key.Type) -> KeyedEncodingContainer where Key: CodingKey { - if let _ = object { - let container = YAMLKeyedEncodingContainer(impl: self, codingPath: codingPath) - return KeyedEncodingContainer(container) + var singleValue: YAMLValue? + var array: YAMLFuture.RefArray? + var object: YAMLFuture.RefObject? + + var value: YAMLValue? { + if let object = self.object { + return .object(object.values) + } + if let array = self.array { + return .array(array.values) + } + return self.singleValue } - guard self.singleValue == nil, self.array == nil else { - preconditionFailure() + init(options: YAMLEncoder._Options, codingPath: [CodingKey]) { + self.options = options + self.codingPath = codingPath } +} - self.object = YAMLFuture.RefObject() - let container = YAMLKeyedEncodingContainer(impl: self, codingPath: codingPath) - return KeyedEncodingContainer(container) - } +extension YAMLEncoderImpl: Encoder { + func container(keyedBy _: Key.Type) -> KeyedEncodingContainer where Key: CodingKey { + if let _ = object { + let container = YAMLKeyedEncodingContainer(impl: self, codingPath: codingPath) + return KeyedEncodingContainer(container) + } - func unkeyedContainer() -> UnkeyedEncodingContainer { - if let _ = array { - return YAMLUnkeyedEncodingContainer(impl: self, codingPath: self.codingPath) - } + guard self.singleValue == nil, self.array == nil else { + preconditionFailure() + } - guard self.singleValue == nil, self.object == nil else { - preconditionFailure() + self.object = YAMLFuture.RefObject() + let container = YAMLKeyedEncodingContainer(impl: self, codingPath: codingPath) + return KeyedEncodingContainer(container) } - self.array = YAMLFuture.RefArray() - return YAMLUnkeyedEncodingContainer(impl: self, codingPath: self.codingPath) - } + func unkeyedContainer() -> UnkeyedEncodingContainer { + if let _ = array { + return YAMLUnkeyedEncodingContainer(impl: self, codingPath: self.codingPath) + } + + guard self.singleValue == nil, self.object == nil else { + preconditionFailure() + } - func singleValueContainer() -> SingleValueEncodingContainer { - guard self.object == nil, self.array == nil else { - preconditionFailure() + self.array = YAMLFuture.RefArray() + return YAMLUnkeyedEncodingContainer(impl: self, codingPath: self.codingPath) } - return YAMLSingleValueEncodingContainer(impl: self, codingPath: self.codingPath) - } + func singleValueContainer() -> SingleValueEncodingContainer { + guard self.object == nil, self.array == nil else { + preconditionFailure() + } + + return YAMLSingleValueEncodingContainer(impl: self, codingPath: self.codingPath) + } } // this is a private protocol to implement convenience methods directly on the EncodingContainers extension YAMLEncoderImpl: _SpecialTreatmentEncoder { - var impl: YAMLEncoderImpl { - return self - } - - // untyped escape hatch. needed for `wrapObject` - func wrapUntyped(_ encodable: Encodable) throws -> YAMLValue { - switch encodable { - case let date as Date: - return try self.wrapDate(date, for: nil) - case let data as Data: - return try self.wrapData(data, for: nil) - case let url as URL: - return .string(url.absoluteString) - case let decimal as Decimal: - return .number(decimal.description) - case let object as [String: Encodable]: // this emits a warning, but it works perfectly - return try self.wrapObject(object, for: nil) - default: - try encodable.encode(to: self) - return self.value ?? .object([:]) - } - } + var impl: YAMLEncoderImpl { + self + } + + // untyped escape hatch. needed for `wrapObject` + func wrapUntyped(_ encodable: Encodable) throws -> YAMLValue { + switch encodable { + case let date as Date: + return try self.wrapDate(date, for: nil) + case let data as Data: + return try self.wrapData(data, for: nil) + case let url as URL: + return .string(url.absoluteString) + case let decimal as Decimal: + return .number(decimal.description) + case let object as [String: Encodable]: // this emits a warning, but it works perfectly + return try self.wrapObject(object, for: nil) + default: + try encodable.encode(to: self) + return self.value ?? .object([:]) + } + } } private protocol _SpecialTreatmentEncoder { - var codingPath: [CodingKey] { get } - var options: YAMLEncoder._Options { get } - var impl: YAMLEncoderImpl { get } + var codingPath: [CodingKey] { get } + var options: YAMLEncoder._Options { get } + var impl: YAMLEncoderImpl { get } } extension _SpecialTreatmentEncoder { - @inline(__always) fileprivate func wrapFloat( - _ float: F, for additionalKey: CodingKey? - ) throws -> YAMLValue { - guard !float.isNaN, !float.isInfinite else { - if case .convertToString(let posInfString, let negInfString, let nanString) = self.options - .nonConformingFloatEncodingStrategy - { - switch float { - case F.infinity: - return .string(posInfString) - case -F.infinity: - return .string(negInfString) + @inline(__always) fileprivate func wrapFloat( + _ float: F, for additionalKey: CodingKey? + ) throws -> YAMLValue { + guard !float.isNaN, !float.isInfinite else { + if case .convertToString(let posInfString, let negInfString, let nanString) = self.options + .nonConformingFloatEncodingStrategy { + switch float { + case F.infinity: + return .string(posInfString) + case -F.infinity: + return .string(negInfString) + default: + // must be nan in this case + return .string(nanString) + } + } + + var path = self.codingPath + if let additionalKey = additionalKey { + path.append(additionalKey) + } + + throw EncodingError.invalidValue( + float, + .init( + codingPath: path, + debugDescription: "Unable to encode \(F.self).\(float) directly in YAML." + ) + ) + } + + var string = float.description + if string.hasSuffix(".0") { + string.removeLast(2) + } + return .number(string) + } + + fileprivate func wrapEncodable(_ encodable: E, for additionalKey: CodingKey?) throws + -> YAMLValue? { + switch encodable { + case let date as Date: + return try self.wrapDate(date, for: additionalKey) + case let data as Data: + return try self.wrapData(data, for: additionalKey) + case let url as URL: + return .string(url.absoluteString) + case let decimal as Decimal: + return .number(decimal.description) + case let object as _YAMLStringDictionaryEncodableMarker: + return try self.wrapObject(object as! [String: Encodable], for: additionalKey) default: - // must be nan in this case - return .string(nanString) + let encoder = self.getEncoder(for: additionalKey) + try encodable.encode(to: encoder) + return encoder.value + } + } + + func wrapDate(_ date: Date, for additionalKey: CodingKey?) throws -> YAMLValue { + switch self.options.dateEncodingStrategy { + case .deferredToDate: + let encoder = self.getEncoder(for: additionalKey) + try date.encode(to: encoder) + return encoder.value ?? .null + + case .secondsSince1970: + return .number(date.timeIntervalSince1970.description) + + case .millisecondsSince1970: + return .number((date.timeIntervalSince1970 * 1000).description) + + case .iso8601: + if #available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) { + return .string(_iso8601Formatter.string(from: date)) + } else { + fatalError("ISO8601DateFormatter is unavailable on this platform.") + } + + case .formatted(let formatter): + return .string(formatter.string(from: date)) + + case .custom(let closure): + let encoder = self.getEncoder(for: additionalKey) + try closure(date, encoder) + // The closure didn't encode anything. Return the default keyed container. + return encoder.value ?? .object([:]) + } + } + + func wrapData(_ data: Data, for additionalKey: CodingKey?) throws -> YAMLValue { + switch self.options.dataEncodingStrategy { + case .deferredToData: + let encoder = self.getEncoder(for: additionalKey) + try data.encode(to: encoder) + return encoder.value ?? .null + + case .base64: + let base64 = data.base64EncodedString() + return .string(base64) + + case .custom(let closure): + let encoder = self.getEncoder(for: additionalKey) + try closure(data, encoder) + // The closure didn't encode anything. Return the default keyed container. + return encoder.value ?? .object([:]) + } + } + + func wrapObject(_ object: [String: Encodable], for additionalKey: CodingKey?) throws -> YAMLValue { + var baseCodingPath = self.codingPath + if let additionalKey = additionalKey { + baseCodingPath.append(additionalKey) + } + var result = [String: YAMLValue]() + result.reserveCapacity(object.count) + + try object.forEach { key, value in + var elemCodingPath = baseCodingPath + elemCodingPath.append(_YAMLKey(stringValue: key)) + + let encoder = YAMLEncoderImpl(options: self.options, codingPath: elemCodingPath) + + var convertedKey = key + if self.options.keyEncodingStrategy == .camelCase { + convertedKey = YAMLEncoder.KeyEncodingStrategy._convertToCamelCase(key) + } + result[convertedKey] = try encoder.wrapUntyped(value) } - } - - var path = self.codingPath - if let additionalKey = additionalKey { - path.append(additionalKey) - } - - throw EncodingError.invalidValue( - float, - .init( - codingPath: path, - debugDescription: "Unable to encode \(F.self).\(float) directly in YAML." - )) - } - - var string = float.description - if string.hasSuffix(".0") { - string.removeLast(2) - } - return .number(string) - } - - fileprivate func wrapEncodable(_ encodable: E, for additionalKey: CodingKey?) throws - -> YAMLValue? - { - switch encodable { - case let date as Date: - return try self.wrapDate(date, for: additionalKey) - case let data as Data: - return try self.wrapData(data, for: additionalKey) - case let url as URL: - return .string(url.absoluteString) - case let decimal as Decimal: - return .number(decimal.description) - case let object as _YAMLStringDictionaryEncodableMarker: - return try self.wrapObject(object as! [String: Encodable], for: additionalKey) - default: - let encoder = self.getEncoder(for: additionalKey) - try encodable.encode(to: encoder) - return encoder.value - } - } - - func wrapDate(_ date: Date, for additionalKey: CodingKey?) throws -> YAMLValue { - switch self.options.dateEncodingStrategy { - case .deferredToDate: - let encoder = self.getEncoder(for: additionalKey) - try date.encode(to: encoder) - return encoder.value ?? .null - - case .secondsSince1970: - return .number(date.timeIntervalSince1970.description) - - case .millisecondsSince1970: - return .number((date.timeIntervalSince1970 * 1000).description) - - case .iso8601: - if #available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) { - return .string(_iso8601Formatter.string(from: date)) - } else { - fatalError("ISO8601DateFormatter is unavailable on this platform.") - } - - case .formatted(let formatter): - return .string(formatter.string(from: date)) - - case .custom(let closure): - let encoder = self.getEncoder(for: additionalKey) - try closure(date, encoder) - // The closure didn't encode anything. Return the default keyed container. - return encoder.value ?? .object([:]) - } - } - - func wrapData(_ data: Data, for additionalKey: CodingKey?) throws -> YAMLValue { - switch self.options.dataEncodingStrategy { - case .deferredToData: - let encoder = self.getEncoder(for: additionalKey) - try data.encode(to: encoder) - return encoder.value ?? .null - - case .base64: - let base64 = data.base64EncodedString() - return .string(base64) - - case .custom(let closure): - let encoder = self.getEncoder(for: additionalKey) - try closure(data, encoder) - // The closure didn't encode anything. Return the default keyed container. - return encoder.value ?? .object([:]) - } - } - - func wrapObject(_ object: [String: Encodable], for additionalKey: CodingKey?) throws -> YAMLValue - { - var baseCodingPath = self.codingPath - if let additionalKey = additionalKey { - baseCodingPath.append(additionalKey) - } - var result = [String: YAMLValue]() - result.reserveCapacity(object.count) - - try object.forEach { (key, value) in - var elemCodingPath = baseCodingPath - elemCodingPath.append(_YAMLKey(stringValue:key)) - - let encoder = YAMLEncoderImpl(options: self.options, codingPath: elemCodingPath) - - var convertedKey = key - if self.options.keyEncodingStrategy == .camelCase { - convertedKey = YAMLEncoder.KeyEncodingStrategy._convertToCamelCase(key) - } - result[convertedKey] = try encoder.wrapUntyped(value) - } - - return .object(result) - } - - fileprivate func getEncoder(for additionalKey: CodingKey?) -> YAMLEncoderImpl { - if let additionalKey = additionalKey { - var newCodingPath = self.codingPath - newCodingPath.append(additionalKey) - return YAMLEncoderImpl(options: self.options, codingPath: newCodingPath) - } - - return self.impl - } + + return .object(result) + } + + fileprivate func getEncoder(for additionalKey: CodingKey?) -> YAMLEncoderImpl { + if let additionalKey = additionalKey { + var newCodingPath = self.codingPath + newCodingPath.append(additionalKey) + return YAMLEncoderImpl(options: self.options, codingPath: newCodingPath) + } + + return self.impl + } } private struct YAMLKeyedEncodingContainer: KeyedEncodingContainerProtocol, - _SpecialTreatmentEncoder -{ - typealias Key = K - - let impl: YAMLEncoderImpl - let object: YAMLFuture.RefObject - let codingPath: [CodingKey] - - private var firstValueWritten: Bool = false - fileprivate var options: YAMLEncoder._Options { - return self.impl.options - } - - init(impl: YAMLEncoderImpl, codingPath: [CodingKey]) { - self.impl = impl - self.object = impl.object! - self.codingPath = codingPath - } - - // used for nested containers - init(impl: YAMLEncoderImpl, object: YAMLFuture.RefObject, codingPath: [CodingKey]) { - self.impl = impl - self.object = object - self.codingPath = codingPath - } - - private func _converted(_ key: Key) -> CodingKey { - switch self.options.keyEncodingStrategy { - case .useDefaultKeys: - return key - case .camelCase: - let newKeyString = YAMLEncoder.KeyEncodingStrategy._convertToCamelCase(key.stringValue) - return _YAMLKey(stringValue: newKeyString, intValue: key.intValue) - } - } - - mutating func encodeNil(forKey key: Self.Key) throws { - self.object.set(.null, for: self._converted(key).stringValue) - } - - mutating func encode(_ value: Bool, forKey key: Self.Key) throws { - self.object.set(.bool(value), for: self._converted(key).stringValue) - } - - mutating func encode(_ value: String, forKey key: Self.Key) throws { - self.object.set(.string(value), for: self._converted(key).stringValue) - } - - mutating func encode(_ value: Double, forKey key: Self.Key) throws { - try encodeFloatingPoint(value, key: self._converted(key)) - } - - mutating func encode(_ value: Float, forKey key: Self.Key) throws { - try encodeFloatingPoint(value, key: self._converted(key)) - } - - mutating func encode(_ value: Int, forKey key: Self.Key) throws { - try encodeFixedWidthInteger(value, key: self._converted(key)) - } - - mutating func encode(_ value: Int8, forKey key: Self.Key) throws { - try encodeFixedWidthInteger(value, key: self._converted(key)) - } - - mutating func encode(_ value: Int16, forKey key: Self.Key) throws { - try encodeFixedWidthInteger(value, key: self._converted(key)) - } - - mutating func encode(_ value: Int32, forKey key: Self.Key) throws { - try encodeFixedWidthInteger(value, key: self._converted(key)) - } - - mutating func encode(_ value: Int64, forKey key: Self.Key) throws { - try encodeFixedWidthInteger(value, key: self._converted(key)) - } - - mutating func encode(_ value: UInt, forKey key: Self.Key) throws { - try encodeFixedWidthInteger(value, key: self._converted(key)) - } - - mutating func encode(_ value: UInt8, forKey key: Self.Key) throws { - try encodeFixedWidthInteger(value, key: self._converted(key)) - } - - mutating func encode(_ value: UInt16, forKey key: Self.Key) throws { - try encodeFixedWidthInteger(value, key: self._converted(key)) - } - - mutating func encode(_ value: UInt32, forKey key: Self.Key) throws { - try encodeFixedWidthInteger(value, key: self._converted(key)) - } - - mutating func encode(_ value: UInt64, forKey key: Self.Key) throws { - try encodeFixedWidthInteger(value, key: self._converted(key)) - } - - mutating func encode(_ value: T, forKey key: Self.Key) throws where T: Encodable { - let convertedKey = self._converted(key) - let encoded = try self.wrapEncodable(value, for: convertedKey) - self.object.set(encoded ?? .object([:]), for: convertedKey.stringValue) - } - - mutating func nestedContainer(keyedBy _: NestedKey.Type, forKey key: Self.Key) - -> KeyedEncodingContainer where NestedKey: CodingKey - { - let convertedKey = self._converted(key) - let newPath = self.codingPath + [convertedKey] - let object = self.object.setObject(for: convertedKey.stringValue) - let nestedContainer = YAMLKeyedEncodingContainer( - impl: impl, object: object, codingPath: newPath) - return KeyedEncodingContainer(nestedContainer) - } - - mutating func nestedUnkeyedContainer(forKey key: Self.Key) -> UnkeyedEncodingContainer { - let convertedKey = self._converted(key) - let newPath = self.codingPath + [convertedKey] - let array = self.object.setArray(for: convertedKey.stringValue) - let nestedContainer = YAMLUnkeyedEncodingContainer( - impl: impl, array: array, codingPath: newPath) - return nestedContainer - } - - mutating func superEncoder() -> Encoder { - let newEncoder = self.getEncoder(for: _YAMLKey.super) - self.object.set(newEncoder, for: _YAMLKey.super.stringValue) - return newEncoder - } - - mutating func superEncoder(forKey key: Self.Key) -> Encoder { - let convertedKey = self._converted(key) - let newEncoder = self.getEncoder(for: convertedKey) - self.object.set(newEncoder, for: convertedKey.stringValue) - return newEncoder - } + _SpecialTreatmentEncoder { + typealias Key = K + + let impl: YAMLEncoderImpl + let object: YAMLFuture.RefObject + let codingPath: [CodingKey] + + private var firstValueWritten: Bool = false + fileprivate var options: YAMLEncoder._Options { + self.impl.options + } + + init(impl: YAMLEncoderImpl, codingPath: [CodingKey]) { + self.impl = impl + self.object = impl.object! + self.codingPath = codingPath + } + + // used for nested containers + init(impl: YAMLEncoderImpl, object: YAMLFuture.RefObject, codingPath: [CodingKey]) { + self.impl = impl + self.object = object + self.codingPath = codingPath + } + + private func _converted(_ key: Key) -> CodingKey { + switch self.options.keyEncodingStrategy { + case .useDefaultKeys: + return key + case .camelCase: + let newKeyString = YAMLEncoder.KeyEncodingStrategy._convertToCamelCase(key.stringValue) + return _YAMLKey(stringValue: newKeyString, intValue: key.intValue) + } + } + + mutating func encodeNil(forKey key: Self.Key) throws { + self.object.set(.null, for: self._converted(key).stringValue) + } + + mutating func encode(_ value: Bool, forKey key: Self.Key) throws { + self.object.set(.bool(value), for: self._converted(key).stringValue) + } + + mutating func encode(_ value: String, forKey key: Self.Key) throws { + self.object.set(.string(value), for: self._converted(key).stringValue) + } + + mutating func encode(_ value: Double, forKey key: Self.Key) throws { + try encodeFloatingPoint(value, key: self._converted(key)) + } + + mutating func encode(_ value: Float, forKey key: Self.Key) throws { + try encodeFloatingPoint(value, key: self._converted(key)) + } + + mutating func encode(_ value: Int, forKey key: Self.Key) throws { + try encodeFixedWidthInteger(value, key: self._converted(key)) + } + + mutating func encode(_ value: Int8, forKey key: Self.Key) throws { + try encodeFixedWidthInteger(value, key: self._converted(key)) + } + + mutating func encode(_ value: Int16, forKey key: Self.Key) throws { + try encodeFixedWidthInteger(value, key: self._converted(key)) + } + + mutating func encode(_ value: Int32, forKey key: Self.Key) throws { + try encodeFixedWidthInteger(value, key: self._converted(key)) + } + + mutating func encode(_ value: Int64, forKey key: Self.Key) throws { + try encodeFixedWidthInteger(value, key: self._converted(key)) + } + + mutating func encode(_ value: UInt, forKey key: Self.Key) throws { + try encodeFixedWidthInteger(value, key: self._converted(key)) + } + + mutating func encode(_ value: UInt8, forKey key: Self.Key) throws { + try encodeFixedWidthInteger(value, key: self._converted(key)) + } + + mutating func encode(_ value: UInt16, forKey key: Self.Key) throws { + try encodeFixedWidthInteger(value, key: self._converted(key)) + } + + mutating func encode(_ value: UInt32, forKey key: Self.Key) throws { + try encodeFixedWidthInteger(value, key: self._converted(key)) + } + + mutating func encode(_ value: UInt64, forKey key: Self.Key) throws { + try encodeFixedWidthInteger(value, key: self._converted(key)) + } + + mutating func encode(_ value: T, forKey key: Self.Key) throws where T: Encodable { + let convertedKey = self._converted(key) + let encoded = try self.wrapEncodable(value, for: convertedKey) + self.object.set(encoded ?? .object([:]), for: convertedKey.stringValue) + } + + mutating func nestedContainer(keyedBy _: NestedKey.Type, forKey key: Self.Key) + -> KeyedEncodingContainer where NestedKey: CodingKey { + let convertedKey = self._converted(key) + let newPath = self.codingPath + [convertedKey] + let object = self.object.setObject(for: convertedKey.stringValue) + let nestedContainer = YAMLKeyedEncodingContainer( + impl: impl, object: object, codingPath: newPath + ) + return KeyedEncodingContainer(nestedContainer) + } + + mutating func nestedUnkeyedContainer(forKey key: Self.Key) -> UnkeyedEncodingContainer { + let convertedKey = self._converted(key) + let newPath = self.codingPath + [convertedKey] + let array = self.object.setArray(for: convertedKey.stringValue) + let nestedContainer = YAMLUnkeyedEncodingContainer( + impl: impl, array: array, codingPath: newPath + ) + return nestedContainer + } + + mutating func superEncoder() -> Encoder { + let newEncoder = self.getEncoder(for: _YAMLKey.super) + self.object.set(newEncoder, for: _YAMLKey.super.stringValue) + return newEncoder + } + + mutating func superEncoder(forKey key: Self.Key) -> Encoder { + let convertedKey = self._converted(key) + let newEncoder = self.getEncoder(for: convertedKey) + self.object.set(newEncoder, for: convertedKey.stringValue) + return newEncoder + } } extension YAMLKeyedEncodingContainer { - @inline(__always) - private mutating func encodeFloatingPoint( - _ float: F, key: CodingKey - ) throws { - let value = try self.wrapFloat(float, for: key) - self.object.set(value, for: key.stringValue) - } - - @inline(__always) private mutating func encodeFixedWidthInteger( - _ value: N, key: CodingKey - ) throws { - self.object.set(.number(value.description), for: key.stringValue) - } + @inline(__always) + private mutating func encodeFloatingPoint( + _ float: F, key: CodingKey + ) throws { + let value = try self.wrapFloat(float, for: key) + self.object.set(value, for: key.stringValue) + } + + @inline(__always) private mutating func encodeFixedWidthInteger( + _ value: N, key: CodingKey + ) throws { + self.object.set(.number(value.description), for: key.stringValue) + } } private struct YAMLUnkeyedEncodingContainer: UnkeyedEncodingContainer, _SpecialTreatmentEncoder { - let impl: YAMLEncoderImpl - let array: YAMLFuture.RefArray - let codingPath: [CodingKey] - - var count: Int { - self.array.array.count - } - private var firstValueWritten: Bool = false - fileprivate var options: YAMLEncoder._Options { - return self.impl.options - } - - init(impl: YAMLEncoderImpl, codingPath: [CodingKey]) { - self.impl = impl - self.array = impl.array! - self.codingPath = codingPath - } - - // used for nested containers - init(impl: YAMLEncoderImpl, array: YAMLFuture.RefArray, codingPath: [CodingKey]) { - self.impl = impl - self.array = array - self.codingPath = codingPath - } - - mutating func encodeNil() throws { - self.array.append(.null) - } - - mutating func encode(_ value: Bool) throws { - self.array.append(.bool(value)) - } - - mutating func encode(_ value: String) throws { - self.array.append(.string(value)) - } - - mutating func encode(_ value: Double) throws { - try encodeFloatingPoint(value) - } - - mutating func encode(_ value: Float) throws { - try encodeFloatingPoint(value) - } - - mutating func encode(_ value: Int) throws { - try encodeFixedWidthInteger(value) - } - - mutating func encode(_ value: Int8) throws { - try encodeFixedWidthInteger(value) - } - - mutating func encode(_ value: Int16) throws { - try encodeFixedWidthInteger(value) - } - - mutating func encode(_ value: Int32) throws { - try encodeFixedWidthInteger(value) - } - - mutating func encode(_ value: Int64) throws { - try encodeFixedWidthInteger(value) - } - - mutating func encode(_ value: UInt) throws { - try encodeFixedWidthInteger(value) - } - - mutating func encode(_ value: UInt8) throws { - try encodeFixedWidthInteger(value) - } - - mutating func encode(_ value: UInt16) throws { - try encodeFixedWidthInteger(value) - } - - mutating func encode(_ value: UInt32) throws { - try encodeFixedWidthInteger(value) - } - - mutating func encode(_ value: UInt64) throws { - try encodeFixedWidthInteger(value) - } - - mutating func encode(_ value: T) throws where T: Encodable { - let key = _YAMLKey(stringValue: "Index \(self.count)", intValue: self.count) - let encoded = try self.wrapEncodable(value, for: key) - self.array.append(encoded ?? .object([:])) - } - - mutating func nestedContainer(keyedBy _: NestedKey.Type) -> KeyedEncodingContainer< - NestedKey - > where NestedKey: CodingKey { - let newPath = self.codingPath + [_YAMLKey(index: self.count)] - let object = self.array.appendObject() - let nestedContainer = YAMLKeyedEncodingContainer( - impl: impl, object: object, codingPath: newPath) - return KeyedEncodingContainer(nestedContainer) - } - - mutating func nestedUnkeyedContainer() -> UnkeyedEncodingContainer { - let newPath = self.codingPath + [_YAMLKey(index: self.count)] - let array = self.array.appendArray() - let nestedContainer = YAMLUnkeyedEncodingContainer( - impl: impl, array: array, codingPath: newPath) - return nestedContainer - } - - mutating func superEncoder() -> Encoder { - let encoder = self.getEncoder(for: _YAMLKey(index: self.count)) - self.array.append(encoder) - return encoder - } + let impl: YAMLEncoderImpl + let array: YAMLFuture.RefArray + let codingPath: [CodingKey] + + var count: Int { + self.array.array.count + } + + private var firstValueWritten: Bool = false + fileprivate var options: YAMLEncoder._Options { + self.impl.options + } + + init(impl: YAMLEncoderImpl, codingPath: [CodingKey]) { + self.impl = impl + self.array = impl.array! + self.codingPath = codingPath + } + + // used for nested containers + init(impl: YAMLEncoderImpl, array: YAMLFuture.RefArray, codingPath: [CodingKey]) { + self.impl = impl + self.array = array + self.codingPath = codingPath + } + + mutating func encodeNil() throws { + self.array.append(.null) + } + + mutating func encode(_ value: Bool) throws { + self.array.append(.bool(value)) + } + + mutating func encode(_ value: String) throws { + self.array.append(.string(value)) + } + + mutating func encode(_ value: Double) throws { + try encodeFloatingPoint(value) + } + + mutating func encode(_ value: Float) throws { + try encodeFloatingPoint(value) + } + + mutating func encode(_ value: Int) throws { + try encodeFixedWidthInteger(value) + } + + mutating func encode(_ value: Int8) throws { + try encodeFixedWidthInteger(value) + } + + mutating func encode(_ value: Int16) throws { + try encodeFixedWidthInteger(value) + } + + mutating func encode(_ value: Int32) throws { + try encodeFixedWidthInteger(value) + } + + mutating func encode(_ value: Int64) throws { + try encodeFixedWidthInteger(value) + } + + mutating func encode(_ value: UInt) throws { + try encodeFixedWidthInteger(value) + } + + mutating func encode(_ value: UInt8) throws { + try encodeFixedWidthInteger(value) + } + + mutating func encode(_ value: UInt16) throws { + try encodeFixedWidthInteger(value) + } + + mutating func encode(_ value: UInt32) throws { + try encodeFixedWidthInteger(value) + } + + mutating func encode(_ value: UInt64) throws { + try encodeFixedWidthInteger(value) + } + + mutating func encode(_ value: T) throws where T: Encodable { + let key = _YAMLKey(stringValue: "Index \(self.count)", intValue: self.count) + let encoded = try self.wrapEncodable(value, for: key) + self.array.append(encoded ?? .object([:])) + } + + mutating func nestedContainer(keyedBy _: NestedKey.Type) -> KeyedEncodingContainer< + NestedKey + > where NestedKey: CodingKey { + let newPath = self.codingPath + [_YAMLKey(index: self.count)] + let object = self.array.appendObject() + let nestedContainer = YAMLKeyedEncodingContainer( + impl: impl, object: object, codingPath: newPath + ) + return KeyedEncodingContainer(nestedContainer) + } + + mutating func nestedUnkeyedContainer() -> UnkeyedEncodingContainer { + let newPath = self.codingPath + [_YAMLKey(index: self.count)] + let array = self.array.appendArray() + let nestedContainer = YAMLUnkeyedEncodingContainer( + impl: impl, array: array, codingPath: newPath + ) + return nestedContainer + } + + mutating func superEncoder() -> Encoder { + let encoder = self.getEncoder(for: _YAMLKey(index: self.count)) + self.array.append(encoder) + return encoder + } } extension YAMLUnkeyedEncodingContainer { - @inline(__always) private mutating func encodeFixedWidthInteger(_ value: N) - throws - { - self.array.append(.number(value.description)) - } - - @inline(__always) - private mutating func encodeFloatingPoint(_ float: F) - throws - { - let value = try self.wrapFloat(float, for: _YAMLKey(index: self.count)) - self.array.append(value) - } + @inline(__always) private mutating func encodeFixedWidthInteger(_ value: N) + throws { + self.array.append(.number(value.description)) + } + + @inline(__always) + private mutating func encodeFloatingPoint(_ float: F) + throws { + let value = try self.wrapFloat(float, for: _YAMLKey(index: self.count)) + self.array.append(value) + } } private struct YAMLSingleValueEncodingContainer: SingleValueEncodingContainer, - _SpecialTreatmentEncoder -{ - let impl: YAMLEncoderImpl - let codingPath: [CodingKey] - - private var firstValueWritten: Bool = false - fileprivate var options: YAMLEncoder._Options { - return self.impl.options - } - - init(impl: YAMLEncoderImpl, codingPath: [CodingKey]) { - self.impl = impl - self.codingPath = codingPath - } - - mutating func encodeNil() throws { - self.preconditionCanEncodeNewValue() - self.impl.singleValue = .null - } - - mutating func encode(_ value: Bool) throws { - self.preconditionCanEncodeNewValue() - self.impl.singleValue = .bool(value) - } - - mutating func encode(_ value: Int) throws { - try encodeFixedWidthInteger(value) - } - - mutating func encode(_ value: Int8) throws { - try encodeFixedWidthInteger(value) - } - - mutating func encode(_ value: Int16) throws { - try encodeFixedWidthInteger(value) - } - - mutating func encode(_ value: Int32) throws { - try encodeFixedWidthInteger(value) - } - - mutating func encode(_ value: Int64) throws { - try encodeFixedWidthInteger(value) - } - - mutating func encode(_ value: UInt) throws { - try encodeFixedWidthInteger(value) - } - - mutating func encode(_ value: UInt8) throws { - try encodeFixedWidthInteger(value) - } - - mutating func encode(_ value: UInt16) throws { - try encodeFixedWidthInteger(value) - } - - mutating func encode(_ value: UInt32) throws { - try encodeFixedWidthInteger(value) - } - - mutating func encode(_ value: UInt64) throws { - try encodeFixedWidthInteger(value) - } - - mutating func encode(_ value: Float) throws { - try encodeFloatingPoint(value) - } - - mutating func encode(_ value: Double) throws { - try encodeFloatingPoint(value) - } - - mutating func encode(_ value: String) throws { - self.preconditionCanEncodeNewValue() - self.impl.singleValue = .string(value) - } - - mutating func encode(_ value: T) throws { - self.preconditionCanEncodeNewValue() - self.impl.singleValue = try self.wrapEncodable(value, for: nil) - } - - func preconditionCanEncodeNewValue() { - precondition( - self.impl.singleValue == nil, - "Attempt to encode value through single value container when previously value already encoded." - ) - } + _SpecialTreatmentEncoder { + let impl: YAMLEncoderImpl + let codingPath: [CodingKey] + + private var firstValueWritten: Bool = false + fileprivate var options: YAMLEncoder._Options { + self.impl.options + } + + init(impl: YAMLEncoderImpl, codingPath: [CodingKey]) { + self.impl = impl + self.codingPath = codingPath + } + + mutating func encodeNil() throws { + self.preconditionCanEncodeNewValue() + self.impl.singleValue = .null + } + + mutating func encode(_ value: Bool) throws { + self.preconditionCanEncodeNewValue() + self.impl.singleValue = .bool(value) + } + + mutating func encode(_ value: Int) throws { + try encodeFixedWidthInteger(value) + } + + mutating func encode(_ value: Int8) throws { + try encodeFixedWidthInteger(value) + } + + mutating func encode(_ value: Int16) throws { + try encodeFixedWidthInteger(value) + } + + mutating func encode(_ value: Int32) throws { + try encodeFixedWidthInteger(value) + } + + mutating func encode(_ value: Int64) throws { + try encodeFixedWidthInteger(value) + } + + mutating func encode(_ value: UInt) throws { + try encodeFixedWidthInteger(value) + } + + mutating func encode(_ value: UInt8) throws { + try encodeFixedWidthInteger(value) + } + + mutating func encode(_ value: UInt16) throws { + try encodeFixedWidthInteger(value) + } + + mutating func encode(_ value: UInt32) throws { + try encodeFixedWidthInteger(value) + } + + mutating func encode(_ value: UInt64) throws { + try encodeFixedWidthInteger(value) + } + + mutating func encode(_ value: Float) throws { + try encodeFloatingPoint(value) + } + + mutating func encode(_ value: Double) throws { + try encodeFloatingPoint(value) + } + + mutating func encode(_ value: String) throws { + self.preconditionCanEncodeNewValue() + self.impl.singleValue = .string(value) + } + + mutating func encode(_ value: T) throws { + self.preconditionCanEncodeNewValue() + self.impl.singleValue = try self.wrapEncodable(value, for: nil) + } + + func preconditionCanEncodeNewValue() { + precondition( + self.impl.singleValue == nil, + "Attempt to encode value through single value container when previously value already encoded." + ) + } } extension YAMLSingleValueEncodingContainer { - @inline(__always) private mutating func encodeFixedWidthInteger(_ value: N) - throws - { - self.preconditionCanEncodeNewValue() - self.impl.singleValue = .number(value.description) - } - - @inline(__always) - private mutating func encodeFloatingPoint(_ float: F) - throws - { - self.preconditionCanEncodeNewValue() - let value = try self.wrapFloat(float, for: nil) - self.impl.singleValue = value - } + @inline(__always) private mutating func encodeFixedWidthInteger(_ value: N) + throws { + self.preconditionCanEncodeNewValue() + self.impl.singleValue = .number(value.description) + } + + @inline(__always) + private mutating func encodeFloatingPoint(_ float: F) + throws { + self.preconditionCanEncodeNewValue() + let value = try self.wrapFloat(float, for: nil) + self.impl.singleValue = value + } } extension YAMLValue { + fileprivate struct Writer { + let options: YAMLEncoder.OutputFormatting - fileprivate struct Writer { - let options: YAMLEncoder.OutputFormatting - - init(options: YAMLEncoder.OutputFormatting) { - self.options = options - } - - func writeValue(_ value: YAMLValue) -> [UInt8] { - var bytes = [UInt8]() - self.writeValuePretty(value, into: &bytes) - return bytes - } - - private func addInset(to bytes: inout [UInt8], depth: Int) { - bytes.append(contentsOf: [UInt8](repeating: ._space, count: depth * YAMLEncoder.singleIndent)) - } - - private func writeValuePretty(_ value: YAMLValue, into bytes: inout [UInt8], depth: Int = 0) { - switch value { - case .null: - if bytes.count > 0 { bytes.append(contentsOf: [._space]) } - bytes.append(contentsOf: [UInt8]._null) - case .bool(true): - if bytes.count > 0 { bytes.append(contentsOf: [._space]) } - bytes.append(contentsOf: [UInt8]._true) - case .bool(false): - if bytes.count > 0 { bytes.append(contentsOf: [._space]) } - bytes.append(contentsOf: [UInt8]._false) - case .string(let string): - if bytes.count > 0 { bytes.append(contentsOf: [._space]) } - self.encodeString(string, to: &bytes) - case .number(let string): - if bytes.count > 0 { bytes.append(contentsOf: [._space]) } - bytes.append(contentsOf: string.utf8) - case .array(let array): - var iterator = array.makeIterator() - while let item = iterator.next() { - bytes.append(contentsOf: [._newline]) - self.addInset(to: &bytes, depth: depth) - bytes.append(contentsOf: [._dash]) - self.writeValuePretty(item, into: &bytes, depth: depth + 1) + init(options: YAMLEncoder.OutputFormatting) { + self.options = options } - case .object(let dict): - if options.contains(.sortedKeys) { - let sorted = dict.sorted { $0.key < $1.key } - self.writePrettyObject(sorted, into: &bytes) - } else { - self.writePrettyObject(dict, into: &bytes, depth: depth) + + func writeValue(_ value: YAMLValue) -> [UInt8] { + var bytes = [UInt8]() + self.writeValuePretty(value, into: &bytes) + return bytes } - } - } - private func writePrettyObject( - _ object: Object, into bytes: inout [UInt8], depth: Int = 0 - ) - where Object.Element == (key: String, value: YAMLValue) { - var iterator = object.makeIterator() + private func addInset(to bytes: inout [UInt8], depth: Int) { + bytes.append(contentsOf: [UInt8](repeating: ._space, count: depth * YAMLEncoder.singleIndent)) + } - while let (key, value) = iterator.next() { - // add a new line when other objects are present already - if bytes.count > 0 { - bytes.append(contentsOf: [._newline]) + private func writeValuePretty(_ value: YAMLValue, into bytes: inout [UInt8], depth: Int = 0) { + switch value { + case .null: + if bytes.count > 0 { bytes.append(contentsOf: [._space]) } + bytes.append(contentsOf: [UInt8]._null) + case .bool(true): + if bytes.count > 0 { bytes.append(contentsOf: [._space]) } + bytes.append(contentsOf: [UInt8]._true) + case .bool(false): + if bytes.count > 0 { bytes.append(contentsOf: [._space]) } + bytes.append(contentsOf: [UInt8]._false) + case .string(let string): + if bytes.count > 0 { bytes.append(contentsOf: [._space]) } + self.encodeString(string, to: &bytes) + case .number(let string): + if bytes.count > 0 { bytes.append(contentsOf: [._space]) } + bytes.append(contentsOf: string.utf8) + case .array(let array): + var iterator = array.makeIterator() + while let item = iterator.next() { + bytes.append(contentsOf: [._newline]) + self.addInset(to: &bytes, depth: depth) + bytes.append(contentsOf: [._dash]) + self.writeValuePretty(item, into: &bytes, depth: depth + 1) + } + case .object(let dict): + if self.options.contains(.sortedKeys) { + let sorted = dict.sorted { $0.key < $1.key } + self.writePrettyObject(sorted, into: &bytes) + } else { + self.writePrettyObject(dict, into: &bytes, depth: depth) + } + } } - self.addInset(to: &bytes, depth: depth) - // key - self.encodeString(key, to: &bytes) - bytes.append(contentsOf: [._colon]) - // value - self.writeValuePretty(value, into: &bytes, depth: depth + 1) - } - // self.addInset(to: &bytes, depth: depth) - } - - private func encodeString(_ string: String, to bytes: inout [UInt8]) { - let stringBytes = string.utf8 - var startCopyIndex = stringBytes.startIndex - var nextIndex = startCopyIndex - - while nextIndex != stringBytes.endIndex { - switch stringBytes[nextIndex] { - case 0..<32, UInt8(ascii: "\""), UInt8(ascii: "\\"): - // All Unicode characters may be placed within the - // quotation marks, except for the characters that MUST be escaped: - // quotation mark, reverse solidus, and the control characters (U+0000 - // through U+001F). - // https://tools.ietf.org/html/rfc8259#section-7 - - // copy the current range over - bytes.append(contentsOf: stringBytes[startCopyIndex.. UInt8 { - switch value { - case 0...9: - return value + UInt8(ascii: "0") - case 10...15: - return value - 10 + UInt8(ascii: "a") - default: - preconditionFailure() - } + + private func writePrettyObject( + _ object: Object, into bytes: inout [UInt8], depth: Int = 0 + ) + where Object.Element == (key: String, value: YAMLValue) { + var iterator = object.makeIterator() + + while let (key, value) = iterator.next() { + // add a new line when other objects are present already + if bytes.count > 0 { + bytes.append(contentsOf: [._newline]) + } + self.addInset(to: &bytes, depth: depth) + // key + self.encodeString(key, to: &bytes) + bytes.append(contentsOf: [._colon]) + // value + self.writeValuePretty(value, into: &bytes, depth: depth + 1) } - bytes.append(UInt8(ascii: "\\")) - bytes.append(UInt8(ascii: "u")) - bytes.append(UInt8(ascii: "0")) - bytes.append(UInt8(ascii: "0")) - let first = stringBytes[nextIndex] / 16 - let remaining = stringBytes[nextIndex] % 16 - bytes.append(valueToAscii(first)) - bytes.append(valueToAscii(remaining)) - } - - nextIndex = stringBytes.index(after: nextIndex) - startCopyIndex = nextIndex - case UInt8(ascii: "/") where options.contains(.withoutEscapingSlashes) == false: - bytes.append(contentsOf: stringBytes[startCopyIndex.. UInt8 { + switch value { + case 0 ... 9: + return value + UInt8(ascii: "0") + case 10 ... 15: + return value - 10 + UInt8(ascii: "a") + default: + preconditionFailure() + } + } + bytes.append(UInt8(ascii: "\\")) + bytes.append(UInt8(ascii: "u")) + bytes.append(UInt8(ascii: "0")) + bytes.append(UInt8(ascii: "0")) + let first = stringBytes[nextIndex] / 16 + let remaining = stringBytes[nextIndex] % 16 + bytes.append(valueToAscii(first)) + bytes.append(valueToAscii(remaining)) + } + + nextIndex = stringBytes.index(after: nextIndex) + startCopyIndex = nextIndex + case UInt8(ascii: "/") where self.options.contains(.withoutEscapingSlashes) == false: + bytes.append(contentsOf: stringBytes[startCopyIndex ..< nextIndex]) + bytes.append(contentsOf: [._backslash, UInt8(ascii: "/")]) + nextIndex = stringBytes.index(after: nextIndex) + startCopyIndex = nextIndex + default: + nextIndex = stringBytes.index(after: nextIndex) + } + } + + // copy everything, that hasn't been copied yet + bytes.append(contentsOf: stringBytes[startCopyIndex ..< nextIndex]) + } } - } } // ===----------------------------------------------------------------------===// // Shared Key Types // ===----------------------------------------------------------------------===// -internal struct _YAMLKey: CodingKey { - public var stringValue: String - public var intValue: Int? +struct _YAMLKey: CodingKey { + public var stringValue: String + public var intValue: Int? - public init(stringValue: String) { - self.stringValue = stringValue - self.intValue = nil - } + public init(stringValue: String) { + self.stringValue = stringValue + self.intValue = nil + } - public init?(intValue: Int) { - self.stringValue = "\(intValue)" - self.intValue = intValue - } + public init?(intValue: Int) { + self.stringValue = "\(intValue)" + self.intValue = intValue + } - public init(stringValue: String, intValue: Int?) { - self.stringValue = stringValue - self.intValue = intValue - } + public init(stringValue: String, intValue: Int?) { + self.stringValue = stringValue + self.intValue = intValue + } - internal init(index: Int) { - self.stringValue = "Index \(index)" - self.intValue = index - } + init(index: Int) { + self.stringValue = "Index \(index)" + self.intValue = index + } - internal static let `super` = _YAMLKey(stringValue: "super") + static let `super` = _YAMLKey(stringValue: "super") } // ===----------------------------------------------------------------------===// @@ -1134,10 +1132,10 @@ internal struct _YAMLKey: CodingKey { // NOTE: This value is implicitly lazy and _must_ be lazy. We're compiled against the latest SDK (w/ ISO8601DateFormatter), but linked against whichever Foundation the user has. ISO8601DateFormatter might not exist, so we better not hit this code path on an older OS. @available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) -internal var _iso8601Formatter: ISO8601DateFormatter = { - let formatter = ISO8601DateFormatter() - formatter.formatOptions = .withInternetDateTime - return formatter +var _iso8601Formatter: ISO8601DateFormatter = { + let formatter = ISO8601DateFormatter() + formatter.formatOptions = .withInternetDateTime + return formatter }() // ===----------------------------------------------------------------------===// @@ -1145,113 +1143,110 @@ internal var _iso8601Formatter: ISO8601DateFormatter = { // ===----------------------------------------------------------------------===// extension EncodingError { - /// Returns a `.invalidValue` error describing the given invalid floating-point value. - /// - /// - /// - parameter value: The value that was invalid to encode. - /// - parameter path: The path of `CodingKey`s taken to encode this value. - /// - returns: An `EncodingError` with the appropriate path and debug description. - fileprivate static func _invalidFloatingPointValue( - _ value: T, at codingPath: [CodingKey] - ) -> EncodingError { - let valueDescription: String - if value == T.infinity { - valueDescription = "\(T.self).infinity" - } else if value == -T.infinity { - valueDescription = "-\(T.self).infinity" - } else { - valueDescription = "\(T.self).nan" - } - - let debugDescription = - "Unable to encode \(valueDescription) directly in YAML. Use YAMLEncoder.NonConformingFloatEncodingStrategy.convertToString to specify how the value should be encoded." - return .invalidValue( - value, EncodingError.Context(codingPath: codingPath, debugDescription: debugDescription)) - } + /// Returns a `.invalidValue` error describing the given invalid floating-point value. + /// + /// + /// - parameter value: The value that was invalid to encode. + /// - parameter path: The path of `CodingKey`s taken to encode this value. + /// - returns: An `EncodingError` with the appropriate path and debug description. + fileprivate static func _invalidFloatingPointValue( + _ value: T, at codingPath: [CodingKey] + ) -> EncodingError { + let valueDescription: String + if value == T.infinity { + valueDescription = "\(T.self).infinity" + } else if value == -T.infinity { + valueDescription = "-\(T.self).infinity" + } else { + valueDescription = "\(T.self).nan" + } + + let debugDescription = + "Unable to encode \(valueDescription) directly in YAML. Use YAMLEncoder.NonConformingFloatEncodingStrategy.convertToString to specify how the value should be encoded." + return .invalidValue( + value, EncodingError.Context(codingPath: codingPath, debugDescription: debugDescription) + ) + } } enum YAMLValue: Equatable { - case string(String) - case number(String) - case bool(Bool) - case null + case string(String) + case number(String) + case bool(Bool) + case null - case array([YAMLValue]) - case object([String: YAMLValue]) + case array([YAMLValue]) + case object([String: YAMLValue]) } extension YAMLValue { - fileprivate var isValue: Bool { - switch self { - case .array, .object: - return false - case .null, .number, .string, .bool: - return true - } - } - - fileprivate var isContainer: Bool { - switch self { - case .array, .object: - return true - case .null, .number, .string, .bool: - return false - } - } + private var isValue: Bool { + switch self { + case .array, .object: + return false + case .null, .number, .string, .bool: + return true + } + } + + private var isContainer: Bool { + switch self { + case .array, .object: + return true + case .null, .number, .string, .bool: + return false + } + } } extension YAMLValue { - fileprivate var debugDataTypeDescription: String { - switch self { - case .array: - return "an array" - case .bool: - return "bool" - case .number: - return "a number" - case .string: - return "a string" - case .object: - return "a dictionary" - case .null: - return "null" - } - } + private var debugDataTypeDescription: String { + switch self { + case .array: + return "an array" + case .bool: + return "bool" + case .number: + return "a number" + case .string: + return "a string" + case .object: + return "a dictionary" + case .null: + return "null" + } + } } extension UInt8 { + static let _space = UInt8(ascii: " ") + static let _return = UInt8(ascii: "\r") + static let _newline = UInt8(ascii: "\n") + static let _tab = UInt8(ascii: "\t") - internal static let _space = UInt8(ascii: " ") - internal static let _return = UInt8(ascii: "\r") - internal static let _newline = UInt8(ascii: "\n") - internal static let _tab = UInt8(ascii: "\t") + static let _colon = UInt8(ascii: ":") + static let _comma = UInt8(ascii: ",") - internal static let _colon = UInt8(ascii: ":") - internal static let _comma = UInt8(ascii: ",") + static let _openbrace = UInt8(ascii: "{") + static let _closebrace = UInt8(ascii: "}") - internal static let _openbrace = UInt8(ascii: "{") - internal static let _closebrace = UInt8(ascii: "}") + static let _openbracket = UInt8(ascii: "[") + static let _closebracket = UInt8(ascii: "]") - internal static let _openbracket = UInt8(ascii: "[") - internal static let _closebracket = UInt8(ascii: "]") - - internal static let _quote = UInt8(ascii: "\"") - internal static let _backslash = UInt8(ascii: "\\") - - internal static let _dash = UInt8(ascii: "-") + static let _quote = UInt8(ascii: "\"") + static let _backslash = UInt8(ascii: "\\") + static let _dash = UInt8(ascii: "-") } extension Array where Element == UInt8 { - - internal static let _true = [ - UInt8(ascii: "t"), UInt8(ascii: "r"), UInt8(ascii: "u"), UInt8(ascii: "e"), - ] - internal static let _false = [ - UInt8(ascii: "f"), UInt8(ascii: "a"), UInt8(ascii: "l"), UInt8(ascii: "s"), UInt8(ascii: "e"), - ] - internal static let _null = [ - UInt8(ascii: "n"), UInt8(ascii: "u"), UInt8(ascii: "l"), UInt8(ascii: "l"), - ] - + static let _true = [ + UInt8(ascii: "t"), UInt8(ascii: "r"), UInt8(ascii: "u"), UInt8(ascii: "e"), + ] + static let _false = [ + UInt8(ascii: "f"), UInt8(ascii: "a"), UInt8(ascii: "l"), UInt8(ascii: "s"), UInt8(ascii: "e"), + ] + static let _null = [ + UInt8(ascii: "n"), UInt8(ascii: "u"), UInt8(ascii: "l"), UInt8(ascii: "l"), + ] } diff --git a/Sources/AWSLambdaDeploymentDescriptorGenerator/DeploymentDescriptorGenerator.swift b/Sources/AWSLambdaDeploymentDescriptorGenerator/DeploymentDescriptorGenerator.swift index 94c0001..7ba1425 100644 --- a/Sources/AWSLambdaDeploymentDescriptorGenerator/DeploymentDescriptorGenerator.swift +++ b/Sources/AWSLambdaDeploymentDescriptorGenerator/DeploymentDescriptorGenerator.swift @@ -1,8 +1,641 @@ /** -TODO + TODO: -1. read `samtranslator schema.json` -2. generate `../DeploymentDescriptor.swift` + 1. read `samtranslator schema.json` + 2. generate `../DeploymentDescriptor.swift` -*/ \ No newline at end of file + */ +import Foundation +import HummingbirdMustache +import Logging +import SwiftSyntax +import SwiftSyntaxBuilder +import OpenAPIRuntime +import OpenAPIURLSession + + +public protocol DeploymentDescriptorGeneratorCommand { + var inputFile: String? { get } + var configFile: String? { get } + var prefix: String? { get } + var outputFolder: String { get } + var inputFolder: String? { get } + var endpoints: String { get } + var module: String? { get } + var output: Bool { get } + var logLevel: String? { get } +} + +public struct DeploymentDescriptorGenerator { + struct FileError: Error { + let filename: String + let error: Error + } + + static var rootPath: String { + #file + .split(separator: "/", omittingEmptySubsequences: false) + .dropLast(3) + .map { String(describing: $0) } + .joined(separator: "/") + } + + public func generate() { + // generate code here + + let filePath = Bundle.module.path(forResource: "SamTranslatorSchema", ofType: "json") ?? "" + let url = URL(fileURLWithPath: filePath) + + do { + let schemaData = try Data(contentsOf: url) + do { + _ = try self.analyzeSAMSchema(from: schemaData) + // access the schema information + } catch { + print("Error analyzing schema: \(error)") + } + + } catch { + print("Error getting schemaData contents of URL: \(error)") + } + } + + // MARK: - generateWithSwiftOpenapi + + public func generateWithSwiftOpenAPI() {} + + // MARK: - generateWithSwiftSyntax + + func writeGeneratedStructWithSwiftSyntax() { + let exampleSchema = TypeSchema( + typeName: "Example", + properties: [ + TypeSchema.Property(name: "firstName", type: "String"), + TypeSchema.Property(name: "lastName", type: "String"), + ], + subTypes: [ + TypeSchema( + typeName: "SubType", + properties: [ + TypeSchema.Property(name: "subProperty1", type: "Int"), + ], + subTypes: [] + ), + ] + ) + self.generateTypeSchema(for: exampleSchema) + } + + func generateDecoderInitializer() -> InitializerDeclSyntax { + let parameters = FunctionParameterClauseSyntax { + FunctionParameterListSyntax { + FunctionParameterSyntax( + modifiers: DeclModifierListSyntax { [DeclModifierSyntax(name: .identifier("from"))] }, + firstName: .identifier("decoder"), + colon: .colonToken(trailingTrivia: .space), + type: TypeSyntax(IdentifierTypeSyntax(name: .identifier("Decoder"))) + ) + } + } + + return InitializerDeclSyntax( + modifiers: DeclModifierListSyntax { [DeclModifierSyntax(name: .keyword(.public))] }, + signature: FunctionSignatureSyntax( + parameterClause: parameters, + effectSpecifiers: FunctionEffectSpecifiersSyntax(throwsSpecifier: .keyword(.throws)) + ), + body: CodeBlockSyntax { + CodeBlockItemListSyntax { + // let container = try decoder.container(keyedBy: CodingKeys.self) + CodeBlockItemSyntax(item: .decl( + DeclSyntax( + VariableDeclSyntax(bindingSpecifier: .keyword(.let)) { + PatternBindingSyntax( + pattern: PatternSyntax("container"), + initializer: + InitializerClauseSyntax( + equal: .equalToken(trailingTrivia: .space), + value: + ExprSyntax( + TryExprSyntax( + tryKeyword: .keyword(.try), + expression: + FunctionCallExprSyntax( + calledExpression: ExprSyntax(MemberAccessExprSyntax( + base: ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier("decoder"))), + name: .identifier("container") + )), + leftParen: .leftParenToken(), + arguments: LabeledExprListSyntax { + LabeledExprSyntax( + label: .identifier("keyedBy"), + colon: .colonToken(trailingTrivia: .space), + expression: ExprSyntax( + MemberAccessExprSyntax( + base: ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier("CodingKeys"))), + name: .identifier("self") + ) + ) + ) + }, + rightParen: .rightParenToken() + ) + )) + ) + ) + } + ) + )) + + // self.typeName = try container.decode(String.self, forKey: .typeName) + CodeBlockItemSyntax(item: .expr( + ExprSyntax( + InfixOperatorExprSyntax( + leftOperand: ExprSyntax( + MemberAccessExprSyntax( + base: ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier("self"))), + name: "typeName" + )), + operator: ExprSyntax(BinaryOperatorExprSyntax(operator: .equalToken())), + rightOperand: ExprSyntax( + TryExprSyntax( + tryKeyword: .keyword(.try), + expression: FunctionCallExprSyntax( + calledExpression: ExprSyntax(MemberAccessExprSyntax( + base: ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier("container"))), + name: .identifier("decode") + )), + leftParen: .leftParenToken(), + arguments: LabeledExprListSyntax { + LabeledExprSyntax( + expression: ExprSyntax( + MemberAccessExprSyntax( + base: ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier("String"))), + name: .identifier("self") + ) + ) + ) + LabeledExprSyntax( + label: .identifier("forKey"), + colon: .colonToken(trailingTrivia: .space), + expression: ExprSyntax( + MemberAccessExprSyntax( + name: "typeName" + ) + ) + ) + }, + rightParen: .rightParenToken() + ) + ) + ) // end of InfixOperatorExprSyntax + ) + ))) + + // self.properties = try container.decode([Property].self, forKey: .properties) + CodeBlockItemSyntax(item: .expr( + ExprSyntax( + InfixOperatorExprSyntax( + leftOperand: ExprSyntax( + MemberAccessExprSyntax( + base: ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier("self"))), + name: "properties" + )), + operator: ExprSyntax(BinaryOperatorExprSyntax(operator: .equalToken())), + rightOperand: ExprSyntax( + TryExprSyntax( + tryKeyword: .keyword(.try), + expression: FunctionCallExprSyntax( + calledExpression: ExprSyntax(MemberAccessExprSyntax( + base: ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier("container"))), + name: .identifier("decode") + )), + leftParen: .leftParenToken(), + arguments: LabeledExprListSyntax { + LabeledExprSyntax( + expression: ExprSyntax( + MemberAccessExprSyntax( + base: ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier("[Property]"))), + name: .identifier("self") + ) + ) + ) + LabeledExprSyntax( + label: .identifier("forKey"), + colon: .colonToken(trailingTrivia: .space), + expression: ExprSyntax( + MemberAccessExprSyntax( + name: "properties" + ) + ) + ) + }, + rightParen: .rightParenToken() + ) + ) + ) // end of InfixOperatorExprSyntax + ) + )) + ) + + // self.subTypes = try container.decode([TypeSchema].self, forKey: .subTypes) + CodeBlockItemSyntax(item: .expr( + ExprSyntax( + InfixOperatorExprSyntax( + leftOperand: ExprSyntax( + MemberAccessExprSyntax( + base: ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier("self"))), + name: "subTypes" + )), + operator: ExprSyntax(BinaryOperatorExprSyntax(operator: .equalToken())), + rightOperand: ExprSyntax( + TryExprSyntax( + tryKeyword: .keyword(.try), + expression: FunctionCallExprSyntax( + calledExpression: ExprSyntax(MemberAccessExprSyntax( + base: ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier("container"))), + name: .identifier("decode") + )), + leftParen: .leftParenToken(), + arguments: LabeledExprListSyntax { + LabeledExprSyntax( + expression: ExprSyntax( + MemberAccessExprSyntax( + base: ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier("[Example]"))), + name: .identifier("self") + ) + ) + ) + LabeledExprSyntax( + label: .identifier("forKey"), + colon: .colonToken(trailingTrivia: .space), + expression: ExprSyntax( + MemberAccessExprSyntax( + name: "subTypes" + ) + ) + ) + }, + rightParen: .rightParenToken() + ) + ) + ) // end of InfixOperatorExprSyntax + ) + )) + ) + } + } + ) + } + + func generateTypeSchema(for schema: TypeSchema) { + // MARK: - Inheritance + + let structInheritance = InheritanceClauseSyntax { + InheritedTypeSyntax(type: TypeSyntax("Decodable")) + InheritedTypeSyntax(type: TypeSyntax("Equatable")) + } + + let enumInheritance = InheritanceClauseSyntax { + InheritedTypeSyntax(type: TypeSyntax("String")) + InheritedTypeSyntax(type: TypeSyntax("CodingKey")) + } + + // MARK: - Initializer + + let parameters = FunctionParameterClauseSyntax { + FunctionParameterListSyntax { + FunctionParameterSyntax( + firstName: .identifier("typeName"), + colon: .colonToken(trailingTrivia: .space), + type: TypeSyntax("String") + ) + FunctionParameterSyntax( + firstName: .identifier("properties"), + colon: .colonToken(trailingTrivia: .space), + type: ArrayTypeSyntax(element: TypeSyntax("Property")) + ) + FunctionParameterSyntax( + firstName: .identifier("subTypes"), + colon: .colonToken(trailingTrivia: .space), + type: ArrayTypeSyntax(element: TypeSyntax("\(raw: schema.typeName)")) + ) + } + } + + let structInit = InitializerDeclSyntax( + signature: FunctionSignatureSyntax( + parameterClause: parameters + ), + body: CodeBlockSyntax { + CodeBlockItemListSyntax { + CodeBlockItemSyntax(item: .expr( + ExprSyntax( + InfixOperatorExprSyntax( + leftOperand: ExprSyntax( + MemberAccessExprSyntax( + base: ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier("self"))), + name: "typeName" + )), + operator: ExprSyntax(BinaryOperatorExprSyntax(operator: .equalToken())), + rightOperand: ExprSyntax( + DeclReferenceExprSyntax(baseName: .identifier("typeName")) + ) + ) + ) + )) + + CodeBlockItemSyntax(item: .expr( + ExprSyntax( + InfixOperatorExprSyntax( + leftOperand: ExprSyntax( + MemberAccessExprSyntax( + base: ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier("self"))), + name: "properties" + )), + operator: ExprSyntax(BinaryOperatorExprSyntax(operator: .equalToken())), + rightOperand: ExprSyntax( + DeclReferenceExprSyntax(baseName: .identifier("properties")) + ) + ) + ) + )) + + CodeBlockItemSyntax(item: .expr( + ExprSyntax( + InfixOperatorExprSyntax( + leftOperand: ExprSyntax( + MemberAccessExprSyntax( + base: ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier("self"))), + name: "subTypes" + )), + operator: ExprSyntax(BinaryOperatorExprSyntax(operator: .equalToken())), + rightOperand: ExprSyntax( + DeclReferenceExprSyntax(baseName: .identifier("subTypes")) + ) + ) + ) + )) + } + } + ) + + let source = SourceFileSyntax { + // MARK: - Type Schema Struct + + StructDeclSyntax(modifiers: DeclModifierListSyntax { [DeclModifierSyntax(name: .keyword(.public))] }, + name: "\(raw: schema.typeName)", + inheritanceClause: structInheritance) { + // MARK: - Main Struct Kyes + + MemberBlockItemListSyntax { + MemberBlockItemSyntax(decl: DeclSyntax("let typeName: String")) + MemberBlockItemSyntax(decl: + VariableDeclSyntax(bindingSpecifier: .keyword(.let)) { + PatternBindingSyntax(pattern: PatternSyntax("properties"), + typeAnnotation: + TypeAnnotationSyntax(colon: .colonToken(trailingTrivia: .space), + type: + ArrayTypeSyntax(element: TypeSyntax("Property")))) + }) + + MemberBlockItemSyntax(decl: DeclSyntax("let subTypes: [\(raw: schema.typeName)]")) + MemberBlockItemSyntax(decl: structInit) + MemberBlockItemSyntax(decl: self.generateDecoderInitializer()) + .with(\.leadingTrivia, .newlines(2)) + } + + // MARK: - Property Struct + + StructDeclSyntax(modifiers: DeclModifierListSyntax { DeclModifierSyntax(name: .keyword(.public)) }, + name: "Property", + inheritanceClause: structInheritance) { + MemberBlockItemListSyntax { + // Property: name + MemberBlockItemSyntax(decl: + VariableDeclSyntax(bindingSpecifier: .keyword(.let)) { + PatternBindingSyntax(pattern: PatternSyntax("name"), + typeAnnotation: + TypeAnnotationSyntax(colon: .colonToken(trailingTrivia: .space), + type: + OptionalTypeSyntax( + wrappedType: TypeSyntax(IdentifierTypeSyntax(name: .identifier("String"))) + ))) + }) + + // Property: type + MemberBlockItemSyntax(decl: + VariableDeclSyntax(bindingSpecifier: .keyword(.let)) { + PatternBindingSyntax(pattern: PatternSyntax("type"), + typeAnnotation: + TypeAnnotationSyntax(colon: .colonToken(trailingTrivia: .space), + type: + TypeSyntax(IdentifierTypeSyntax(name: .identifier("String"))))) + }) + } + }.with(\.leadingTrivia, .newlines(2)) + + // MARK: - Enum + + EnumDeclSyntax(modifiers: DeclModifierListSyntax { DeclModifierSyntax(name: .keyword(.private)) }, + name: .identifier("CodingKeys"), + inheritanceClause: enumInheritance) { + MemberBlockItemListSyntax { + EnumCaseDeclSyntax { + EnumCaseElementListSyntax { + EnumCaseElementSyntax( + name: .identifier("typeName"), + rawValue: InitializerClauseSyntax( + equal: .equalToken(trailingTrivia: .space), + value: ExprSyntax(StringLiteralExprSyntax(content: "typeName")) + ) + ) + } + }.with(\.leadingTrivia, .newlines(1)) + + EnumCaseDeclSyntax { + EnumCaseElementListSyntax { + EnumCaseElementSyntax(name: "properties") + } + }.with(\.leadingTrivia, .newlines(1)) + + EnumCaseDeclSyntax { + EnumCaseElementListSyntax { + EnumCaseElementSyntax(name: "subTypes") + } + }.with(\.leadingTrivia, .newlines(1)) + } + }.with(\.leadingTrivia, .newlines(2)) // end of enum + }.with(\.leadingTrivia, .newlines(1)) // end of Type Schema Struct + } // end of SourceFile + + let renderedStruct = source.formatted().description + print(renderedStruct) + self.writeGeneratedResultToFile(renderedStruct) + } + + // MARK: - generateWithSwiftMustache + + public func generateWithSwiftMustache() { + do { + let library = try Templates.createLibrary() + let template = library.getTemplate(named: "structTemplate") + + // TODO: Decode JSON here + let properties: [TypeSchema.Property] = [ + .init(name: "id", type: "Int"), + .init(name: "name", type: "String"), + ] + + let schema = TypeSchema(typeName: "Hello", + properties: properties, + subTypes: []) + + let modelContext: [String: Any] = [ + "scope": "", + "object": "struct", + "name": schema.typeName, + "shapeProtocol": "Codable", + "typeName": schema.typeName, + "properties": schema.properties.map { property in + [ + "scope": "", + "variable": property.name ?? "", + "type": property.type, + "isOptional": property.type.contains("?"), + "last": property == schema.properties.last, + ] + }, + ] as [String: Any] + + if let template = template { + let renderedStruct = template.render(modelContext) + print(renderedStruct) + self.writeGeneratedResultToFile(renderedStruct) + } else { + print("Error: Template 'structTemplate' not found") + } + } catch { + print("Error generating Swift struct: \(error)") + } + } + + func writeGeneratedResultToFile(_ result: String) { + let projectDirectory = "\(DeploymentDescriptorGenerator.rootPath)" + let filePath = projectDirectory + "/Sources/AWSLambdaDeploymentDescriptorGenerator/dummyGenerated.swift" + + let directoryPath = (filePath as NSString).deletingLastPathComponent + var isDirectory: ObjCBool = false + if !FileManager.default.fileExists(atPath: directoryPath, isDirectory: &isDirectory) { + print("Error: Directory does not exist.") + return + } + + let writable = FileManager.default.isWritableFile(atPath: directoryPath) + if !writable { + print("Error: No write permissions for the directory.") + return + } + + do { + if try result.writeIfChanged(toFile: filePath) { + print("Success Wrote ✅") + } + } catch { + print("Error writing file: \(error)") + } + } + + func analyzeSAMSchema(from jsonData: Data) throws -> JSONSchema { + let decoder = JSONDecoder() + let schema = try decoder.decode(JSONSchema.self, from: jsonData) + + print("Schema Information:") + print(" - Schema URL: \(schema.schema)") + print(" - Overall Type: \(schema.type ?? [.null])") + + if let properties = schema.properties { + print("\n Properties:") + for (name, propertyType) in properties { + print(" - \(name): \(propertyType)") + } + } + + if let definitions = schema.definitions { + print("\n Definitions:") + for (name, definitionType) in definitions { + print(" - \(name): \(definitionType)") + } + } + + return schema + } +} + +extension String { + /// Only writes to file if the string contents are different to the file contents. This is used to stop XCode rebuilding and reindexing files unnecessarily. + /// If the file is written to XCode assumes it has changed even when it hasn't + /// - Parameters: + /// - toFile: Filename + /// - atomically: make file write atomic + /// - encoding: string encoding + func writeIfChanged(toFile: String) throws -> Bool { + do { + let original = try String(contentsOfFile: toFile) + guard original != self else { return false } + } catch { + print(error) + } + try write(toFile: toFile, atomically: true, encoding: .utf8) + return true + } +} + +public class HBMustacheTemplateAdapter: TemplateRendering { + private let template: HBMustacheTemplate + + public init(string: String) throws { + self.template = try HBMustacheTemplate(string: string) + } + + public func render(_ object: Any?) -> String { + self.template.render(object) + } +} + +class MockTemplate: TemplateRendering { + private let templateString: String + + init(string: String) { + self.templateString = string + } + + func render(_: Any?) -> String { + self.templateString + } +} + +class MockTemplateLibrary: TemplateLibrary { + private var templates: [String: MockTemplate] = [:] + + func register(_ template: MockTemplate, named name: String) { + self.templates[name] = template + } + + func getTemplate(named name: String) -> TemplateRendering? { + self.templates[name] + } +} + +public class HBMustacheLibraryAdapter: TemplateLibrary { + private var templates: [String: HBMustacheTemplateAdapter] = [:] + + public func register(_ template: HBMustacheTemplateAdapter, named name: String) { + self.templates[name] = template + } + + public func getTemplate(named name: String) -> TemplateRendering? { + self.templates[name] + } +} diff --git a/Sources/AWSLambdaDeploymentDescriptorGenerator/JSONSChemaReader.swift b/Sources/AWSLambdaDeploymentDescriptorGenerator/JSONSChemaReader.swift index 28cd685..86100cd 100644 --- a/Sources/AWSLambdaDeploymentDescriptorGenerator/JSONSChemaReader.swift +++ b/Sources/AWSLambdaDeploymentDescriptorGenerator/JSONSChemaReader.swift @@ -1,21 +1,64 @@ +// MARK: - JSONSchema .. + struct JSONSchema: Decodable { let id: String? - let schema: String + let schema: SchemaVersion + let additionalProperties: Bool? + let required: [String]? let description: String? - let type: JSONPrimitiveType + let type: [JSONPrimitiveType]? let properties: [String: JSONUnionType]? let definitions: [String: JSONUnionType]? - + + enum SchemaVersion: String, Decodable { + case draft4 = "http://json-schema.org/draft-04/schema#" + + init(from decoder: any Decoder) throws { + let container = try decoder.singleValueContainer() + + let value = try container.decode(String.self) + if value == SchemaVersion.draft4.rawValue { + self = .draft4 + } else { + throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: container.codingPath, debugDescription: "➡️ Unsupported schema version: \(value). Expected \(SchemaVersion.draft4.rawValue)")) + } + } + } + enum CodingKeys: String, CodingKey { case id = "$id" case schema = "$schema" + case additionalProperties + case required case description case type case properties case definitions } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.id = try container.decodeIfPresent(String.self, forKey: .id) + self.schema = try container.decode(SchemaVersion.self, forKey: .schema) + self.additionalProperties = try container.decodeIfPresent(Bool.self, forKey: .additionalProperties) + self.required = try container.decodeIfPresent([String].self, forKey: .required) + self.description = try container.decodeIfPresent(String.self, forKey: .description) + + if let types = try? container.decode([JSONPrimitiveType].self, forKey: .type) { + self.type = types + } else if let type = try? container.decode(JSONPrimitiveType.self, forKey: .type) { + self.type = [type] + } else { + self.type = nil + } + + self.properties = try container.decodeIfPresent([String: JSONUnionType].self, forKey: .properties) + self.definitions = try container.decodeIfPresent([String: JSONUnionType].self, forKey: .definitions) + } } +// MARK: - JSONPrimitiveType .. + // https://json-schema.org/understanding-json-schema/reference/type enum JSONPrimitiveType: Decodable, Equatable { case string @@ -25,9 +68,10 @@ enum JSONPrimitiveType: Decodable, Equatable { case integer case number case null - + init(from decoder: any Decoder) throws { let container = try decoder.singleValueContainer() + let value = try container.decode(String.self) if value == "string" { self = .string @@ -44,110 +88,155 @@ enum JSONPrimitiveType: Decodable, Equatable { } else if value == "number" { self = .number } else { - throw DecodingError.typeMismatch(JSONPrimitiveType.self, DecodingError.Context.init(codingPath: container.codingPath, debugDescription: "Unknown value (\(value)) for type property. Please make sure the schema refers to https://json-schema.org/understanding-json-schema/reference/type", underlyingError: nil)) + throw DecodingError.typeMismatch(JSONPrimitiveType.self, DecodingError.Context(codingPath: container.codingPath, debugDescription: "➡️ Unknown value (\(value)) for type property. Please make sure the schema refers to https://json-schema.org/understanding-json-schema/reference/type", underlyingError: nil)) } } } -// TODO change to a struct to support pattern ? - -enum ArrayItem: Decodable, Equatable { - case type(JSONPrimitiveType) - case ref(String) - - enum CodingKeys: String, CodingKey { - case type - case ref = "$ref" - } - init(from decoder: any Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - var allKeys = ArraySlice(container.allKeys) - guard let onlyKey = allKeys.popFirst(), allKeys.isEmpty else { - throw DecodingError.typeMismatch(ArrayItem.self, DecodingError.Context.init(codingPath: container.codingPath, debugDescription: "Invalid number of keys found, expected one, found \(allKeys.count).", underlyingError: nil)) - } - switch onlyKey { - case .type: - let primitiveType = try container.decode(JSONPrimitiveType.self, forKey: .type) - self = .type(primitiveType) - case .ref: - let ref = try container.decode(String.self, forKey: .ref) - self = .ref(ref) - } - } -} +// MARK: - JSONUnionType .. enum JSONUnionType: Decodable { case anyOf([JSONType]) - case allOf([JSONType]) + case allOf([JSONUnionType]) case type(JSONType) - + enum CodingKeys: String, CodingKey { case anyOf case allOf } - - init(from decoder: any Decoder) throws { + + init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) + var allKeys = ArraySlice(container.allKeys) - if let onlyKey = allKeys.popFirst(), allKeys.isEmpty { - // there is an anyOf or allOf key + + if let onlyKey = allKeys.popFirst(), allKeys.isEmpty { switch onlyKey { - case .allOf: fatalError("not yet implemented") case .anyOf: - let value = try container.decode(Array.self, forKey: .anyOf) + let value = try container.decode([JSONType].self, forKey: .anyOf) self = .anyOf(value) + + case .allOf: + let values = try container.decode([JSONUnionType].self, forKey: .allOf) + self = .allOf(values) } } else { - // there is no anyOf or allOf key, the entry is a raw JSONType, without key let container = try decoder.singleValueContainer() let jsonType = try container.decode(JSONType.self) self = .type(jsonType) } } - + func jsonType() -> JSONType { guard case .type(let jsonType) = self else { - fatalError("not a JSONType") + fatalError("Not a JSONType") } - return jsonType } + + public func any() -> [JSONType]? { + guard case .anyOf(let anyOf) = self else { + fatalError("not an anyOf") + } + + return anyOf + } + + public func all() -> [JSONUnionType]? { + guard case .allOf(let allOf) = self else { + fatalError("not an allOf") + } + + return allOf + } } -// TODO maybe convert this in an enum to cover all possible values of JSONPrimitiveType extensions +// MARK: - JSONType .. + struct JSONType: Decodable { - let type: JSONPrimitiveType + let type: [JSONPrimitiveType]? let required: [String]? let description: String? let additionalProperties: Bool? - + let enumeration: [String]? + let ref: String? + let subSchema: SubSchema? + + // Nested enums for specific schema types + indirect enum SubSchema { + case string(StringSchema) + case object(ObjectSchema) + case array(ArraySchema) + case number(NumberSchema) + case boolean + case null + } + // for Object // https://json-schema.org/understanding-json-schema/reference/object - let properties: [String: JSONUnionType]? - + struct ObjectSchema: Decodable { + let properties: [String: JSONUnionType]? + let minProperties: Int? + let maxProperties: Int? + let patternProperties: [String: JSONUnionType]? + + // Validate required within string array if present + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.properties = try container.decodeIfPresent([String: JSONUnionType].self, forKey: .properties) + self.minProperties = try container.decodeIfPresent(Int.self, forKey: .minProperties) + self.maxProperties = try container.decodeIfPresent(Int.self, forKey: .maxProperties) + self.patternProperties = try container.decodeIfPresent([String: JSONUnionType].self, forKey: .patternProperties) + } + } + // for String // https://json-schema.org/understanding-json-schema/reference/string - let pattern: String? // or RegEx ? - // not supported at the moment - // let minLength - // let maxLength - // let format: String // should be an enum to match specification - + struct StringSchema: Decodable { + let pattern: String? + let minLength: Int? + let maxLength: Int? + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.pattern = try container.decodeIfPresent(String.self, forKey: .pattern) + self.minLength = try container.decodeIfPresent(Int.self, forKey: .minLength) + self.maxLength = try container.decodeIfPresent(Int.self, forKey: .maxLength) + } + } + // for Array type // https://json-schema.org/understanding-json-schema/reference/array - let items: ArrayItem? - // not supported at the moment - // let prefixItems - // let unevaluatedItems - // let contains - // let minContains - // let maxContains - // let minItems - // let maxItems - // let uniqueItems + struct ArraySchema: Decodable { + let items: JSONType? + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) - enum CodingKeys: String, CodingKey { + self.items = try container.decodeIfPresent(JSONType.self, forKey: .items) + } + } + + // for Number + // https://json-schema.org/understanding-json-schema/reference/numeric + struct NumberSchema: Decodable { + let multipleOf: Double? + let minimum: Double? + let exclusiveMinimum: Bool? + let maximum: Double? + let exclusiveMaximum: Bool? + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.multipleOf = try container.decodeIfPresent(Double.self, forKey: .multipleOf) + self.minimum = try container.decodeIfPresent(Double.self, forKey: .minimum) + self.exclusiveMinimum = try container.decodeIfPresent(Bool.self, forKey: .exclusiveMinimum) + self.maximum = try container.decodeIfPresent(Double.self, forKey: .maximum) + self.exclusiveMaximum = try container.decodeIfPresent(Bool.self, forKey: .exclusiveMaximum) + } + } + + private enum CodingKeys: String, CodingKey { case type case items case pattern @@ -155,5 +244,94 @@ struct JSONType: Decodable { case description case properties case additionalProperties + case minItems + case exclusiveMinimum, exclusiveMaximum + case multipleOf, minimum, maximum + case enumeration = "enum" + case minProperties + case maxProperties + case ref = "$ref" + case minLength, maxLength + case patternProperties + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + if let types = try? container.decode([JSONPrimitiveType].self, forKey: .type) { + self.type = types + } else if let type = try? container.decode(JSONPrimitiveType.self, forKey: .type) { + self.type = [type] + } else { + self.type = nil + } + + self.required = try container.decodeIfPresent([String].self, forKey: .required) + self.description = try container.decodeIfPresent(String.self, forKey: .description) + self.additionalProperties = try container.decodeIfPresent(Bool.self, forKey: .additionalProperties) + self.enumeration = try container.decodeIfPresent([String].self, forKey: .enumeration) + self.ref = try container.decodeIfPresent(String.self, forKey: .ref) + + if let type = self.type, type.count == 1 { + switch type[0] { + case .string: + self.subSchema = try .string(StringSchema(from: decoder)) + case .object: + self.subSchema = try .object(ObjectSchema(from: decoder)) + case .boolean: + self.subSchema = .boolean + case .array: + self.subSchema = try .array(ArraySchema(from: decoder)) + case .integer, .number: + self.subSchema = try .number(NumberSchema(from: decoder)) + case .null: + self.subSchema = .null + } + } else { + self.subSchema = nil + } + } + + // Methods.. + func getProperties() -> [String: JSONUnionType]? { + if case .object(let schema) = subSchema { + return schema.properties + } + return nil + } + + func object() -> ObjectSchema? { + if case .object(let schema) = self.subSchema { + return schema + } + return nil + } + + func object(for property: String) -> JSONUnionType? { + if case .object(let schema) = self.subSchema { + return schema.properties?[property] + } + return nil + } + + func stringSchema() -> StringSchema? { + if case .string(let schema) = self.subSchema { + return schema + } + return nil + } + + func arraySchema() -> ArraySchema? { + if case .array(let schema) = self.subSchema { + return schema + } + return nil + } + + func items() -> JSONType? { + if case .array(let schema) = self.subSchema { + return schema.items + } + return nil } } diff --git a/Sources/AWSLambdaDeploymentDescriptorGenerator/samtranslator schema.json b/Sources/AWSLambdaDeploymentDescriptorGenerator/Resources/SamTranslatorSchema.json similarity index 99% rename from Sources/AWSLambdaDeploymentDescriptorGenerator/samtranslator schema.json rename to Sources/AWSLambdaDeploymentDescriptorGenerator/Resources/SamTranslatorSchema.json index ac671b9..c791334 100644 --- a/Sources/AWSLambdaDeploymentDescriptorGenerator/samtranslator schema.json +++ b/Sources/AWSLambdaDeploymentDescriptorGenerator/Resources/SamTranslatorSchema.json @@ -473,7 +473,7 @@ "required": [ "UserPool", "Trigger" - ], + ], "type": "object" }, "AWS::Serverless::Function.DynamoDBEvent": { @@ -802,15 +802,15 @@ "Schedule": { "type": "string" }, - "Name": { - "type": "string" - }, - "Description": { - "type": "string" - }, - "Enabled": { - "type": "boolean" - } + "Name": { + "type": "string" + }, + "Description": { + "type": "string" + }, + "Enabled": { + "type": "boolean" + } }, "required": [ "Schedule" diff --git a/Sources/AWSLambdaDeploymentDescriptorGenerator/Resources/TypeSchemaTranslator.json b/Sources/AWSLambdaDeploymentDescriptorGenerator/Resources/TypeSchemaTranslator.json new file mode 100644 index 0000000..481544c --- /dev/null +++ b/Sources/AWSLambdaDeploymentDescriptorGenerator/Resources/TypeSchemaTranslator.json @@ -0,0 +1,15 @@ +{ + "typeName": "testTypeName", + "properties": [ + { + "name": "propertyName1", + "type": "propertyType1" + }, + { + "name": "propertyName2", + "type": "propertyType2" + } + ], + "subTypes": [] +} + diff --git a/Sources/AWSLambdaDeploymentDescriptorGenerator/Resources/openapi-generator-config.yaml b/Sources/AWSLambdaDeploymentDescriptorGenerator/Resources/openapi-generator-config.yaml new file mode 100644 index 0000000..ecefb47 --- /dev/null +++ b/Sources/AWSLambdaDeploymentDescriptorGenerator/Resources/openapi-generator-config.yaml @@ -0,0 +1,3 @@ +generate: + - types + - client diff --git a/Sources/AWSLambdaDeploymentDescriptorGenerator/Resources/openapi.yaml b/Sources/AWSLambdaDeploymentDescriptorGenerator/Resources/openapi.yaml new file mode 100644 index 0000000..979a092 --- /dev/null +++ b/Sources/AWSLambdaDeploymentDescriptorGenerator/Resources/openapi.yaml @@ -0,0 +1,711 @@ +openapi: 3.1.0 +info: + title: SAMTemplate + version: 1.0.0 + +components: + schemas: + main: + "$schema": http://json-schema.org/draft-04/schema# + additionalProperties: false + properties: + AWSTemplateFormatVersion: + enum: + - '2010-09-09' + type: string + Conditions: + additionalProperties: false + patternProperties: + "^[a-zA-Z0-9]+$": + type: object + type: object + Description: + description: Template description + maxLength: 1024 + type: string + Mappings: + additionalProperties: false + patternProperties: + "^[a-zA-Z0-9]+$": + type: object + type: object + Metadata: + type: object + Outputs: + additionalProperties: false + maxProperties: 60 + minProperties: 1 + patternProperties: + "^[a-zA-Z0-9]+$": + type: object + type: object + Parameters: + additionalProperties: false + maxProperties: 50 + patternProperties: + "^[a-zA-Z0-9]+$": + "$ref": "#/components/schemas/Parameter" + type: object + Resources: + additionalProperties: false + patternProperties: + "^[a-zA-Z0-9]+$": + anyOf: + - "$ref": "#/components/schemas/AWSServerlessApi" + - "$ref": "#/components/schemas/AWSServerlessFunction" + - "$ref": "#/components/schemas/AWSServerlessSimpleTable" + - "$ref": "#/components/schemas/CloudFormationResource" + type: object + Transform: + enum: + - AWSServerless-2016-10-31 + type: string + Globals: + type: object + Properties: + type: object + required: + - Resources + type: object + AWSServerlessApi: + additionalProperties: false + properties: + DeletionPolicy: + type: string + UpdateReplacePolicy: + type: string + Condition: + type: string + DependsOn: + anyOf: + - pattern: "^[a-zA-Z0-9]+$" + type: string + - items: + pattern: "^[a-zA-Z0-9]+$" + type: string + type: array + Metadata: + type: object + Properties: + additionalProperties: false + properties: + CacheClusterEnabled: + type: boolean + CacheClusterSize: + type: string + DefinitionBody: + type: object + DefinitionUri: + anyOf: + - type: + - string + - "$ref": "#/components/schemas/AWSServerlessApi.S3Location" + Description: + type: string + Name: + type: string + StageName: + anyOf: + - type: string + - type: object + TracingEnabled: + type: boolean + Variables: + additionalProperties: false + patternProperties: + "^[a-zA-Z0-9]+$": + anyOf: + - type: string + - type: object + type: object + required: + - StageName + type: object + Type: + enum: + - AWSServerlessApi + type: string + required: + - Type + - Properties + type: object + AWSServerlessApi.S3Location: + additionalProperties: false + properties: + Bucket: + type: string + Key: + type: string + Version: + type: number + required: + - Bucket + - Key + type: object + AWSServerlessFunction: + additionalProperties: false + properties: + DeletionPolicy: + type: string + UpdateReplacePolicy: + type: string + Condition: + type: string + DependsOn: + anyOf: + - pattern: "^[a-zA-Z0-9]+$" + type: string + - items: + pattern: "^[a-zA-Z0-9]+$" + type: string + type: array + Metadata: + type: object + Properties: + allOf: + - anyOf: + - properties: + InlineCode: + type: string + - properties: + CodeUri: + anyOf: + - type: + - string + - "$ref": "#/components/schemas/AWSServerlessFunction.S3Location" + - properties: + DeadLetterQueue: + "$ref": "#/components/schemas/AWSServerlessFunction.DeadLetterQueue" + Description: + type: string + Environment: + "$ref": "#/components/schemas/AWSServerlessFunction.FunctionEnvironment" + Events: + additionalProperties: false + patternProperties: + "^[a-zA-Z0-9]+$": + "$ref": "#/components/schemas/AWSServerlessFunction.EventSource" + type: object + FunctionName: + type: string + Handler: + type: string + KmsKeyArn: + type: string + MemorySize: + type: number + Policies: + anyOf: + - type: + - string + - items: + type: string + type: array + - "$ref": "#/components/schemas/AWSServerlessFunction.IAMPolicyDocument" + - items: + "$ref": "#/components/schemas/AWSServerlessFunction.IAMPolicyDocument" + type: array + Role: + anyOf: + - type: string + - type: object + Runtime: + type: string + Tags: + additionalProperties: false + patternProperties: + "^[a-zA-Z0-9]+$": + type: string + type: object + Timeout: + type: number + Tracing: + type: string + VpcConfig: + "$ref": "#/components/schemas/AWSServerlessFunction.VpcConfig" + required: + - Handler + - Runtime + type: object + Type: + enum: + - AWSServerlessFunction + type: string + required: + - Type + - Properties + type: object + AWSServerlessFunction.AlexaSkillEvent: + additionalProperties: false + properties: + Variables: + additionalProperties: false + patternProperties: + "^[a-zA-Z0-9]+$": + type: string + type: object + type: object + AWSServerlessFunction.ApiEvent: + additionalProperties: false + properties: + Method: + type: string + Path: + type: string + RestApiId: + anyOf: + - type: string + - type: object + required: + - Method + - Path + type: object + AWSServerlessFunction.CloudWatchEventEvent: + additionalProperties: false + properties: + EventBusName: + type: string + Input: + type: string + InputPath: + type: string + Pattern: + type: object + required: + - Pattern + type: object + AWSServerlessFunction.EventBridgeRule: + additionalProperties: false + properties: + Input: + type: string + InputPath: + type: string + Pattern: + type: object + State: + type: string + RuleName: + type: string + DeadLetterConfig: + additionalProperties: false + properties: + Arn: + type: string + Type: + type: string + QueueLogicalId: + type: string + type: object + RetryPolicy: + additionalProperties: false + minProperties: 1 + properties: + MaximumEventAgeInSeconds: + type: number + MaximumRetryAttempts: + type: number + type: object + required: + - Pattern + type: object + AWSServerlessFunction.LogEvent: + additionalProperties: false + properties: + LogGroupName: + type: string + FilterPattern: + type: string + required: + - LogGroupName + - FilterPattern + type: object + AWSServerlessFunction.DeadLetterQueue: + additionalProperties: false + properties: + TargetArn: + type: string + Type: + type: string + required: + - TargetArn + - Type + type: object + AWSServerlessFunction.CognitoEvent: + additionalProperties: false + properties: + UserPool: + anyOf: + - type: string + - type: object + Trigger: + anyOf: + - type: string + - items: + type: string + type: array + required: + - UserPool + - Trigger + type: object + AWSServerlessFunction.DynamoDBEvent: + additionalProperties: false + properties: + BatchSize: + type: number + StartingPosition: + type: string + Stream: + type: string + Enabled: + type: boolean + required: + - BatchSize + - StartingPosition + - Stream + type: object + AWSServerlessFunction.EventSource: + additionalProperties: false + properties: + Properties: + anyOf: + - "$ref": "#/components/schemas/AWSServerlessFunction.S3Event" + - "$ref": "#/components/schemas/AWSServerlessFunction.SNSEvent" + - "$ref": "#/components/schemas/AWSServerlessFunction.KinesisEvent" + - "$ref": "#/components/schemas/AWSServerlessFunction.MSKEvent" + - "$ref": "#/components/schemas/AWSServerlessFunction.MQEvent" + - "$ref": "#/components/schemas/AWSServerlessFunction.SQSEvent" + - "$ref": "#/components/schemas/AWSServerlessFunction.DynamoDBEvent" + - "$ref": "#/components/schemas/AWSServerlessFunction.ApiEvent" + - "$ref": "#/components/schemas/AWSServerlessFunction.ScheduleEvent" + - "$ref": "#/components/schemas/AWSServerlessFunction.CloudWatchEventEvent" + - "$ref": "#/components/schemas/AWSServerlessFunction.EventBridgeRule" + - "$ref": "#/components/schemas/AWSServerlessFunction.LogEvent" + - "$ref": "#/components/schemas/AWSServerlessFunction.IoTRuleEvent" + - "$ref": "#/components/schemas/AWSServerlessFunction.AlexaSkillEvent" + - "$ref": "#/components/schemas/AWSServerlessFunction.CognitoEvent" + Type: + type: string + required: + - Properties + - Type + type: object + AWSServerlessFunction.FunctionEnvironment: + additionalProperties: false + properties: + Variables: + additionalProperties: false + patternProperties: + "^[a-zA-Z0-9]+$": + type: string + type: object + required: + - Variables + type: object + AWSServerlessFunction.IAMPolicyDocument: + additionalProperties: false + properties: + Statement: + items: + type: object + type: array + required: + - Statement + type: object + AWSServerlessFunction.IoTRuleEvent: + additionalProperties: false + properties: + AwsIotSqlVersion: + type: string + Sql: + type: string + required: + - Sql + type: object + AWSServerlessFunction.KinesisEvent: + additionalProperties: false + properties: + BatchSize: + type: number + StartingPosition: + type: string + Stream: + type: string + Enabled: + type: boolean + required: + - StartingPosition + - Stream + type: object + AWSServerlessFunction.MSKEvent: + additionalProperties: false + properties: + StartingPosition: + type: string + Stream: + type: string + Topics: + type: array + required: + - StartingPosition + - Stream + - Topics + type: object + AWSServerlessFunction.MQEvent: + additionalProperties: false + properties: + Broker: + type: string + Queues: + type: array + SourceAccessConfigurations: + type: array + required: + - Broker + - Queues + - SourceAccessConfigurations + type: object + AWSServerlessFunction.SQSEvent: + additionalProperties: false + properties: + BatchSize: + type: number + Queue: + anyOf: + - type: string + - type: object + Enabled: + type: boolean + required: + - Queue + type: object + AWSServerlessFunction.S3Event: + additionalProperties: false + properties: + Bucket: + anyOf: + - type: string + - type: object + Events: + anyOf: + - type: + - string + - items: + type: string + type: array + Filter: + "$ref": "#/components/schemas/AWSServerlessFunction.S3NotificationFilter" + required: + - Bucket + - Events + type: object + AWSServerlessFunction.S3Location: + additionalProperties: false + properties: + Bucket: + type: string + Key: + type: string + Version: + type: number + required: + - Bucket + - Key + type: object + AWSServerlessFunction.S3NotificationFilter: + additionalProperties: false + properties: + S3Key: + anyOf: + - type: string + - type: object + required: + - S3Key + type: object + AWSServerlessFunction.SNSEvent: + additionalProperties: false + properties: + Topic: + type: string + Region: + type: string + FilterPolicy: + type: object + FilterPolicyScope: + type: string + required: + - Topic + type: object + AWSServerlessFunction.ScheduleEvent: + additionalProperties: false + properties: + Input: + type: string + Schedule: + type: string + Name: + type: string + Description: + type: string + Enabled: + type: boolean + required: + - Schedule + type: object + AWSServerlessFunction.VpcConfig: + additionalProperties: false + properties: + SecurityGroupIds: + items: + type: string + type: array + SubnetIds: + items: + type: string + type: array + SubnetIdsUsingRef: + items: + type: object + type: array + required: + - SecurityGroupIds + - SubnetIds + type: object + AWSServerlessSimpleTable: + additionalProperties: false + properties: + DeletionPolicy: + type: string + UpdateReplacePolicy: + type: string + Condition: + type: string + DependsOn: + anyOf: + - pattern: "^[a-zA-Z0-9]+$" + type: string + - items: + pattern: "^[a-zA-Z0-9]+$" + type: string + type: array + Metadata: + type: object + Properties: + additionalProperties: false + properties: + PrimaryKey: + "$ref": "#/components/schemas/AWSServerlessSimpleTable.PrimaryKey" + ProvisionedThroughput: + "$ref": "#/components/schemas/AWSServerlessSimpleTable.ProvisionedThroughput" + SSESpecification: + "$ref": "#/components/schemas/AWSServerlessSimpleTable.SSESpecification" + type: object + Type: + enum: + - AWSServerlessSimpleTable + type: string + required: + - Type + type: object + AWSServerlessSimpleTable.PrimaryKey: + additionalProperties: false + properties: + Name: + type: string + Type: + type: string + required: + - Type + type: object + AWSServerlessSimpleTable.ProvisionedThroughput: + additionalProperties: false + properties: + ReadCapacityUnits: + type: number + WriteCapacityUnits: + type: number + required: + - WriteCapacityUnits + type: object + AWSServerlessSimpleTable.SSESpecification: + additionalProperties: false + properties: + SSEEnabled: + type: boolean + required: + - SSEEnabled + type: object + CloudFormationResource: + additionalProperties: true + properties: + Type: + pattern: "^(?!^AWSServerless).*" + type: string + required: + - Type + type: object + Parameter: + additionalProperties: false + properties: + AllowedPattern: + type: string + AllowedValues: + type: array + ConstraintDescription: + type: string + Default: + type: string + Description: + type: string + MaxLength: + type: string + MaxValue: + type: string + MinLength: + type: string + MinValue: + type: string + NoEcho: + type: + - string + - boolean + Type: + enum: + - String + - Number + - List + - CommaDelimitedList + - AWSEC2AvailabilityZoneName + - AWSEC2ImageId + - AWSEC2InstanceId + - AWSEC2KeyPairKeyName + - AWSEC2SecurityGroupGroupName + - AWSEC2SecurityGroupId + - AWSEC2SubnetId + - AWSEC2VolumeId + - AWSEC2VPCId + - AWSRoute53HostedZoneId + - List + - List + - List + - List + - List + - List + - List + - List + - List + - List + type: string + required: + - Type + type: object + Tag: + additionalProperties: false + properties: + Key: + type: string + Value: + anyOf: + - type: string + - type: object + type: object diff --git a/Sources/AWSLambdaDeploymentDescriptorGenerator/String.swift b/Sources/AWSLambdaDeploymentDescriptorGenerator/String.swift new file mode 100644 index 0000000..091f4fb --- /dev/null +++ b/Sources/AWSLambdaDeploymentDescriptorGenerator/String.swift @@ -0,0 +1,271 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Soto for AWS open source project +// +// Copyright (c) 2017-2020 the Soto project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Soto project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation + +extension String { + public func toSwiftLabelCase() -> String { + let snakeCase = self.replacingOccurrences(of: "-", with: "_") + if snakeCase.allLetterIsSnakeUppercased() { + return snakeCase.lowercased().camelCased(capitalize: false) + } + return snakeCase.camelCased(capitalize: false) + } + + public func toSwiftVariableCase() -> String { + self.toSwiftLabelCase().reservedwordEscaped() + } + + public func toSwiftClassCase() -> String { + self.replacingOccurrences(of: "-", with: "_") + .camelCased(capitalize: true) + .reservedwordEscaped() + } + + // for some reason the Region and Partition enum are not camel cased + public func toSwiftRegionEnumCase() -> String { + self.replacingOccurrences(of: "-", with: "") + } + + public func toSwiftEnumCase() -> String { + self + .replacingOccurrences(of: ".", with: "_") + .replacingOccurrences(of: ":", with: "_") + .replacingOccurrences(of: "-", with: "_") + .replacingOccurrences(of: " ", with: "_") + .replacingOccurrences(of: "/", with: "_") + .replacingOccurrences(of: "(", with: "_") + .replacingOccurrences(of: ")", with: "_") + .replacingOccurrences(of: "*", with: "all") + .toSwiftLabelCase() + .reservedwordEscaped() + } + + public func tagStriped() -> String { + self.replacingOccurrences(of: "<[^>]+>", with: "", options: .regularExpression, range: nil) + } + + /// back slash encode special characters + public func addingBackslashEncoding() -> String { + var newString = "" + for c in self { + if let replacement = String.backslashEncodeMap[c] { + newString.append(contentsOf: replacement) + } else { + newString.append(c) + } + } + return newString + } + + func camelCased(capitalize: Bool) -> String { + let items = self.split(separator: "_") + let firstWord = items.first! + let firstWordProcessed: String + if capitalize { + firstWordProcessed = firstWord.upperFirst() + } else { + firstWordProcessed = firstWord.lowerFirstWord() + } + let remainingItems = items.dropFirst().map { word -> String in + if word.allLetterIsSnakeUppercased() { + return String(word) + } + return word.capitalized + } + return firstWordProcessed + remainingItems.joined() + } + + func reservedwordEscaped() -> String { + if self == "self" { + return "_self" + } + if swiftReservedWords.contains(self) { + return "`\(self)`" + } + return self + } + + private static let backslashEncodeMap: [String.Element: String] = [ + "\"": "\\\"", + "\\": "\\\\", + "\n": "\\n", + "\t": "\\t", + "\r": "\\r", + ] + + func deletingPrefix(_ prefix: String) -> String { + guard self.hasPrefix(prefix) else { return self } + return String(self.dropFirst(prefix.count)) + } + + mutating func deletePrefix(_ prefix: String) { + self = self.deletingPrefix(prefix) + } + + func deletingSuffix(_ suffix: String) -> String { + guard self.hasSuffix(suffix) else { return self } + return String(self.dropLast(suffix.count)) + } + + mutating func deleteSuffix(_ suffix: String) { + self = self.deletingSuffix(suffix) + } + + func removingWhitespaces() -> String { + components(separatedBy: .whitespaces).joined() + } + + mutating func removeWhitespaces() { + self = self.removingWhitespaces() + } + + func removingCharacterSet(in characterset: CharacterSet) -> String { + components(separatedBy: characterset).joined() + } + + mutating func removeCharacterSet(in characterset: CharacterSet) { + self = self.removingCharacterSet(in: characterset) + } + + private func capitalizingFirstLetter() -> String { + prefix(1).capitalized + dropFirst() + } + + private mutating func capitalizeFirstLetter() { + self = self.capitalizingFirstLetter() + } + + mutating func trimCharacters(in characterset: CharacterSet) { + self = self.trimmingCharacters(in: characterset) + } +} + +extension StringProtocol { + func allLetterIsNumeric() -> Bool { + for c in self { + if !c.isNumber { + return false + } + } + return true + } + + func dropLast(while: (Self.Element) throws -> Bool) rethrows -> Self.SubSequence { + var position = self.endIndex + var count = 0 + while position != self.startIndex { + position = self.index(before: position) + if try !`while`(self[position]) { + break + } + count += 1 + } + return self.dropLast(count) + } + + private func lowerFirst() -> String { + String(self[startIndex]).lowercased() + self[index(after: startIndex)...] + } + + fileprivate func upperFirst() -> String { + String(self[self.startIndex]).uppercased() + self[index(after: startIndex)...] + } + + /// Lowercase first letter, or if first word is an uppercase acronym then lowercase the whole of the acronym + fileprivate func lowerFirstWord() -> String { + var firstLowercase = self.startIndex + var lastUppercaseOptional: Self.Index? + // get last uppercase character, first lowercase character + while firstLowercase != self.endIndex, self[firstLowercase].isSnakeUppercase() { + lastUppercaseOptional = firstLowercase + firstLowercase = self.index(after: firstLowercase) + } + // if first character was never set first character must be lowercase + guard let lastUppercase = lastUppercaseOptional else { + return String(self) + } + if firstLowercase == self.endIndex { + // if first lowercase letter is the end index then whole word is uppercase and + // should be wholly lowercased + return self.lowercased() + } else if lastUppercase == self.startIndex { + // if last uppercase letter is the first letter then only lower that character + return self.lowerFirst() + } else { + // We have an acronym at the start, lowercase the whole of it + return self[startIndex ..< lastUppercase].lowercased() + self[lastUppercase...] + } + } + + fileprivate func allLetterIsSnakeUppercased() -> Bool { + for c in self { + if !c.isSnakeUppercase() { + return false + } + } + return true + } +} + +extension Character { + fileprivate func isSnakeUppercase() -> Bool { + self.isNumber || ("A" ... "Z").contains(self) || self == "_" + } +} + +private let swiftReservedWords: Set = [ + "as", + "async", + "await", + "break", + "case", + "catch", + "class", + "continue", + "default", + "defer", + "do", + "else", + "enum", + "extension", + "false", + "for", + "func", + "if", + "import", + "in", + "internal", + "is", + "let", + "nil", + "operator", + "private", + "protocol", + "Protocol", + "public", + "repeat", + "return", + "self", + "Self", + "static", + "struct", + "switch", + "true", + "try", + "Type", + "var", + "where", + "while", +] diff --git a/Sources/AWSLambdaDeploymentDescriptorGenerator/Templates/Templates.swift b/Sources/AWSLambdaDeploymentDescriptorGenerator/Templates/Templates.swift new file mode 100644 index 0000000..12277f7 --- /dev/null +++ b/Sources/AWSLambdaDeploymentDescriptorGenerator/Templates/Templates.swift @@ -0,0 +1,21 @@ +import HummingbirdMustache + +public enum Templates { + static var values: [String: String] = [ + "enum": enumTemplate, + "structTemplate": structTemplate, + ] + + public static func createLibrary() throws -> TemplateLibrary { + let library = HBMustacheLibraryAdapter() + for (name, templateString) in self.values { + do { + let template = try HBMustacheTemplateAdapter(string: templateString) + library.register(template, named: name) + } catch { + print("Error creating template '\(name)': \(error)") + } + } + return library + } +} diff --git a/Sources/AWSLambdaDeploymentDescriptorGenerator/Templates/enum.swift b/Sources/AWSLambdaDeploymentDescriptorGenerator/Templates/enum.swift new file mode 100644 index 0000000..c4906cc --- /dev/null +++ b/Sources/AWSLambdaDeploymentDescriptorGenerator/Templates/enum.swift @@ -0,0 +1,47 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Soto for AWS open source project +// +// Copyright (c) 2017-2023 the Soto project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Soto project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +extension Templates { + static let enumTemplate = """ + {{%CONTENT_TYPE:TEXT}} + {{#isExtensible}} + {{scope}} struct {{name}}: RawRepresentable, Equatable, Codable, Sendable, CodingKeyRepresentable { + {{scope}} var rawValue: String + + {{scope}} init(rawValue: String) { + self.rawValue = rawValue + } + + {{#values}} + {{#documentation}} + {{>comment}} + {{/documentation}} + {{scope}} static var {{case}}: Self { .init(rawValue: "{{rawValue}}") } + {{/values}} + } + {{/isExtensible}} + {{^isExtensible}} + {{scope}} enum {{name}}: String, CustomStringConvertible, Codable, Sendable, CodingKeyRepresentable { + {{#values}} + {{#documentation}} + {{>comment}} + {{/documentation}} + case {{case}} = "{{rawValue}}" + {{/values}} + {{scope}} var description: String { return self.rawValue } + } + {{/isExtensible}} + + """ +} diff --git a/Sources/AWSLambdaDeploymentDescriptorGenerator/Templates/struct.swift b/Sources/AWSLambdaDeploymentDescriptorGenerator/Templates/struct.swift new file mode 100644 index 0000000..733e159 --- /dev/null +++ b/Sources/AWSLambdaDeploymentDescriptorGenerator/Templates/struct.swift @@ -0,0 +1,76 @@ +extension Templates { + static let structTemplate = #""" + {{%CONTENT_TYPE:TEXT}} + + {{scope}} struct {{name}}: {{shapeProtocol}} { + {{scope}} let typeName: String + {{#properties}} + {{scope}} let {{variable}}: {{type}} + {{/properties}} + {{#subTypes}} + {{scope}} let subTypes: [{{name}}] + {{/subTypes}} + + {{scope}} let properties: [Property] + + {{scope}} init(typeName: String, {{#properties}}{{variable}}: {{type}}{{#isOptional}}?{{/isOptional}}{{^last}}, {{/last}}{{/properties}}{{#subTypes}}, subTypes: [{{name}}]{{/subTypes}}, properties: [Property]) { + self.typeName = typeName + {{#properties}} + self.{{variable}} = {{variable}} + {{/properties}} + {{#subTypes}} + self.subTypes = subTypes + {{/subTypes}} + self.properties = properties + } + + {{scope}} init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.typeName = try container.decode(String.self, forKey: .typeName) + {{#properties}} + self.{{variable}} = try container.decode({{type}}.self, forKey: .{{variable}}) + {{/properties}} + {{#subTypes}} + self.subTypes = try container.decode([{{name}}].self, forKey: .subTypes) + {{/subTypes}} + self.properties = try container.decode([Property].self, forKey: .properties) + } + + private enum CodingKeys: String, CodingKey { + case typeName + {{#properties}} + case {{variable}} + {{/properties}} + {{#subTypes}} + case subTypes + {{/subTypes}} + case properties + } + + {{scope}} struct Property: Codable { + {{scope}} let name: String + {{scope}} let type: String + + {{scope}} init(name: String, type: String) { + self.name = name + self.type = type + } + + {{scope}} init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.name = try container.decode(String.self, forKey: .name) + self.type = try container.decode(String.self, forKey: .type) + } + + private enum CodingKeys: String, CodingKey { + case name + case type + } + } + } + + {{#subTypes}} + {{>structTemplate}} + {{/subTypes}} + """# +} diff --git a/Sources/AWSLambdaDeploymentDescriptorGenerator/TypeSchema.swift b/Sources/AWSLambdaDeploymentDescriptorGenerator/TypeSchema.swift new file mode 100644 index 0000000..0984155 --- /dev/null +++ b/Sources/AWSLambdaDeploymentDescriptorGenerator/TypeSchema.swift @@ -0,0 +1,51 @@ + +public protocol TemplateRendering { + func render(_ object: Any?) -> String +} + +public protocol TemplateLibrary { + func getTemplate(named name: String) -> TemplateRendering? +} + +struct TypeSchema: Decodable, Equatable { + let typeName: String + let properties: [Property] + let subTypes: [TypeSchema] + + init(typeName: String, properties: [Property], subTypes: [TypeSchema]) { + self.typeName = typeName + self.properties = properties + self.subTypes = subTypes + } + + struct Property: Decodable, Equatable { + let name: String? + let type: String + + init(name: String, type: String) { + self.name = name + self.type = type + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.name = try container.decode(String.self, forKey: .name) + self.type = try container.decode(String.self, forKey: .type) + } + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.typeName = try container.decode(String.self, forKey: .typeName) + self.properties = try container.decode([Property].self, forKey: .properties) + self.subTypes = try container.decode([TypeSchema].self, forKey: .subTypes) + } + + private enum CodingKeys: String, CodingKey { + case typeName + case properties + case subTypes + case name + case type + } +} diff --git a/Sources/AWSLambdaDeploymentDescriptorGenerator/dummyGenerated.swift b/Sources/AWSLambdaDeploymentDescriptorGenerator/dummyGenerated.swift new file mode 100644 index 0000000..93e8936 --- /dev/null +++ b/Sources/AWSLambdaDeploymentDescriptorGenerator/dummyGenerated.swift @@ -0,0 +1,29 @@ + +public struct Example: Decodable, Equatable { + let typeName: String + let properties: [Property] + let subTypes: [Example] + init(typeName: String, properties: [Property], subTypes: [Example]) { + self.typeName = typeName + self.properties = properties + self.subTypes = subTypes + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.typeName = try container.decode(String.self, forKey: .typeName) + self.properties = try container.decode([Property].self, forKey: .properties) + self.subTypes = try container.decode([Example].self, forKey: .subTypes) + } + + public struct Property: Decodable, Equatable { + let name: String? + let type: String + } + + private enum CodingKeys: String, CodingKey { + case typeName = "typeName" + case properties + case subTypes + } +} diff --git a/Sources/DeploymentDescriptorGeneratorExecutable/DeployDescriptorGenerator.swift b/Sources/DeploymentDescriptorGeneratorExecutable/DeployDescriptorGenerator.swift new file mode 100644 index 0000000..5a5d4c0 --- /dev/null +++ b/Sources/DeploymentDescriptorGeneratorExecutable/DeployDescriptorGenerator.swift @@ -0,0 +1,51 @@ + +import ArgumentParser +import AWSLambdaDeploymentDescriptorGenerator +import Foundation +import Logging + +@main +struct Command: AsyncParsableCommand, DeploymentDescriptorGeneratorCommand { + @Option(name: .long, help: "Folder to output service files to") + var outputFolder: String + + @Option(name: .long, help: "Folder to find model files") + var inputFolder: String? + + @Option(name: .shortAndLong, help: "Input model file") + var inputFile: String? + + @Option(name: [.short, .customLong("config")], help: "Configuration file") + var configFile: String? + + @Option(name: .shortAndLong, help: "Prefix applied to output swift files") + var prefix: String? + + @Option(name: .shortAndLong, help: "Endpoint JSON file") + var endpoints: String = Self.defaultEndpoints + + @Option(name: .shortAndLong, help: "Only output files for specified module") + var module: String? + + @Flag(name: .long, inversion: .prefixedNo, help: "Output files") + var output: Bool = true + + @Option(name: .long, help: "Log Level (trace, debug, info, error)") + var logLevel: String? + + static var rootPath: String { + #file + .split(separator: "/", omittingEmptySubsequences: false) + .dropLast(3) + .map { String(describing: $0) } + .joined(separator: "/") + } + + static var defaultOutputFolder: String { "\(rootPath)/samGenerated/models" } + static var defaultInputFolder: String { "\(rootPath)/samGenerated/inputs" } + static var defaultEndpoints: String { "\(rootPath)/samGenerated/json" } + + func run() async throws { +// try await DeploymentDescriptorGenerator().generate() + } +} diff --git a/Tests/AWSLambdaDeploymentDescriptorGeneratorTests/DeploymentDescriptorGeneratorTest.swift b/Tests/AWSLambdaDeploymentDescriptorGeneratorTests/DeploymentDescriptorGeneratorTest.swift new file mode 100644 index 0000000..e989893 --- /dev/null +++ b/Tests/AWSLambdaDeploymentDescriptorGeneratorTests/DeploymentDescriptorGeneratorTest.swift @@ -0,0 +1,50 @@ +// +// DeploymentDescriptorGeneratorTest.swift +// + +@testable import AWSLambdaDeploymentDescriptorGenerator +import Foundation +import HummingbirdMustache +import SwiftSyntax +import XCTest + +final class DeploymentDescriptorGeneratorTest: XCTestCase { + var generator: DeploymentDescriptorGenerator! + + // [https://github.com/apple/swift/blob/9af806e8fd93df3499b1811deae7729176879cb0/test/stdlib/TestJSONEncoder.swift] + func testTypeSchemaTranslatorReader() throws { + // load schema from file (the file must be referenced in the Resource section of Package.swift + // https://stackoverflow.com/questions/47177036/use-resources-in-unit-tests-with-swift-package-manager + let filePath = Bundle.module.path(forResource: "TypeSchemaTranslator", ofType: "json") + let fp = try XCTUnwrap(filePath) + let url = URL(fileURLWithPath: fp) + + let schemaData = try Data(contentsOf: url) + + let decoder = JSONDecoder() + _ = try decoder.decode(TypeSchema.self, from: schemaData) + } + + func testGenerateCodableStruct() { + let template = try! HBMustacheTemplate(string: "") + let library = HBMustacheLibrary() + library.register(template, named: "structTemplate") + + let generator = DeploymentDescriptorGenerator() + + generator.writeGeneratedStructWithSwiftSyntax() + } + + private func captureOutput(_ closure: () -> Void) -> String { + let pipe = Pipe() + let previousStdout = dup(STDOUT_FILENO) + dup2(pipe.fileHandleForWriting.fileDescriptor, STDOUT_FILENO) + + closure() + + fflush(stdout) + dup2(previousStdout, STDOUT_FILENO) + let data = pipe.fileHandleForReading.readDataToEndOfFile() + return String(data: data, encoding: .utf8) ?? "" + } +} diff --git a/Tests/AWSLambdaDeploymentDescriptorGeneratorTests/JSONSchemaReaderTest.swift b/Tests/AWSLambdaDeploymentDescriptorGeneratorTests/JSONSchemaReaderTest.swift index 562312a..5edf055 100644 --- a/Tests/AWSLambdaDeploymentDescriptorGeneratorTests/JSONSchemaReaderTest.swift +++ b/Tests/AWSLambdaDeploymentDescriptorGeneratorTests/JSONSchemaReaderTest.swift @@ -12,61 +12,678 @@ // // ===----------------------------------------------------------------------===// +@testable import AWSLambdaDeploymentDescriptorGenerator import Foundation import XCTest -@testable import AWSLambdaDeploymentDescriptorGenerator final class JSONSchemaReaderTest: XCTestCase { - + // [https://github.com/apple/swift/blob/9af806e8fd93df3499b1811deae7729176879cb0/test/stdlib/TestJSONEncoder.swift] func testSimpleJSONSchemaReader() throws { - // load schema from file (the file must be referenced in the Resource section of Package.swift // https://stackoverflow.com/questions/47177036/use-resources-in-unit-tests-with-swift-package-manager let filePath = Bundle.module.path(forResource: "SimpleJSONSchema", ofType: "json") let fp = try XCTUnwrap(filePath) - let url = URL(fileURLWithPath: fp) - + let url = URL(fileURLWithPath: fp) + let schemaData = try Data(contentsOf: url) let decoder = JSONDecoder() let schema = try decoder.decode(JSONSchema.self, from: schemaData) -// print(schema) - - XCTAssertTrue(schema.type == .object) + + XCTAssertTrue(schema.type?.first == .object) + XCTAssertNotNil(schema.properties) XCTAssertTrue(schema.properties?.count == 2) - let fruits = try XCTUnwrap(schema.properties?["fruits"]) - XCTAssertTrue(fruits.jsonType().type == .array) - XCTAssertTrue(fruits.jsonType().items == ArrayItem.type(.string)) - - let vegetable = try XCTUnwrap(schema.properties?["vegetables"]) - XCTAssertTrue(vegetable.jsonType().items == ArrayItem.ref("#/definitions/veggie")) - - XCTAssertTrue(schema.definitions?.count == 2) - let veggie = try XCTUnwrap(schema.definitions?["veggie"]) - XCTAssertTrue(veggie.jsonType().type == .object) - XCTAssertTrue(veggie.jsonType().required?.count == 2) - let veggieName = try XCTUnwrap(veggie.jsonType().properties?["veggieName"]) - XCTAssertTrue(veggieName.jsonType().type == .string) - let veggieLike = try XCTUnwrap(veggie.jsonType().properties?["veggieLike"]) - XCTAssertTrue(veggieLike.jsonType().type == .boolean) + + // fruits properties + if let fruitsProperty = schema.properties?["fruits"] { + switch fruitsProperty { + case .type(let jsonType): + XCTAssertEqual(jsonType.type?.first, .array) + XCTAssertEqual(jsonType.items()?.type?.first, .string) + default: + XCTFail("Expected a JSONType") + } + } + + // vegetables properties + if let vegetablesProperty = schema.properties?["vegetables"] { + switch vegetablesProperty { + case .type(let jsonType): + XCTAssertEqual(jsonType.type?.first, .array) + if let items = jsonType.items() { + XCTAssertEqual(items.ref, "#/definitions/veggie") + } + default: + XCTFail("Expected a JSONType") + } + } + + // veggie definitions + if let veggieDefinition = schema.definitions?["veggie"] { + switch veggieDefinition { + case .type(let jsonType): + XCTAssertEqual(jsonType.type?.first, .object) + XCTAssertEqual(jsonType.required, ["veggieName", "veggieLike"]) + + if let properties = jsonType.getProperties() { + XCTAssertEqual(properties.count, 2) + + if let veggieNameProperty = properties["veggieName"] { + switch veggieNameProperty { + case .type(let jsonType): + XCTAssertEqual(jsonType.type?.first, .string) + XCTAssertEqual(jsonType.description, "The name of the vegetable.") + default: + XCTFail("Expected a JSONType") + } + } + + if let veggieLikeProperty = properties["veggieLike"] { + switch veggieLikeProperty { + case .type(let jsonType): + XCTAssertEqual(jsonType.type?.first, .boolean) + XCTAssertEqual(jsonType.description, "Do I like this vegetable?") + default: + XCTFail("Expected a JSONType") + } + } + } + default: + XCTFail("Expected a JSONType") + } + } + + // DependsOn definitions + if let dependsOnDefinition = schema.definitions?["DependsOn"] { + switch dependsOnDefinition { + case .anyOf(let jsonTypes): + XCTAssertEqual(jsonTypes.count, 2) + + if let firstType = jsonTypes.first, let secondType = jsonTypes.last { + switch firstType.subSchema { + case .string(let stringSchema): + XCTAssertEqual(stringSchema.pattern, "^[a-zA-Z0-9]+$") + XCTAssertEqual(firstType.type?.first, .string) + default: + XCTFail("Expected a StringSchema") + } + + switch secondType.subSchema { + case .array(let arraySchema): + XCTAssertEqual(arraySchema.items?.type?.first, .string) + XCTAssertEqual(secondType.type?.first, .array) + default: + XCTFail("Expected an ArraySchema") + } + } + default: + XCTFail("Expected an anyOf type") + } + } } func testSAMJSONSchemaReader() throws { - // load schema from file (the file must be referenced in the Resource section of Package.swift // https://stackoverflow.com/questions/47177036/use-resources-in-unit-tests-with-swift-package-manager let filePath = Bundle.module.path(forResource: "SAMJSONSchema", ofType: "json") let fp = try XCTUnwrap(filePath) - let url = URL(fileURLWithPath: fp) - - let schemaData = try Data(contentsOf: url) + let url = URL(fileURLWithPath: fp) + let schemaData = try Data(contentsOf: url) let decoder = JSONDecoder() let schema = try decoder.decode(JSONSchema.self, from: schemaData) - print(schema) + let schemaURLString = schema.schema.rawValue + + // schema URL + XCTAssertTrue(schemaURLString.hasPrefix("http://json-schema.org/draft-"), "Unknown schema version format") + let expectedVersion = JSONSchema.SchemaVersion(rawValue: schemaURLString) + XCTAssertNotNil(expectedVersion, "Schema version not found in supported versions") + XCTAssertEqual(schema.schema.rawValue, "http://json-schema.org/draft-04/schema#") + + // additionalProperties + XCTAssertFalse(schema.additionalProperties!) + + // required properties + XCTAssertEqual(schema.required, ["Resources"]) + + // type + XCTAssertEqual(schema.type?.first, .object) + + // properties: + XCTAssertNotNil(schema.properties) + + // properties -> AWSTemplateFormatVersion + guard let awstefVersion = schema.properties?["AWSTemplateFormatVersion"] else { + XCTFail("Missing AWSTemplateFormatVersion property in schema") + return + } + + XCTAssertEqual(awstefVersion.jsonType().type?.first, .string) + XCTAssertEqual(awstefVersion.jsonType().enumeration?.first, "2010-09-09") + + // properties -> Conditions + guard let conditionsProperty = schema.properties?["Conditions"] else { + XCTFail("Missing Conditions property in schema") + return + } + + XCTAssertEqual(conditionsProperty.jsonType().type?.first, .object) + XCTAssertNotNil(conditionsProperty.jsonType().object()?.patternProperties) + + // properties -> Description + guard let descriptionProperty = schema.properties?["Description"] else { + XCTFail("Missing Description property in schema") + return + } + XCTAssertEqual(descriptionProperty.jsonType().type?.first, .string) + XCTAssertEqual(descriptionProperty.jsonType().description, "Template description") + XCTAssertEqual(descriptionProperty.jsonType().stringSchema()?.maxLength, 1024) + + // properties -> Resources + guard let resourcesProperty = schema.properties?["Resources"] else { + XCTFail("Missing Resources property in schema") + return + } + XCTAssertEqual(resourcesProperty.jsonType().type?.first, .object) + XCTAssertNotNil(resourcesProperty.jsonType().object()?.patternProperties) + + if let patternProperties = schema.properties?["patternProperties"] { + switch patternProperties { + case .anyOf(let jsonTypes): + + if let firstType = jsonTypes.first, let secondType = jsonTypes.last { + switch firstType.subSchema { + case .string(let stringSchema): + XCTAssertEqual(stringSchema.pattern, "^[a-zA-Z0-9]+$") + XCTAssertEqual(firstType.type?.first, .string) + default: + XCTFail("Expected a StringSchema") + } + + switch secondType.subSchema { + case .array(let arraySchema): + XCTAssertEqual(arraySchema.items?.type?.first, .string) + XCTAssertEqual(secondType.type?.first, .array) + default: + XCTFail("Expected an ArraySchema") + } + } + default: + XCTFail("Expected an anyOf type") + } + } + + // properties -> Parameters + guard let parametersProperty = schema.properties?["Parameters"] else { + XCTFail("Missing Parameters property in schema") + return + } + XCTAssertEqual(parametersProperty.jsonType().type?.first, .object) + XCTAssertEqual(parametersProperty.jsonType().object()?.maxProperties, 50) + + let paramPatternProperties = parametersProperty.jsonType().object()?.patternProperties + XCTAssertNotNil(paramPatternProperties) + XCTAssertEqual(paramPatternProperties?.keys.first, "^[a-zA-Z0-9]+$") + guard let paramSchema = paramPatternProperties?.values.first as? JSONUnionType else { + XCTFail("Expected a JSONUnionType in Parameters patternProperties") + return + } + XCTAssertEqual(paramSchema.jsonType().ref, "#/definitions/Parameter") + + // properties -> Mappings + guard let mappingsProperty = schema.properties?["Mappings"] else { + XCTFail("Missing Mappings property in schema") + return + } + XCTAssertEqual(mappingsProperty.jsonType().type?.first, .object) + XCTAssertEqual(mappingsProperty.jsonType().object()?.patternProperties?.keys.first, "^[a-zA-Z0-9]+$") - // TODO : validate a couple of assertions here (not all) + // properties -> Metadata + guard let metadataProperty = schema.properties?["Metadata"] else { + XCTFail("Missing Metadata property in schema") + return + } + XCTAssertEqual(metadataProperty.jsonType().type?.first, .object) + + // properties -> Outputs + guard let outputsProperty = schema.properties?["Outputs"] else { + XCTFail("Missing Outputs property in schema") + return + } + XCTAssertEqual(outputsProperty.jsonType().type?.first, .object) + XCTAssertEqual(outputsProperty.jsonType().object()?.maxProperties, 60) + XCTAssertEqual(outputsProperty.jsonType().object()?.minProperties, 1) + XCTAssertEqual(outputsProperty.jsonType().object()?.patternProperties?.keys.first, "^[a-zA-Z0-9]+$") + + // properties -> Transform + guard let transformProperty = schema.properties?["Transform"] else { + XCTFail("Missing Transform property in schema") + return + } + XCTAssertEqual(transformProperty.jsonType().type?.first, .string) + XCTAssertEqual(transformProperty.jsonType().enumeration?.first, "AWS::Serverless-2016-10-31") + + // properties -> Globals + guard let globalsProperty = schema.properties?["Globals"] else { + XCTFail("Missing Globals property in schema") + return + } + XCTAssertEqual(globalsProperty.jsonType().type?.first, .object) + + // properties -> Properties + guard let propertiesProperty = schema.properties?["Properties"] else { + XCTFail("Missing Properties property in schema") + return + } + XCTAssertEqual(propertiesProperty.jsonType().type?.first, .object) + + // definitions + XCTAssertNotNil(schema.definitions) + + let patternProperties = ["Conditions", "Mappings", "Outputs", "Parameters", "Resources"] + for prop in patternProperties { + XCTAssertNotNil(schema.properties?[prop]) + if let property = schema.properties?[prop] { + XCTAssertTrue(property.jsonType().additionalProperties == false) + } + } + } + + func testJSONTypePatternProperties() throws { + try self._testJSONExtract(json: """ + { + "additionalProperties": false, + "patternProperties": { + "^[a-zA-Z0-9]+$": { + "type": "object" + } + }, + "type": "object" + } + """, decodeTo: JSONType.self) + } + + func testJSONTypeMinMaxProperties() throws { + try self._testJSONExtract(json: """ + { + "additionalProperties": false, + "maxProperties": 50, + "patternProperties": { + "^[a-zA-Z0-9]+$": { + "$ref": "#/definitions/Parameter" + } + }, + "type": "object" + } + """, decodeTo: JSONType.self) + } + + func testJSONTypeResources() throws { + try self._testJSONExtract(json: """ + { + "additionalProperties": false, + "patternProperties": { + "^[a-zA-Z0-9]+$": { + "anyOf": [ + { + "$ref": "#/definitions/AWS::Serverless::Api" + }, + { + "$ref": "#/definitions/AWS::Serverless::Function" + }, + { + "$ref": "#/definitions/AWS::Serverless::SimpleTable" + }, + { + "$ref": "#/definitions/CloudFormationResource" + } + ] + } + }, + "type": "object" + } + """, decodeTo: JSONType.self) + } + + func testJSONTypeAnyOf() throws { + try self._testJSONExtract(json: """ + { + "additionalProperties": false, + "properties": { + "Method": { + "type": "string" + }, + "Path": { + "type": "string" + }, + "RestApiId": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "object" + } + ] + } + }, + "required": [ + "Method", + "Path" + ], + "type": "object" + } + """, decodeTo: JSONType.self) + } + + func testJSONUnionType() throws { + try self._testJSONExtract(json: """ + { + "additionalProperties": false, + "properties": { + "UserPool": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "object" + } + ] + }, + "Trigger": { + "anyOf": [ + { + "type": "string" + }, + { + "items": { + "type": "string" + }, + "type": "array" + } + ] + } + }, + "required": [ + "UserPool", + "Trigger" + ], + "type": "object" + + } + """, decodeTo: JSONType.self) } + func testJSONTypeProperties() throws { + try self._testJSONExtract(json: """ + { + "additionalProperties": false, + "properties": { + "Properties": { + "anyOf": [ + { + "$ref": "#/definitions/AWS::Serverless::Function.S3Event" + }, + { + "$ref": "#/definitions/AWS::Serverless::Function.SNSEvent" + }, + { + "$ref": "#/definitions/AWS::Serverless::Function.KinesisEvent" + }, + { + "$ref": "#/definitions/AWS::Serverless::Function.MSKEvent" + }, + { + "$ref": "#/definitions/AWS::Serverless::Function.MQEvent" + }, + { + "$ref": "#/definitions/AWS::Serverless::Function.SQSEvent" + }, + { + "$ref": "#/definitions/AWS::Serverless::Function.DynamoDBEvent" + }, + { + "$ref": "#/definitions/AWS::Serverless::Function.ApiEvent" + }, + { + "$ref": "#/definitions/AWS::Serverless::Function.ScheduleEvent" + }, + { + "$ref": "#/definitions/AWS::Serverless::Function.CloudWatchEventEvent" + }, + { + "$ref": "#/definitions/AWS::Serverless::Function.EventBridgeRule" + }, + { + "$ref": "#/definitions/AWS::Serverless::Function.LogEvent" + }, + { + "$ref": "#/definitions/AWS::Serverless::Function.IoTRuleEvent" + }, + { + "$ref": "#/definitions/AWS::Serverless::Function.AlexaSkillEvent" + }, + { + "$ref": "#/definitions/AWS::Serverless::Function.CognitoEvent" + } + ] + }, + "Type": { + "type": "string" + } + }, + "required": [ + "Properties", + "Type" + ], + "type": "object" + + } + """, decodeTo: JSONType.self) + } + + func testJSONTypeParameter() throws { + try self._testJSONExtract(json: """ + { + "Parameter": { + "additionalProperties": false, + "properties": { + "AllowedPattern": { + "type": "string" + }, + "AllowedValues": { + "type": "array" + }, + "ConstraintDescription": { + "type": "string" + }, + "Default": { + "type": "string" + }, + "Description": { + "type": "string" + }, + "MaxLength": { + "type": "string" + }, + "MaxValue": { + "type": "string" + }, + "MinLength": { + "type": "string" + }, + "MinValue": { + "type": "string" + }, + "NoEcho": { + "type": [ + "string", + "boolean" + ] + }, + "Type": { + "enum": [ + "String", + "Number", + "List", + "CommaDelimitedList", + "AWS::EC2::AvailabilityZone::Name", + "AWS::EC2::Image::Id", + "AWS::EC2::Instance::Id", + "AWS::EC2::KeyPair::KeyName", + "AWS::EC2::SecurityGroup::GroupName", + "AWS::EC2::SecurityGroup::Id", + "AWS::EC2::Subnet::Id", + "AWS::EC2::Volume::Id", + "AWS::EC2::VPC::Id", + "AWS::Route53::HostedZone::Id", + "List", + "List", + "List", + "List", + "List", + "List", + "List", + "List", + "List", + "List" + ], + "type": "string" + } + }, + "required": [ + "Type" + ], + "type": "object" + } + + } + """, decodeTo: JSONType.self) + } + + func testJSONTypeDependsOn() throws { + try self._testJSONExtract(json: """ + { + "DependsOn": { + "anyOf": [ + { + "pattern": "^[a-zA-Z0-9]+$", + "type": "string" + }, + { + "items": { + "pattern": "^[a-zA-Z0-9]+$", + "type": "string" + }, + "type": "array" + } + ] + } + } + """, decodeTo: JSONType.self) + } + + func testBasicJSONType() throws { + try self._testJSONExtract(json: """ + { + "properties": { + "InlineCode": { + "type": "string" + } + } + } + """, decodeTo: JSONType.self) + } + + func testAllOfJSONType() throws { + try self._testJSONExtract(json: """ + { + "Properties": { + "allOf": [ + { + "anyOf": [ + { + "properties": { + "InlineCode": { + "type": "string" + } + } + }, + { + "properties": { + "CodeUri": { + "anyOf": [ + { + "type": [ + "string" + ] + }, + { + "$ref": "#/definitions/AWS::Serverless::Function.S3Location" + } + ] + } + } + } + ] + } + ] + } + } + """, decodeTo: JSONType.self) + } + + func testJSONTypeServerlessFunction() throws { + try self._testJSONExtract(json: """ + { + "additionalProperties": false, + "properties": { + "Properties": { + "allOf": [ + { + "anyOf": [ + { + "properties": { + "InlineCode": { + "type": "string" + } + } + }, + { + "properties": { + "CodeUri": { + "anyOf": [ + { + "type": [ + "string" + ] + }, + { + "$ref": "#/definitions/AWS::Serverless::Function.S3Location" + } + ] + } + } + } + ] + } + ] + } + }, + "required": [ + "Type", + "Properties" + ], + "type": "object" + } + """, decodeTo: JSONType.self) + } + + func _testJSONExtract(json: String, decodeTo type: Decodable.Type) throws { + let decoder = JSONDecoder() + let data = try XCTUnwrap(json.data(using: .utf8)) + XCTAssertNoThrow(try decoder.decode(type, from: data)) + } } diff --git a/Tests/AWSLambdaDeploymentDescriptorGeneratorTests/Resources/SAMJSONSchema.json b/Tests/AWSLambdaDeploymentDescriptorGeneratorTests/Resources/SAMJSONSchema.json index ac671b9..bc349a4 100644 --- a/Tests/AWSLambdaDeploymentDescriptorGeneratorTests/Resources/SAMJSONSchema.json +++ b/Tests/AWSLambdaDeploymentDescriptorGeneratorTests/Resources/SAMJSONSchema.json @@ -473,7 +473,7 @@ "required": [ "UserPool", "Trigger" - ], + ], "type": "object" }, "AWS::Serverless::Function.DynamoDBEvent": { @@ -671,7 +671,6 @@ ], "type": "object" }, - "AWS::Serverless::Function.SQSEvent": { "additionalProperties": false, "properties": { @@ -802,15 +801,15 @@ "Schedule": { "type": "string" }, - "Name": { - "type": "string" - }, - "Description": { - "type": "string" - }, - "Enabled": { - "type": "boolean" - } + "Name": { + "type": "string" + }, + "Description": { + "type": "string" + }, + "Enabled": { + "type": "boolean" + } }, "required": [ "Schedule" diff --git a/Tests/AWSLambdaDeploymentDescriptorGeneratorTests/Resources/SimpleJSONSchema.json b/Tests/AWSLambdaDeploymentDescriptorGeneratorTests/Resources/SimpleJSONSchema.json index 3c20364..aecfa04 100644 --- a/Tests/AWSLambdaDeploymentDescriptorGeneratorTests/Resources/SimpleJSONSchema.json +++ b/Tests/AWSLambdaDeploymentDescriptorGeneratorTests/Resources/SimpleJSONSchema.json @@ -29,20 +29,6 @@ "description": "Do I like this vegetable?" } } - }, - "DependsOn": { - "anyOf": [ - { - "pattern": "^[a-zA-Z0-9]+$", - "type": "string" - }, - { - "items": { - "type": "string" - }, - "type": "array" - } - ] - }, + } } -} \ No newline at end of file +} diff --git a/Tests/AWSLambdaDeploymentDescriptorGeneratorTests/Resources/TypeSchemaTranslator.json b/Tests/AWSLambdaDeploymentDescriptorGeneratorTests/Resources/TypeSchemaTranslator.json new file mode 100644 index 0000000..481544c --- /dev/null +++ b/Tests/AWSLambdaDeploymentDescriptorGeneratorTests/Resources/TypeSchemaTranslator.json @@ -0,0 +1,15 @@ +{ + "typeName": "testTypeName", + "properties": [ + { + "name": "propertyName1", + "type": "propertyType1" + }, + { + "name": "propertyName2", + "type": "propertyType2" + } + ], + "subTypes": [] +} + diff --git a/Tests/AWSLambdaDeploymentDescriptorTests/DeploymentDescriptorBase.swift b/Tests/AWSLambdaDeploymentDescriptorTests/DeploymentDescriptorBase.swift index 24c41c0..77a1113 100644 --- a/Tests/AWSLambdaDeploymentDescriptorTests/DeploymentDescriptorBase.swift +++ b/Tests/AWSLambdaDeploymentDescriptorTests/DeploymentDescriptorBase.swift @@ -16,13 +16,12 @@ import XCTest class DeploymentDescriptorBaseTest: XCTestCase { - var codeURI: String! = nil let fileManager = FileManager.default let functionName = MockDeploymentDescriptorBuilder.functionName override func setUpWithError() throws { - // create a fake lambda package zip file + // create a fake lambda package zip file let (_, tempFile) = try self.prepareTemporaryPackageFile() self.codeURI = tempFile } @@ -32,7 +31,7 @@ class DeploymentDescriptorBaseTest: XCTestCase { self.deleteTemporaryPackageFile(self.codeURI) self.codeURI = nil } - + @discardableResult func prepareTemporaryPackageFile() throws -> (String, String) { let fm = FileManager.default @@ -65,7 +64,7 @@ class DeploymentDescriptorBaseTest: XCTestCase { var value: [String] = [] switch self { case .keyOnly(let i, let k): - value = [String(repeating: " ", count: indent * i) + "\(k):"] + value = [String(repeating: " ", count: indent * i) + "\(k):"] case .keyValue(let i, let kv): value = kv.keys.map { String(repeating: " ", count: indent * i) + "\($0): \(kv[$0] ?? "")" } case .arrayKey(let i, let k): @@ -79,8 +78,7 @@ class DeploymentDescriptorBaseTest: XCTestCase { } private func testDeploymentDescriptor(deployment: String, - expected: [Expected]) -> Bool { - + expected: [Expected]) -> Bool { // given let samYAML = deployment @@ -88,47 +86,47 @@ class DeploymentDescriptorBaseTest: XCTestCase { let result = expected.allSatisfy { // each string in the expected [] is present in the YAML var result = true - $0.string().forEach { - result = result && samYAML.contains( $0 ) + for item in $0.string() { + result = result && samYAML.contains(item) } return result } - if !result { + if !result { print("===========") print(samYAML) print("-----------") - print(expected.compactMap { $0.string().joined(separator: "\n") } .joined(separator: "\n")) + print(expected.compactMap { $0.string().joined(separator: "\n") }.joined(separator: "\n")) print("===========") - } + } return result } func generateAndTestDeploymentDescriptor(deployment: T, - expected: [Expected]) -> Bool { + expected: [Expected]) -> Bool { // when let samYAML = deployment.toYAML() - return testDeploymentDescriptor(deployment: samYAML, expected: expected) + return self.testDeploymentDescriptor(deployment: samYAML, expected: expected) } func generateAndTestDeploymentDescriptor(deployment: T, - expected: Expected) -> Bool { - return generateAndTestDeploymentDescriptor(deployment: deployment, expected: [expected]) + expected: Expected) -> Bool { + self.generateAndTestDeploymentDescriptor(deployment: deployment, expected: [expected]) } func expectedSAMHeaders() -> [Expected] { - return [Expected.keyValue(indent: 0, - keyValue: [ - "Description": "A SAM template to deploy a Swift Lambda function", - "AWSTemplateFormatVersion": "2010-09-09", - "Transform": "AWS::Serverless-2016-10-31"]) - ] + [Expected.keyValue(indent: 0, + keyValue: [ + "Description": "A SAM template to deploy a Swift Lambda function", + "AWSTemplateFormatVersion": "2010-09-09", + "Transform": "AWS::Serverless-2016-10-31", + ])] } func expectedFunction(architecture: String = "arm64") -> [Expected] { - return [ + [ Expected.keyOnly(indent: 0, key: "Resources"), Expected.keyOnly(indent: 1, key: "TestLambda"), Expected.keyValue(indent: 2, keyValue: ["Type": "AWS::Serverless::Function"]), @@ -136,42 +134,41 @@ class DeploymentDescriptorBaseTest: XCTestCase { Expected.keyValue(indent: 3, keyValue: [ "Handler": "Provided", "CodeUri": self.codeURI, - "Runtime": "provided.al2"]), + "Runtime": "provided.al2", + ]), Expected.keyOnly(indent: 3, key: "Architectures"), - Expected.arrayKey(indent: 4, key: architecture) - ] - + Expected.arrayKey(indent: 4, key: architecture), + ] } func expectedEnvironmentVariables() -> [Expected] { - return [ + [ Expected.keyOnly(indent: 3, key: "Environment"), Expected.keyOnly(indent: 4, key: "Variables"), - Expected.keyValue(indent: 5, keyValue: ["NAME1": "VALUE1"]) + Expected.keyValue(indent: 5, keyValue: ["NAME1": "VALUE1"]), ] } func expectedHttpAPi() -> [Expected] { - return [ + [ Expected.keyOnly(indent: 3, key: "Events"), Expected.keyOnly(indent: 4, key: "HttpApiEvent"), - Expected.keyValue(indent: 5, keyValue: ["Type": "HttpApi"]) - + Expected.keyValue(indent: 5, keyValue: ["Type": "HttpApi"]), ] } func expectedQueue() -> [Expected] { - return [ + [ Expected.keyOnly(indent: 0, key: "Resources"), Expected.keyOnly(indent: 1, key: "QueueTestQueue"), Expected.keyValue(indent: 2, keyValue: ["Type": "AWS::SQS::Queue"]), Expected.keyOnly(indent: 2, key: "Properties"), - Expected.keyValue(indent: 3, keyValue: ["QueueName": "test-queue"]) + Expected.keyValue(indent: 3, keyValue: ["QueueName": "test-queue"]), ] } func expectedQueueEventSource(source: String) -> [Expected] { - return [ + [ Expected.keyOnly(indent: 3, key: "Events"), Expected.keyOnly(indent: 4, key: "SQSEvent"), Expected.keyValue(indent: 5, keyValue: ["Type": "SQS"]), @@ -181,19 +178,19 @@ class DeploymentDescriptorBaseTest: XCTestCase { Expected.keyOnly(indent: 6, key: "Queue"), Expected.keyOnly(indent: 7, key: "Fn::GetAtt"), Expected.arrayKey(indent: 8, key: source), - Expected.arrayKey(indent: 8, key: "Arn") + Expected.arrayKey(indent: 8, key: "Arn"), ] } func expectedQueueEventSource(arn: String) -> [Expected] { - return [ + [ Expected.keyOnly(indent: 3, key: "Events"), Expected.keyOnly(indent: 4, key: "SQSEvent"), Expected.keyValue(indent: 5, keyValue: ["Type": "SQS"]), Expected.keyOnly(indent: 5, key: "Properties"), Expected.keyValue(indent: 6, keyValue: ["Enabled": "true", "BatchSize": "10", - "Queue": arn]) + "Queue": arn]), ] } } diff --git a/Tests/AWSLambdaDeploymentDescriptorTests/DeploymentDescriptorBuilderTests.swift b/Tests/AWSLambdaDeploymentDescriptorTests/DeploymentDescriptorBuilderTests.swift index c32d551..54e7bb8 100644 --- a/Tests/AWSLambdaDeploymentDescriptorTests/DeploymentDescriptorBuilderTests.swift +++ b/Tests/AWSLambdaDeploymentDescriptorTests/DeploymentDescriptorBuilderTests.swift @@ -12,24 +12,23 @@ // // ===----------------------------------------------------------------------===// -import XCTest @testable import AWSLambdaDeploymentDescriptor +import XCTest // This test case tests the logic built into the DSL, // i.e. the additional resources created automatically // and the check on existence of the ZIP file // the rest is boiler plate code final class DeploymentDescriptorBuilderTests: DeploymentDescriptorBaseTest { - - //MARK: ServerlessFunction resource + // MARK: ServerlessFunction resource + func testGenericFunction() { - // given let expected: [Expected] = expectedSAMHeaders() + - expectedFunction() + - expectedEnvironmentVariables() + - expectedHttpAPi() - + expectedFunction() + + expectedEnvironmentVariables() + + expectedHttpAPi() + let testDeployment = MockDeploymentDescriptorBuilder( withFunction: true, architecture: .arm64, @@ -37,148 +36,145 @@ final class DeploymentDescriptorBuilderTests: DeploymentDescriptorBaseTest { eventSource: HttpApi().resource(), environmentVariable: ["NAME1": "VALUE1"] ) - + XCTAssertTrue(self.generateAndTestDeploymentDescriptor(deployment: testDeployment, expected: expected)) - } - + func testQueueResource() { - // given let expected = expectedQueue() - + let queue = Queue(logicalName: "QueueTestQueue", physicalName: "test-queue") - + let testDeployment = MockDeploymentDescriptorBuilder(withResource: queue) - + XCTAssertTrue(self.generateAndTestDeploymentDescriptor(deployment: testDeployment, expected: expected)) } // check wether the builder creates additional queue resources func testLambdaCreateAdditionalResourceWithName() { - // given let expected = expectedQueue() - + let sqsEventSource = Sqs("test-queue").resource() - + let testDeployment = MockDeploymentDescriptorBuilder( withFunction: true, architecture: .arm64, codeURI: self.codeURI, eventSource: sqsEventSource, - environmentVariable: [:]) - + environmentVariable: [:] + ) + XCTAssertTrue(self.generateAndTestDeploymentDescriptor(deployment: testDeployment, expected: expected)) } - + // check wether the builder creates additional queue resources func testLambdaCreateAdditionalResourceWithQueue() { - // given let expected = expectedQueue() - + let sqsEventSource = Sqs(Queue(logicalName: "QueueTestQueue", physicalName: "test-queue")).resource() - + let testDeployment = MockDeploymentDescriptorBuilder( withFunction: true, architecture: .arm64, codeURI: self.codeURI, eventSource: sqsEventSource, - environmentVariable: [:] ) - + environmentVariable: [:] + ) + XCTAssertTrue(self.generateAndTestDeploymentDescriptor(deployment: testDeployment, expected: expected)) } - + // check wether the builder detects missing ZIP package func testLambdaMissingZIPPackage() { - // when let name = "TestFunction" let codeUri = "/path/does/not/exist/lambda.zip" - + // then XCTAssertThrowsError(try Function.packagePath(name: name, codeUri: codeUri)) } - + // check wether the builder detects existing packages func testLambdaExistingZIPPackage() throws { - // given XCTAssertNoThrow(try prepareTemporaryPackageFile()) - let (tempDir, tempFile) = try prepareTemporaryPackageFile() + let (tempDir, tempFile) = try prepareTemporaryPackageFile() let expected = Expected.keyValue(indent: 3, keyValue: ["CodeUri": tempFile]) - + CommandLine.arguments = ["test", "--archive-path", tempDir] - + let testDeployment = MockDeploymentDescriptorBuilder( withFunction: true, architecture: .arm64, codeURI: self.codeURI, eventSource: HttpApi().resource(), - environmentVariable: ["NAME1": "VALUE1"] ) - + environmentVariable: ["NAME1": "VALUE1"] + ) + XCTAssertTrue(self.generateAndTestDeploymentDescriptor(deployment: testDeployment, expected: expected)) - + // cleanup XCTAssertNoThrow(deleteTemporaryPackageFile(tempFile)) } - + func testFunctionDescription() { // given let description = "My function description" let expected = [Expected.keyValue(indent: 3, keyValue: ["Description": description])] - + // when let function = Function(name: functionName, codeURI: self.codeURI) { description } - + // then let testDeployment = MockDeploymentDescriptorBuilder(withResource: function) XCTAssertTrue(self.generateAndTestDeploymentDescriptor(deployment: testDeployment, expected: expected)) } - + func testFunctionAliasModifier() { // given let aliasName = "MyAlias" let sha256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" let expected = [Expected.keyValue(indent: 3, keyValue: ["AutoPublishAliasAllProperties": "true", "AutoPublishAlias": aliasName, - "AutoPublishCodeSha256" : sha256])] - + "AutoPublishCodeSha256": sha256])] + // when let function = Function(name: functionName, codeURI: self.codeURI) .autoPublishAlias(aliasName, all: true) - + // then let testDeployment = MockDeploymentDescriptorBuilder(withResource: function) XCTAssertTrue(self.generateAndTestDeploymentDescriptor(deployment: testDeployment, expected: expected)) } - + func testFunctionEphemeralStorageModifier() { // given let size = 1024 let expected = [ Expected.keyOnly(indent: 3, key: "EphemeralStorage"), - Expected.keyValue(indent: 4, keyValue: ["Size": "\(size)"]) + Expected.keyValue(indent: 4, keyValue: ["Size": "\(size)"]), ] - + // when let function = Function(name: functionName, codeURI: self.codeURI) .ephemeralStorage(size) - + // then let testDeployment = MockDeploymentDescriptorBuilder(withResource: function) XCTAssertTrue(self.generateAndTestDeploymentDescriptor(deployment: testDeployment, expected: expected)) } - + func testeventInvokeConfigWithArn() { // given let validArn1 = "arn:aws:sqs:eu-central-1:012345678901:lambda-test" @@ -193,21 +189,21 @@ final class DeploymentDescriptorBuilderTests: DeploymentDescriptorBaseTest { "Destination": validArn1]), Expected.keyOnly(indent: 4, key: "OnFailure"), Expected.keyValue(indent: 5, keyValue: ["Type": "Lambda", - "Destination": validArn2]) + "Destination": validArn2]), ] - + // when let function = Function(name: functionName, codeURI: self.codeURI) .eventInvoke(onSuccess: validArn1, onFailure: validArn2, maximumEventAgeInSeconds: 900, maximumRetryAttempts: 3) - + // then let testDeployment = MockDeploymentDescriptorBuilder(withResource: function) XCTAssertTrue(self.generateAndTestDeploymentDescriptor(deployment: testDeployment, expected: expected)) } - + func testeventInvokeConfigWithSuccessQueue() { // given let queue1 = Queue(logicalName: "queue1", physicalName: "queue1") @@ -221,22 +217,21 @@ final class DeploymentDescriptorBuilderTests: DeploymentDescriptorBaseTest { Expected.keyOnly(indent: 6, key: "Destination"), Expected.keyOnly(indent: 7, key: "Fn::GetAtt"), Expected.arrayKey(indent: 8, key: "queue1"), - Expected.arrayKey(indent: 8, key: "Arn") + Expected.arrayKey(indent: 8, key: "Arn"), ] - + // when let function = Function(name: functionName, codeURI: self.codeURI) .eventInvoke(onSuccess: queue1.resource()[0], onFailure: nil, maximumEventAgeInSeconds: 900, maximumRetryAttempts: 3) - + // then let testDeployment = MockDeploymentDescriptorBuilder(withResource: function) XCTAssertTrue(self.generateAndTestDeploymentDescriptor(deployment: testDeployment, expected: expected)) - } - + func testeventInvokeConfigWithFailureQueue() { // given let queue1 = Queue(logicalName: "queue1", physicalName: "queue1") @@ -250,22 +245,21 @@ final class DeploymentDescriptorBuilderTests: DeploymentDescriptorBaseTest { Expected.keyOnly(indent: 6, key: "Destination"), Expected.keyOnly(indent: 7, key: "Fn::GetAtt"), Expected.arrayKey(indent: 8, key: "queue1"), - Expected.arrayKey(indent: 8, key: "Arn") + Expected.arrayKey(indent: 8, key: "Arn"), ] - + // when let function = Function(name: functionName, codeURI: self.codeURI) .eventInvoke(onSuccess: nil, onFailure: queue1.resource()[0], maximumEventAgeInSeconds: 900, maximumRetryAttempts: 3) - + // then let testDeployment = MockDeploymentDescriptorBuilder(withResource: function) XCTAssertTrue(self.generateAndTestDeploymentDescriptor(deployment: testDeployment, expected: expected)) - } - + func testeventInvokeConfigWithSuccessLambda() { // given let expected = [ @@ -278,9 +272,9 @@ final class DeploymentDescriptorBuilderTests: DeploymentDescriptorBaseTest { Expected.keyOnly(indent: 6, key: "Destination"), Expected.keyOnly(indent: 7, key: "Fn::GetAtt"), Expected.arrayKey(indent: 8, key: functionName), - Expected.arrayKey(indent: 8, key: "Arn") + Expected.arrayKey(indent: 8, key: "Arn"), ] - + // when var function = Function(name: functionName, codeURI: self.codeURI) let resource = function.resource() @@ -289,21 +283,21 @@ final class DeploymentDescriptorBuilderTests: DeploymentDescriptorBaseTest { onFailure: nil, maximumEventAgeInSeconds: 900, maximumRetryAttempts: 3) - + // then let testDeployment = MockDeploymentDescriptorBuilder(withResource: function) XCTAssertTrue(self.generateAndTestDeploymentDescriptor(deployment: testDeployment, expected: expected)) } - + func testURLConfigCors() { // given let expected = [ Expected.keyOnly(indent: 3, key: "FunctionUrlConfig"), - Expected.keyValue(indent: 4, keyValue: ["AuthType" : "AWS_IAM"]), - Expected.keyValue(indent: 4, keyValue: ["InvokeMode" : "BUFFERED"]), + Expected.keyValue(indent: 4, keyValue: ["AuthType": "AWS_IAM"]), + Expected.keyValue(indent: 4, keyValue: ["InvokeMode": "BUFFERED"]), Expected.keyOnly(indent: 4, key: "Cors"), - Expected.keyValue(indent: 5, keyValue: ["MaxAge":"99", - "AllowCredentials" : "true"]), + Expected.keyValue(indent: 5, keyValue: ["MaxAge": "99", + "AllowCredentials": "true"]), Expected.keyOnly(indent: 5, key: "AllowHeaders"), Expected.arrayKey(indent: 6, key: "header1"), Expected.arrayKey(indent: 6, key: "header2"), @@ -317,7 +311,7 @@ final class DeploymentDescriptorBuilderTests: DeploymentDescriptorBaseTest { Expected.arrayKey(indent: 6, key: "header1"), Expected.arrayKey(indent: 6, key: "header2"), ] - + // when var function = Function(name: functionName, codeURI: self.codeURI) let resource = function.resource() @@ -343,34 +337,32 @@ final class DeploymentDescriptorBuilderTests: DeploymentDescriptorBaseTest { "header2" } } - + // then let testDeployment = MockDeploymentDescriptorBuilder(withResource: function) XCTAssertTrue(self.generateAndTestDeploymentDescriptor(deployment: testDeployment, expected: expected)) - } - + func testURLConfigNoCors() { // given let expected = [ Expected.keyOnly(indent: 3, key: "FunctionUrlConfig"), - Expected.keyValue(indent: 4, keyValue: ["AuthType" : "AWS_IAM"]), - Expected.keyValue(indent: 4, keyValue: ["InvokeMode" : "BUFFERED"]), + Expected.keyValue(indent: 4, keyValue: ["AuthType": "AWS_IAM"]), + Expected.keyValue(indent: 4, keyValue: ["InvokeMode": "BUFFERED"]), ] - + // when var function = Function(name: functionName, codeURI: self.codeURI) let resource = function.resource() XCTAssertTrue(resource.count == 1) function = function.urlConfig(authType: .iam, invokeMode: .buffered) - + // then let testDeployment = MockDeploymentDescriptorBuilder(withResource: function) XCTAssertTrue(self.generateAndTestDeploymentDescriptor(deployment: testDeployment, expected: expected)) - } - + func testFileSystemConfig() { // given let validArn1 = "arn:aws:elasticfilesystem:eu-central-1:012345678901:access-point/fsap-abcdef01234567890" @@ -380,23 +372,24 @@ final class DeploymentDescriptorBuilderTests: DeploymentDescriptorBaseTest { let expected = [ Expected.keyOnly(indent: 3, key: "FileSystemConfigs"), Expected.arrayKey(indent: 4, key: ""), - Expected.keyValue(indent: 5, keyValue: ["Arn":validArn1, - "LocalMountPath" : mount1]), - Expected.keyValue(indent: 5, keyValue: ["Arn":validArn2, - "LocalMountPath" : mount2]) + Expected.keyValue(indent: 5, keyValue: ["Arn": validArn1, + "LocalMountPath": mount1]), + Expected.keyValue(indent: 5, keyValue: ["Arn": validArn2, + "LocalMountPath": mount2]), ] - + // when let function = Function(name: functionName, codeURI: self.codeURI) .fileSystem(validArn1, mountPoint: mount1) .fileSystem(validArn2, mountPoint: mount2) - + // then let testDeployment = MockDeploymentDescriptorBuilder(withResource: function) XCTAssertTrue(self.generateAndTestDeploymentDescriptor(deployment: testDeployment, expected: expected)) } - - //MARK: SimpleTable resource + + // MARK: SimpleTable resource + func testSimpleTable() { // given let expected = [ @@ -404,20 +397,20 @@ final class DeploymentDescriptorBuilderTests: DeploymentDescriptorBaseTest { Expected.keyValue(indent: 2, keyValue: ["Type": "AWS::Serverless::SimpleTable"]), Expected.keyValue(indent: 3, keyValue: ["TableName": "swift-lambda-table"]), Expected.keyOnly(indent: 3, key: "PrimaryKey"), - Expected.keyValue(indent: 4, keyValue: ["Type": "String", "Name" : "id"]), + Expected.keyValue(indent: 4, keyValue: ["Type": "String", "Name": "id"]), ] - + // when let table = Table(logicalName: "SwiftLambdaTable", physicalName: "swift-lambda-table", primaryKeyName: "id", primaryKeyType: "String") - + // then let testDeployment = MockDeploymentDescriptorBuilder(withResource: table) XCTAssertTrue(self.generateAndTestDeploymentDescriptor(deployment: testDeployment, expected: expected)) } - + func testSimpleTableCapacityThroughput() { // given let writeCapacity = 999 @@ -425,19 +418,18 @@ final class DeploymentDescriptorBuilderTests: DeploymentDescriptorBaseTest { let expected = [ Expected.keyOnly(indent: 3, key: "ProvisionedThroughput"), Expected.keyValue(indent: 4, keyValue: ["ReadCapacityUnits": "\(readCapacity)"]), - Expected.keyValue(indent: 4, keyValue: ["WriteCapacityUnits": "\(writeCapacity)"]) + Expected.keyValue(indent: 4, keyValue: ["WriteCapacityUnits": "\(writeCapacity)"]), ] - + // when let table = Table(logicalName: "SwiftLambdaTable", physicalName: "swift-lambda-table", primaryKeyName: "id", primaryKeyType: "String") .provisionedThroughput(readCapacityUnits: readCapacity, writeCapacityUnits: writeCapacity) - + // then let testDeployment = MockDeploymentDescriptorBuilder(withResource: table) XCTAssertTrue(self.generateAndTestDeploymentDescriptor(deployment: testDeployment, expected: expected)) } - } diff --git a/Tests/AWSLambdaDeploymentDescriptorTests/DeploymentDescriptorTests.swift b/Tests/AWSLambdaDeploymentDescriptorTests/DeploymentDescriptorTests.swift index 2b6a58f..1975b01 100644 --- a/Tests/AWSLambdaDeploymentDescriptorTests/DeploymentDescriptorTests.swift +++ b/Tests/AWSLambdaDeploymentDescriptorTests/DeploymentDescriptorTests.swift @@ -17,9 +17,7 @@ import XCTest // this test case tests the generation of the SAM deployment descriptor in JSON final class DeploymentDescriptorTests: DeploymentDescriptorBaseTest { - func testSAMHeader() { - // given let expected = expectedSAMHeaders() @@ -29,7 +27,6 @@ final class DeploymentDescriptorTests: DeploymentDescriptorBaseTest { } func testLambdaFunctionResource() { - // given let expected = [expectedFunction(), expectedSAMHeaders()].flatMap { $0 } @@ -39,36 +36,32 @@ final class DeploymentDescriptorTests: DeploymentDescriptorBaseTest { } func testLambdaFunctionWithSpecificArchitectures() { - // given let expected = [expectedFunction(architecture: ServerlessFunctionProperties.Architectures.x64.rawValue), expectedSAMHeaders()] - .flatMap { $0 } + .flatMap { $0 } // when let testDeployment = MockDeploymentDescriptor(withFunction: true, architecture: .x64, codeURI: self.codeURI) - // then + // then XCTAssertTrue(self.generateAndTestDeploymentDescriptor(deployment: testDeployment, expected: expected)) } func testAllFunctionProperties() { - // given let expected = [Expected.keyValue(indent: 3, keyValue: ["AutoPublishAliasAllProperties": "true", - "AutoPublishAlias" : "alias", - "AutoPublishCodeSha256" : "sha256", - "Description" : "my function description" - ] ), + "AutoPublishAlias": "alias", + "AutoPublishCodeSha256": "sha256", + "Description": "my function description"]), Expected.keyOnly(indent: 3, key: "EphemeralStorage"), - Expected.keyValue(indent: 4, keyValue: ["Size": "1024"]) - ] + Expected.keyValue(indent: 4, keyValue: ["Size": "1024"])] - // when + // when var functionProperties = ServerlessFunctionProperties(codeUri: self.codeURI, architecture: .arm64) functionProperties.autoPublishAliasAllProperties = true functionProperties.autoPublishAlias = "alias" @@ -81,24 +74,24 @@ final class DeploymentDescriptorTests: DeploymentDescriptorBaseTest { // then let testDeployment = MockDeploymentDescriptor(withFunction: false, - codeURI: self.codeURI, - additionalResources: [ functionToTest ]) + codeURI: self.codeURI, + additionalResources: [functionToTest]) XCTAssertTrue(self.generateAndTestDeploymentDescriptor(deployment: testDeployment, expected: expected)) } - + func testEventInvokeConfig() { // given let expected = [ Expected.keyOnly(indent: 3, key: "EventInvokeConfig"), Expected.keyOnly(indent: 4, key: "DestinationConfig"), Expected.keyOnly(indent: 5, key: "OnSuccess"), - Expected.keyValue(indent: 6, keyValue: ["Type" : "SNS"]), + Expected.keyValue(indent: 6, keyValue: ["Type": "SNS"]), Expected.keyOnly(indent: 5, key: "OnFailure"), - Expected.keyValue(indent: 6, keyValue: ["Destination" : "arn:aws:sqs:eu-central-1:012345678901:lambda-test"]), - Expected.keyValue(indent: 6, keyValue: ["Type" : "Lambda"]) + Expected.keyValue(indent: 6, keyValue: ["Destination": "arn:aws:sqs:eu-central-1:012345678901:lambda-test"]), + Expected.keyValue(indent: 6, keyValue: ["Type": "Lambda"]), ] - + // when var functionProperties = ServerlessFunctionProperties(codeUri: self.codeURI, architecture: .arm64) let validArn = "arn:aws:sqs:eu-central-1:012345678901:lambda-test" @@ -109,12 +102,14 @@ final class DeploymentDescriptorTests: DeploymentDescriptorBaseTest { type: .lambda) let destinations = ServerlessFunctionProperties.EventInvokeConfiguration.EventInvokeDestinationConfiguration( onSuccess: destination1, - onFailure: destination2) - + onFailure: destination2 + ) + let invokeConfig = ServerlessFunctionProperties.EventInvokeConfiguration( destinationConfig: destinations, maximumEventAgeInSeconds: 999, - maximumRetryAttempts: 33) + maximumRetryAttempts: 33 + ) functionProperties.eventInvokeConfig = invokeConfig let functionToTest = Resource(type: .function, properties: functionProperties, @@ -122,11 +117,10 @@ final class DeploymentDescriptorTests: DeploymentDescriptorBaseTest { // then let testDeployment = MockDeploymentDescriptor(withFunction: false, - codeURI: self.codeURI, - additionalResources: [ functionToTest ]) + codeURI: self.codeURI, + additionalResources: [functionToTest]) XCTAssertTrue(self.generateAndTestDeploymentDescriptor(deployment: testDeployment, expected: expected)) - } func testFileSystemConfig() { @@ -137,15 +131,15 @@ final class DeploymentDescriptorTests: DeploymentDescriptorBaseTest { let expected = [ Expected.keyOnly(indent: 3, key: "FileSystemConfigs"), Expected.arrayKey(indent: 4, key: ""), - Expected.keyValue(indent: 5, keyValue: ["Arn":validArn, - "LocalMountPath" : mount1]), - Expected.keyValue(indent: 5, keyValue: ["Arn":validArn, - "LocalMountPath" : mount2]) + Expected.keyValue(indent: 5, keyValue: ["Arn": validArn, + "LocalMountPath": mount1]), + Expected.keyValue(indent: 5, keyValue: ["Arn": validArn, + "LocalMountPath": mount2]), ] - + // when var functionProperties = ServerlessFunctionProperties(codeUri: self.codeURI, architecture: .arm64) - + if let fileSystemConfig1 = ServerlessFunctionProperties.FileSystemConfig(arn: validArn, localMountPath: mount1), let fileSystemConfig2 = ServerlessFunctionProperties.FileSystemConfig(arn: validArn, localMountPath: mount2) { functionProperties.fileSystemConfigs = [fileSystemConfig1, fileSystemConfig2] @@ -159,12 +153,12 @@ final class DeploymentDescriptorTests: DeploymentDescriptorBaseTest { // then let testDeployment = MockDeploymentDescriptor(withFunction: false, - codeURI: self.codeURI, - additionalResources: [ functionToTest ]) + codeURI: self.codeURI, + additionalResources: [functionToTest]) XCTAssertTrue(self.generateAndTestDeploymentDescriptor(deployment: testDeployment, expected: expected)) } - + func testInvalidFileSystemConfig() { // given let validArn = "arn:aws:elasticfilesystem:eu-central-1:012345678901:access-point/fsap-abcdef01234567890" @@ -187,16 +181,16 @@ final class DeploymentDescriptorTests: DeploymentDescriptorBaseTest { XCTAssertNil(fileSystemConfig3) XCTAssertNotNil(fileSystemConfig4) } - + func testURLConfig() { // given let expected = [ Expected.keyOnly(indent: 3, key: "FunctionUrlConfig"), - Expected.keyValue(indent: 4, keyValue: ["AuthType" : "AWS_IAM"]), - Expected.keyValue(indent: 4, keyValue: ["InvokeMode" : "BUFFERED"]), + Expected.keyValue(indent: 4, keyValue: ["AuthType": "AWS_IAM"]), + Expected.keyValue(indent: 4, keyValue: ["InvokeMode": "BUFFERED"]), Expected.keyOnly(indent: 4, key: "Cors"), - Expected.keyValue(indent: 5, keyValue: ["MaxAge":"99", - "AllowCredentials" : "true"]), + Expected.keyValue(indent: 5, keyValue: ["MaxAge": "99", + "AllowCredentials": "true"]), Expected.keyOnly(indent: 5, key: "AllowHeaders"), Expected.arrayKey(indent: 6, key: "allowHeaders"), Expected.keyOnly(indent: 5, key: "AllowMethods"), @@ -204,12 +198,12 @@ final class DeploymentDescriptorTests: DeploymentDescriptorBaseTest { Expected.keyOnly(indent: 5, key: "AllowOrigins"), Expected.arrayKey(indent: 6, key: "allowOrigin"), Expected.keyOnly(indent: 5, key: "ExposeHeaders"), - Expected.arrayKey(indent: 6, key: "exposeHeaders") + Expected.arrayKey(indent: 6, key: "exposeHeaders"), ] - + // when var functionProperties = ServerlessFunctionProperties(codeUri: self.codeURI, architecture: .arm64) - + let cors = ServerlessFunctionProperties.URLConfig.Cors(allowCredentials: true, allowHeaders: ["allowHeaders"], allowMethods: ["allowMethod"], @@ -227,14 +221,13 @@ final class DeploymentDescriptorTests: DeploymentDescriptorBaseTest { // then let testDeployment = MockDeploymentDescriptor(withFunction: false, - codeURI: self.codeURI, - additionalResources: [ functionToTest ]) + codeURI: self.codeURI, + additionalResources: [functionToTest]) XCTAssertTrue(self.generateAndTestDeploymentDescriptor(deployment: testDeployment, expected: expected)) } func testSimpleTableResource() { - // given let expected = [ Expected.keyOnly(indent: 0, key: "Resources"), @@ -244,8 +237,8 @@ final class DeploymentDescriptorTests: DeploymentDescriptorBaseTest { Expected.keyOnly(indent: 3, key: "PrimaryKey"), Expected.keyValue(indent: 3, keyValue: ["TableName": "TestTable"]), Expected.keyValue(indent: 4, keyValue: ["Name": "pk", - "Type": "String"]) - ] + "Type": "String"]), + ] let pk = SimpleTableProperties.PrimaryKey(name: "pk", type: "String") let props = SimpleTableProperties(primaryKey: pk, tableName: "TestTable") @@ -256,8 +249,7 @@ final class DeploymentDescriptorTests: DeploymentDescriptorBaseTest { // when let testDeployment = MockDeploymentDescriptor(withFunction: false, codeURI: self.codeURI, - additionalResources: [ table ] - ) + additionalResources: [table]) // then XCTAssertTrue(self.generateAndTestDeploymentDescriptor(deployment: testDeployment, @@ -265,7 +257,6 @@ final class DeploymentDescriptorTests: DeploymentDescriptorBaseTest { } func testSQSQueueResource() { - // given let expected = expectedQueue() @@ -277,9 +268,7 @@ final class DeploymentDescriptorTests: DeploymentDescriptorBaseTest { // when let testDeployment = MockDeploymentDescriptor(withFunction: false, codeURI: self.codeURI, - additionalResources: [ queue ] - - ) + additionalResources: [queue]) // test XCTAssertTrue(self.generateAndTestDeploymentDescriptor(deployment: testDeployment, @@ -287,24 +276,24 @@ final class DeploymentDescriptorTests: DeploymentDescriptorBaseTest { } func testHttpApiEventSourceCatchAll() { - // given let expected = expectedSAMHeaders() + - expectedFunction(architecture: ServerlessFunctionProperties.Architectures.defaultArchitecture().rawValue) + - [ - Expected.keyOnly(indent: 4, key: "HttpApiEvent"), - Expected.keyValue(indent: 5, keyValue: ["Type": "HttpApi"]) - ] + expectedFunction(architecture: ServerlessFunctionProperties.Architectures.defaultArchitecture().rawValue) + + [ + Expected.keyOnly(indent: 4, key: "HttpApiEvent"), + Expected.keyValue(indent: 5, keyValue: ["Type": "HttpApi"]), + ] let httpApi = Resource( - type: .httpApi, - properties: nil, - name: "HttpApiEvent") + type: .httpApi, + properties: nil, + name: "HttpApiEvent" + ) // when let testDeployment = MockDeploymentDescriptor(withFunction: true, codeURI: self.codeURI, - eventSource: [ httpApi ] ) + eventSource: [httpApi]) // then XCTAssertTrue(self.generateAndTestDeploymentDescriptor(deployment: testDeployment, @@ -312,28 +301,28 @@ final class DeploymentDescriptorTests: DeploymentDescriptorBaseTest { } func testHttpApiEventSourceSpecific() { - // given let expected = expectedSAMHeaders() + - expectedFunction(architecture: ServerlessFunctionProperties.Architectures.defaultArchitecture().rawValue) + - [ - Expected.keyOnly(indent: 4, key: "HttpApiEvent"), - Expected.keyValue(indent: 5, keyValue: ["Type": "HttpApi"]), - Expected.keyOnly(indent: 5, key: "Properties"), - Expected.keyValue(indent: 6, keyValue: ["Path": "/test", - "Method": "GET"]) - ] + expectedFunction(architecture: ServerlessFunctionProperties.Architectures.defaultArchitecture().rawValue) + + [ + Expected.keyOnly(indent: 4, key: "HttpApiEvent"), + Expected.keyValue(indent: 5, keyValue: ["Type": "HttpApi"]), + Expected.keyOnly(indent: 5, key: "Properties"), + Expected.keyValue(indent: 6, keyValue: ["Path": "/test", + "Method": "GET"]), + ] let props = HttpApiProperties(method: .GET, path: "/test") let httpApi = Resource( - type: .httpApi, - properties: props, - name: "HttpApiEvent") + type: .httpApi, + properties: props, + name: "HttpApiEvent" + ) // when let testDeployment = MockDeploymentDescriptor(withFunction: true, codeURI: self.codeURI, - eventSource: [ httpApi ]) + eventSource: [httpApi]) // then XCTAssertTrue(self.generateAndTestDeploymentDescriptor(deployment: testDeployment, @@ -341,12 +330,11 @@ final class DeploymentDescriptorTests: DeploymentDescriptorBaseTest { } func testSQSEventSourceWithArn() { - let name = #"arn:aws:sqs:eu-central-1:012345678901:lambda-test"# // given let expected = expectedSAMHeaders() + - expectedFunction() + - expectedQueueEventSource(arn: name) + expectedFunction() + + expectedQueueEventSource(arn: name) let props = SQSEventProperties(byRef: name, batchSize: 10, @@ -358,7 +346,7 @@ final class DeploymentDescriptorTests: DeploymentDescriptorBaseTest { // when let testDeployment = MockDeploymentDescriptor(withFunction: true, codeURI: self.codeURI, - eventSource: [ queue ] ) + eventSource: [queue]) // then XCTAssertTrue(self.generateAndTestDeploymentDescriptor(deployment: testDeployment, @@ -366,11 +354,10 @@ final class DeploymentDescriptorTests: DeploymentDescriptorBaseTest { } func testSQSEventSourceWithoutArn() { - // given - let expected = expectedSAMHeaders() + - expectedFunction() + - expectedQueueEventSource(source: "QueueQueueLambdaTest") + let expected = expectedSAMHeaders() + + expectedFunction() + + expectedQueueEventSource(source: "QueueQueueLambdaTest") let props = SQSEventProperties(byRef: "queue-lambda-test", batchSize: 10, @@ -382,7 +369,7 @@ final class DeploymentDescriptorTests: DeploymentDescriptorBaseTest { // when let testDeployment = MockDeploymentDescriptor(withFunction: true, codeURI: self.codeURI, - eventSource: [ queue ] ) + eventSource: [queue]) // then XCTAssertTrue(self.generateAndTestDeploymentDescriptor(deployment: testDeployment, @@ -390,48 +377,44 @@ final class DeploymentDescriptorTests: DeploymentDescriptorBaseTest { } func testEnvironmentVariablesString() { - // given let expected = [ Expected.keyOnly(indent: 3, key: "Environment"), Expected.keyOnly(indent: 4, key: "Variables"), Expected.keyValue(indent: 5, keyValue: [ "TEST2_VAR": "TEST2_VALUE", - "TEST1_VAR": "TEST1_VALUE" - ]) + "TEST1_VAR": "TEST1_VALUE", + ]), ] let testDeployment = MockDeploymentDescriptor(withFunction: true, codeURI: self.codeURI, environmentVariable: SAMEnvironmentVariable(["TEST1_VAR": "TEST1_VALUE", - "TEST2_VAR": "TEST2_VALUE"]) ) + "TEST2_VAR": "TEST2_VALUE"])) XCTAssertTrue(self.generateAndTestDeploymentDescriptor(deployment: testDeployment, expected: expected)) - } func testEnvironmentVariablesArray() { - // given let expected = [ Expected.keyOnly(indent: 3, key: "Environment"), Expected.keyOnly(indent: 4, key: "Variables"), Expected.keyOnly(indent: 5, key: "TEST1_VAR"), - Expected.keyValue(indent: 6, keyValue: ["Ref": "TEST1_VALUE"]) + Expected.keyValue(indent: 6, keyValue: ["Ref": "TEST1_VALUE"]), ] var envVar = SAMEnvironmentVariable() envVar.append("TEST1_VAR", ["Ref": "TEST1_VALUE"]) let testDeployment = MockDeploymentDescriptor(withFunction: true, codeURI: self.codeURI, - environmentVariable: envVar ) + environmentVariable: envVar) XCTAssertTrue(self.generateAndTestDeploymentDescriptor(deployment: testDeployment, expected: expected)) } func testEnvironmentVariablesDictionary() { - // given let expected = [ Expected.keyOnly(indent: 3, key: "Environment"), @@ -439,26 +422,25 @@ final class DeploymentDescriptorTests: DeploymentDescriptorBaseTest { Expected.keyOnly(indent: 5, key: "TEST1_VAR"), Expected.keyOnly(indent: 6, key: "Fn::GetAtt"), Expected.arrayKey(indent: 7, key: "TEST1_VALUE"), - Expected.arrayKey(indent: 7, key: "Arn") + Expected.arrayKey(indent: 7, key: "Arn"), ] var envVar = SAMEnvironmentVariable() envVar.append("TEST1_VAR", ["Fn::GetAtt": ["TEST1_VALUE", "Arn"]]) let testDeployment = MockDeploymentDescriptor(withFunction: true, codeURI: self.codeURI, - environmentVariable: envVar ) + environmentVariable: envVar) XCTAssertTrue(self.generateAndTestDeploymentDescriptor(deployment: testDeployment, expected: expected)) } func testEnvironmentVariablesResource() { - // given let expected = [ Expected.keyOnly(indent: 3, key: "Environment"), Expected.keyOnly(indent: 4, key: "Variables"), Expected.keyOnly(indent: 5, key: "TEST1_VAR"), - Expected.keyValue(indent: 6, keyValue: ["Ref": "LogicalName"]) + Expected.keyValue(indent: 6, keyValue: ["Ref": "LogicalName"]), ] let props = SQSResourceProperties(queueName: "PhysicalName") @@ -467,7 +449,7 @@ final class DeploymentDescriptorTests: DeploymentDescriptorBaseTest { envVar.append("TEST1_VAR", resource) let testDeployment = MockDeploymentDescriptor(withFunction: true, codeURI: self.codeURI, - environmentVariable: envVar ) + environmentVariable: envVar) XCTAssertTrue(self.generateAndTestDeploymentDescriptor(deployment: testDeployment, expected: expected)) } @@ -534,5 +516,4 @@ final class DeploymentDescriptorTests: DeploymentDescriptorBaseTest { // then XCTAssertEqual("event-bridge", arn!.service()) } - } diff --git a/Tests/AWSLambdaDeploymentDescriptorTests/FileDigestTests.swift b/Tests/AWSLambdaDeploymentDescriptorTests/FileDigestTests.swift index dbb8cef..60b57dc 100644 --- a/Tests/AWSLambdaDeploymentDescriptorTests/FileDigestTests.swift +++ b/Tests/AWSLambdaDeploymentDescriptorTests/FileDigestTests.swift @@ -12,15 +12,12 @@ // // ===----------------------------------------------------------------------===// -import XCTest -import CryptoKit @testable import AWSLambdaDeploymentDescriptor +import CryptoKit +import XCTest final class FileDigestTests: XCTestCase { - - func testFileDigest() throws { - let expected = "4a5d82d7a7a76a1487fb12ae7f1c803208b6b5e1cfb9ae14afdc0916301e3415" let tempDir = FileManager.default.temporaryDirectory.path let tempFile = "\(tempDir)/temp.txt" @@ -29,12 +26,11 @@ final class FileDigestTests: XCTestCase { defer { try? FileManager.default.removeItem(atPath: tempFile) } - + if let result = FileDigest.hex(from: tempFile) { XCTAssertEqual(result, expected) } else { XCTFail("digest is nil") } } - } diff --git a/Tests/AWSLambdaDeploymentDescriptorTests/MockedDeploymentDescriptor.swift b/Tests/AWSLambdaDeploymentDescriptorTests/MockedDeploymentDescriptor.swift index 5ec56b4..970ccaa 100644 --- a/Tests/AWSLambdaDeploymentDescriptorTests/MockedDeploymentDescriptor.swift +++ b/Tests/AWSLambdaDeploymentDescriptorTests/MockedDeploymentDescriptor.swift @@ -12,9 +12,9 @@ // // ===----------------------------------------------------------------------===// +@testable import AWSLambdaDeploymentDescriptor import Foundation import XCTest -@testable import AWSLambdaDeploymentDescriptor protocol MockDeploymentDescriptorBehavior { func toJSON() -> String @@ -22,7 +22,6 @@ protocol MockDeploymentDescriptorBehavior { } struct MockDeploymentDescriptor: MockDeploymentDescriptorBehavior { - let deploymentDescriptor: SAMDeploymentDescriptor init(withFunction: Bool = true, @@ -32,21 +31,21 @@ struct MockDeploymentDescriptor: MockDeploymentDescriptorBehavior { environmentVariable: SAMEnvironmentVariable? = nil, additionalResources: [Resource] = []) { if withFunction { - let properties = ServerlessFunctionProperties( - codeUri: codeURI, - architecture: architecture, - eventSources: eventSource ?? [], - environment: environmentVariable ?? SAMEnvironmentVariable.none) + codeUri: codeURI, + architecture: architecture, + eventSources: eventSource ?? [], + environment: environmentVariable ?? SAMEnvironmentVariable.none + ) let serverlessFunction = Resource( - type: .function, - properties: properties, - name: "TestLambda") + type: .function, + properties: properties, + name: "TestLambda" + ) self.deploymentDescriptor = SAMDeploymentDescriptor( description: "A SAM template to deploy a Swift Lambda function", - resources: [ serverlessFunction ] + additionalResources - + resources: [serverlessFunction] + additionalResources ) } else { self.deploymentDescriptor = SAMDeploymentDescriptor( @@ -55,21 +54,21 @@ struct MockDeploymentDescriptor: MockDeploymentDescriptorBehavior { ) } } + func toJSON() -> String { - return self.deploymentDescriptor.toJSON(pretty: false) + self.deploymentDescriptor.toJSON(pretty: false) } + func toYAML() -> String { - return self.deploymentDescriptor.toYAML() + self.deploymentDescriptor.toYAML() } } struct MockDeploymentDescriptorBuilder: MockDeploymentDescriptorBehavior { - static let functionName = "TestLambda" let deploymentDescriptor: DeploymentDescriptor init(withResource resource: any BuilderResource) { - self.deploymentDescriptor = DeploymentDescriptor { "A SAM template to deploy a Swift Lambda function" resource @@ -82,7 +81,6 @@ struct MockDeploymentDescriptorBuilder: MockDeploymentDescriptorBehavior { eventSource: Resource, environmentVariable: [String: String]) { if withFunction { - self.deploymentDescriptor = DeploymentDescriptor { "A SAM template to deploy a Swift Lambda function" @@ -106,16 +104,18 @@ struct MockDeploymentDescriptorBuilder: MockDeploymentDescriptorBehavior { } func toJSON() -> String { - return self.deploymentDescriptor.samDeploymentDescriptor.toJSON(pretty: false) + self.deploymentDescriptor.samDeploymentDescriptor.toJSON(pretty: false) } + func toYAML() -> String { - return self.deploymentDescriptor.samDeploymentDescriptor.toYAML() + self.deploymentDescriptor.samDeploymentDescriptor.toYAML() } static func packageDir() -> String { - return "/\(functionName)" + "/\(self.functionName)" } + static func packageZip() -> String { - return "/\(functionName).zip" + "/\(self.functionName).zip" } } diff --git a/Tests/AWSLambdaDeploymentDescriptorTests/YAMLEncoderTests.swift b/Tests/AWSLambdaDeploymentDescriptorTests/YAMLEncoderTests.swift index a0245ef..6cdf792 100644 --- a/Tests/AWSLambdaDeploymentDescriptorTests/YAMLEncoderTests.swift +++ b/Tests/AWSLambdaDeploymentDescriptorTests/YAMLEncoderTests.swift @@ -20,995 +20,1028 @@ import XCTest @testable import AWSLambdaDeploymentDescriptor struct TopLevelObjectWrapper: Codable, Equatable { - var value: T + var value: T - static func == (lhs: TopLevelObjectWrapper, rhs: TopLevelObjectWrapper) -> Bool { - return lhs.value == rhs.value - } + static func == (lhs: TopLevelObjectWrapper, rhs: TopLevelObjectWrapper) -> Bool { + lhs.value == rhs.value + } - init(_ value: T) { - self.value = value - } + init(_ value: T) { + self.value = value + } } class TestYAMLEncoder: XCTestCase { + // MARK: - Encoding Top-Level fragments + + func test_encodingTopLevelFragments() { + func _testFragment(value: T, fragment: String) { + let data: Data + let payload: String + + do { + data = try YAMLEncoder().encode(value) + payload = try XCTUnwrap(String(decoding: data, as: UTF8.self)) + XCTAssertEqual(fragment, payload) + } catch { + XCTFail("Failed to encode \(T.self) to YAML: \(error)") + return + } + } + + _testFragment(value: 2, fragment: "2") + _testFragment(value: false, fragment: "false") + _testFragment(value: true, fragment: "true") + _testFragment(value: Float(1), fragment: "1") + _testFragment(value: Double(2), fragment: "2") + _testFragment( + value: Decimal(Double(Float.leastNormalMagnitude)), + fragment: "0.000000000000000000000000000000000000011754943508222875648" + ) + _testFragment(value: "test", fragment: "test") + let v: Int? = nil + _testFragment(value: v, fragment: "null") + } - // MARK: - Encoding Top-Level fragments - func test_encodingTopLevelFragments() { + // MARK: - Encoding Top-Level Empty Types - func _testFragment(value: T, fragment: String) { - let data: Data - let payload: String + func test_encodingTopLevelEmptyStruct() { + let empty = EmptyStruct() + self._testRoundTrip(of: empty, expectedYAML: self._yamlEmptyDictionary) + } - do { - data = try YAMLEncoder().encode(value) - payload = try XCTUnwrap(String.init(decoding: data, as: UTF8.self)) - XCTAssertEqual(fragment, payload) - } catch { - XCTFail("Failed to encode \(T.self) to YAML: \(error)") - return - } - } - - _testFragment(value: 2, fragment: "2") - _testFragment(value: false, fragment: "false") - _testFragment(value: true, fragment: "true") - _testFragment(value: Float(1), fragment: "1") - _testFragment(value: Double(2), fragment: "2") - _testFragment( - value: Decimal(Double(Float.leastNormalMagnitude)), - fragment: "0.000000000000000000000000000000000000011754943508222875648") - _testFragment(value: "test", fragment: "test") - let v: Int? = nil - _testFragment(value: v, fragment: "null") - } - - // MARK: - Encoding Top-Level Empty Types - func test_encodingTopLevelEmptyStruct() { - let empty = EmptyStruct() - _testRoundTrip(of: empty, expectedYAML: _yamlEmptyDictionary) - } - - func test_encodingTopLevelEmptyClass() { - let empty = EmptyClass() - _testRoundTrip(of: empty, expectedYAML: _yamlEmptyDictionary) - } - - // MARK: - Encoding Top-Level Single-Value Types - func test_encodingTopLevelSingleValueEnum() { - _testRoundTrip(of: Switch.off) - _testRoundTrip(of: Switch.on) - - _testRoundTrip(of: TopLevelArrayWrapper(Switch.off)) - _testRoundTrip(of: TopLevelArrayWrapper(Switch.on)) - } - - func test_encodingTopLevelSingleValueStruct() { - _testRoundTrip(of: Timestamp(3_141_592_653)) - _testRoundTrip(of: TopLevelArrayWrapper(Timestamp(3_141_592_653))) - } - - func test_encodingTopLevelSingleValueClass() { - _testRoundTrip(of: Counter()) - _testRoundTrip(of: TopLevelArrayWrapper(Counter())) - } - - // MARK: - Encoding Top-Level Structured Types - func test_encodingTopLevelStructuredStruct() { - // Address is a struct type with multiple fields. - let address = Address.testValue - _testRoundTrip(of: address) - } - - func test_encodingTopLevelStructuredClass() { - // Person is a class with multiple fields. - let expectedYAML = ["name: Johnny Appleseed", "email: appleseed@apple.com"] - let person = Person.testValue - _testRoundTrip(of: person, expectedYAML: expectedYAML) - } - - func test_encodingTopLevelStructuredSingleStruct() { - // Numbers is a struct which encodes as an array through a single value container. - let numbers = Numbers.testValue - _testRoundTrip(of: numbers) - } - - func test_encodingTopLevelStructuredSingleClass() { - // Mapping is a class which encodes as a dictionary through a single value container. - let mapping = Mapping.testValue - _testRoundTrip(of: mapping) - } - - func test_encodingTopLevelDeepStructuredType() { - // Company is a type with fields which are Codable themselves. - let company = Company.testValue - _testRoundTrip(of: company) - } - - // MARK: - Output Formatting Tests - func test_encodingOutputFormattingDefault() { - let expectedYAML = ["name: Johnny Appleseed", "email: appleseed@apple.com"] - let person = Person.testValue - _testRoundTrip(of: person, expectedYAML: expectedYAML) - } - - func test_encodingOutputFormattingPrettyPrinted() throws { - let expectedYAML = ["name: Johnny Appleseed", "email: appleseed@apple.com"] - let person = Person.testValue - _testRoundTrip(of: person, expectedYAML: expectedYAML) - - let encoder = YAMLEncoder() - encoder.outputFormatting = [.sortedKeys] - - let emptyArray: [Int] = [] - let arrayOutput = try encoder.encode(emptyArray) - XCTAssertEqual(String.init(decoding: arrayOutput, as: UTF8.self), "") - - let emptyDictionary: [String: Int] = [:] - let dictionaryOutput = try encoder.encode(emptyDictionary) - XCTAssertEqual(String.init(decoding: dictionaryOutput, as: UTF8.self), "") - - struct DataType: Encodable { - let array = [1, 2, 3] - let dictionary: [String: Int] = [:] - let emptyAray: [Int] = [] - let secondArray: [Int] = [4, 5, 6] - let secondDictionary: [String: Int] = ["one": 1, "two": 2, "three": 3] - let singleElement: [Int] = [1] - let subArray: [String: [Int]] = ["array": []] - let subDictionary: [String: [String: Int]] = ["dictionary": [:]] - } - - let dataOutput = try encoder.encode([DataType(), DataType()]) - XCTAssertEqual( - String.init(decoding: dataOutput, as: UTF8.self), - """ - - - - array: - - 1 - - 2 - - 3 - dictionary: - emptyAray: - secondArray: - - 4 - - 5 - - 6 - secondDictionary: - one: 1 - three: 3 - two: 2 - singleElement: - - 1 - subArray: - array: - subDictionary: - dictionary: - - - array: - - 1 - - 2 - - 3 - dictionary: - emptyAray: - secondArray: - - 4 - - 5 - - 6 - secondDictionary: - one: 1 - three: 3 - two: 2 - singleElement: - - 1 - subArray: - array: - subDictionary: - dictionary: - """) - } - - func test_encodingOutputFormattingSortedKeys() { - let expectedYAML = ["name: Johnny Appleseed", "email: appleseed@apple.com"] - let person = Person.testValue - _testRoundTrip(of: person, expectedYAML: expectedYAML) - } - - func test_encodingOutputFormattingPrettyPrintedSortedKeys() { - let expectedYAML = ["name: Johnny Appleseed", "email: appleseed@apple.com"] - let person = Person.testValue - _testRoundTrip(of: person, expectedYAML: expectedYAML) - } - - // MARK: - Date Strategy Tests - func test_encodingDate() { - // We can't encode a top-level Date, so it'll be wrapped in an array. - _testRoundTrip(of: TopLevelArrayWrapper(Date())) - } - - func test_encodingDateSecondsSince1970() { - let seconds = 1000.0 - let expectedYAML = ["1000"] - - let d = Date(timeIntervalSince1970: seconds) - _testRoundTrip( - of: d, - expectedYAML: expectedYAML, - dateEncodingStrategy: .secondsSince1970) - } - - func test_encodingDateMillisecondsSince1970() { - let seconds = 1000.0 - let expectedYAML = ["1000000"] - - _testRoundTrip( - of: Date(timeIntervalSince1970: seconds), - expectedYAML: expectedYAML, - dateEncodingStrategy: .millisecondsSince1970) - } - - func test_encodingDateISO8601() { - let formatter = ISO8601DateFormatter() - formatter.formatOptions = .withInternetDateTime - - let timestamp = Date(timeIntervalSince1970: 1000) - let expectedYAML = ["\(formatter.string(from: timestamp))"] - - // We can't encode a top-level Date, so it'll be wrapped in an array. - _testRoundTrip( - of: TopLevelArrayWrapper(timestamp), - expectedYAML: expectedYAML, - dateEncodingStrategy: .iso8601) - - } - - func test_encodingDateFormatted() { - let formatter = DateFormatter() - formatter.dateStyle = .full - formatter.timeStyle = .full - - let timestamp = Date(timeIntervalSince1970: 1000) - let expectedYAML = ["\(formatter.string(from: timestamp))"] - - // We can't encode a top-level Date, so it'll be wrapped in an array. - _testRoundTrip( - of: TopLevelArrayWrapper(timestamp), - expectedYAML: expectedYAML, - dateEncodingStrategy: .formatted(formatter)) - } - - func test_encodingDateCustom() { - let timestamp = Date() - - // We'll encode a number instead of a date. - let encode = { (_ data: Date, _ encoder: Encoder) throws -> Void in - var container = encoder.singleValueContainer() - try container.encode(42) - } - // let decode = { (_: Decoder) throws -> Date in return timestamp } - - // We can't encode a top-level Date, so it'll be wrapped in an array. - let expectedYAML = ["42"] - _testRoundTrip( - of: TopLevelArrayWrapper(timestamp), - expectedYAML: expectedYAML, - dateEncodingStrategy: .custom(encode)) - } - - func test_encodingDateCustomEmpty() { - let timestamp = Date() - - // Encoding nothing should encode an empty keyed container ({}). - let encode = { (_: Date, _: Encoder) throws -> Void in } - // let decode = { (_: Decoder) throws -> Date in return timestamp } - - // We can't encode a top-level Date, so it'll be wrapped in an array. - let expectedYAML = [""] - _testRoundTrip( - of: TopLevelArrayWrapper(timestamp), - expectedYAML: expectedYAML, - dateEncodingStrategy: .custom(encode)) - } - - // MARK: - Data Strategy Tests - func test_encodingBase64Data() { - let data = Data([0xDE, 0xAD, 0xBE, 0xEF]) - - // We can't encode a top-level Data, so it'll be wrapped in an array. - let expectedYAML = ["3q2+7w=="] - _testRoundTrip(of: TopLevelArrayWrapper(data), expectedYAML: expectedYAML) - } - - func test_encodingCustomData() { - // We'll encode a number instead of data. - let encode = { (_ data: Data, _ encoder: Encoder) throws -> Void in - var container = encoder.singleValueContainer() - try container.encode(42) - } - // let decode = { (_: Decoder) throws -> Data in return Data() } - - // We can't encode a top-level Data, so it'll be wrapped in an array. - let expectedYAML = ["42"] - _testRoundTrip( - of: TopLevelArrayWrapper(Data()), - expectedYAML: expectedYAML, - dataEncodingStrategy: .custom(encode)) - } - - func test_encodingCustomDataEmpty() { - // Encoding nothing should encode an empty keyed container ({}). - let encode = { (_: Data, _: Encoder) throws -> Void in } - // let decode = { (_: Decoder) throws -> Data in return Data() } - - // We can't encode a top-level Data, so it'll be wrapped in an array. - let expectedYAML = [""] - _testRoundTrip( - of: TopLevelArrayWrapper(Data()), - expectedYAML: expectedYAML, - dataEncodingStrategy: .custom(encode)) - } - - // MARK: - Non-Conforming Floating Point Strategy Tests - func test_encodingNonConformingFloats() { - _testEncodeFailure(of: TopLevelArrayWrapper(Float.infinity)) - _testEncodeFailure(of: TopLevelArrayWrapper(-Float.infinity)) - _testEncodeFailure(of: TopLevelArrayWrapper(Float.nan)) - - _testEncodeFailure(of: TopLevelArrayWrapper(Double.infinity)) - _testEncodeFailure(of: TopLevelArrayWrapper(-Double.infinity)) - _testEncodeFailure(of: TopLevelArrayWrapper(Double.nan)) - } - - func test_encodingNonConformingFloatStrings() { - let encodingStrategy: YAMLEncoder.NonConformingFloatEncodingStrategy = .convertToString( - positiveInfinity: "INF", negativeInfinity: "-INF", nan: "NaN") - // let decodingStrategy: YAMLDecoder.NonConformingFloatDecodingStrategy = .convertFromString(positiveInfinity: "INF", negativeInfinity: "-INF", nan: "NaN") - - _testRoundTrip( - of: TopLevelArrayWrapper(Float.infinity), - expectedYAML: ["INF"], - nonConformingFloatEncodingStrategy: encodingStrategy) - _testRoundTrip( - of: TopLevelArrayWrapper(-Float.infinity), - expectedYAML: ["-INF"], - nonConformingFloatEncodingStrategy: encodingStrategy) - - // Since Float.nan != Float.nan, we have to use a placeholder that'll encode NaN but actually round-trip. - _testRoundTrip( - of: TopLevelArrayWrapper(FloatNaNPlaceholder()), - expectedYAML: ["NaN"], - nonConformingFloatEncodingStrategy: encodingStrategy) - - _testRoundTrip( - of: TopLevelArrayWrapper(Double.infinity), - expectedYAML: ["INF"], - nonConformingFloatEncodingStrategy: encodingStrategy) - _testRoundTrip( - of: TopLevelArrayWrapper(-Double.infinity), - expectedYAML: ["-INF"], - nonConformingFloatEncodingStrategy: encodingStrategy) - - // Since Double.nan != Double.nan, we have to use a placeholder that'll encode NaN but actually round-trip. - _testRoundTrip( - of: TopLevelArrayWrapper(DoubleNaNPlaceholder()), - expectedYAML: ["NaN"], - nonConformingFloatEncodingStrategy: encodingStrategy) - } - - // MARK: - Encoder Features - func test_nestedContainerCodingPaths() { - let encoder = YAMLEncoder() - do { - _ = try encoder.encode(NestedContainersTestType()) - } catch { - XCTFail("Caught error during encoding nested container types: \(error)") - } - } - - func test_superEncoderCodingPaths() { - let encoder = YAMLEncoder() - do { - _ = try encoder.encode(NestedContainersTestType(testSuperEncoder: true)) - } catch { - XCTFail("Caught error during encoding nested container types: \(error)") - } - } - - func test_notFoundSuperDecoder() { - struct NotFoundSuperDecoderTestType: Decodable { - init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - _ = try container.superDecoder(forKey: .superDecoder) - } - - private enum CodingKeys: String, CodingKey { - case superDecoder = "super" - } - } - // let decoder = YAMLDecoder() - // do { - // let _ = try decoder.decode(NotFoundSuperDecoderTestType.self, from: Data(#"{}"#.utf8)) - // } catch { - // XCTFail("Caught error during decoding empty super decoder: \(error)") - // } - } - - // MARK: - Test encoding and decoding of built-in Codable types - func test_codingOfBool() { - test_codingOf(value: Bool(true), toAndFrom: "true") - test_codingOf(value: Bool(false), toAndFrom: "false") - - // do { - // _ = try YAMLDecoder().decode([Bool].self, from: "[1]".data(using: .utf8)!) - // XCTFail("Coercing non-boolean numbers into Bools was expected to fail") - // } catch { } - - // Check that a Bool false or true isn't converted to 0 or 1 - // struct Foo: Decodable { - // var intValue: Int? - // var int8Value: Int8? - // var int16Value: Int16? - // var int32Value: Int32? - // var int64Value: Int64? - // var uintValue: UInt? - // var uint8Value: UInt8? - // var uint16Value: UInt16? - // var uint32Value: UInt32? - // var uint64Value: UInt64? - // var floatValue: Float? - // var doubleValue: Double? - // var decimalValue: Decimal? - // let boolValue: Bool - // } - - // func testValue(_ valueName: String) { - // do { - // let jsonData = "{ \"\(valueName)\": false }".data(using: .utf8)! - // _ = try YAMLDecoder().decode(Foo.self, from: jsonData) - // XCTFail("Decoded 'false' as non Bool for \(valueName)") - // } catch {} - // do { - // let jsonData = "{ \"\(valueName)\": true }".data(using: .utf8)! - // _ = try YAMLDecoder().decode(Foo.self, from: jsonData) - // XCTFail("Decoded 'true' as non Bool for \(valueName)") - // } catch {} - // } - - // testValue("intValue") - // testValue("int8Value") - // testValue("int16Value") - // testValue("int32Value") - // testValue("int64Value") - // testValue("uintValue") - // testValue("uint8Value") - // testValue("uint16Value") - // testValue("uint32Value") - // testValue("uint64Value") - // testValue("floatValue") - // testValue("doubleValue") - // testValue("decimalValue") - // let falseJsonData = "{ \"boolValue\": false }".data(using: .utf8)! - // if let falseFoo = try? YAMLDecoder().decode(Foo.self, from: falseJsonData) { - // XCTAssertFalse(falseFoo.boolValue) - // } else { - // XCTFail("Could not decode 'false' as a Bool") - // } - - // let trueJsonData = "{ \"boolValue\": true }".data(using: .utf8)! - // if let trueFoo = try? YAMLDecoder().decode(Foo.self, from: trueJsonData) { - // XCTAssertTrue(trueFoo.boolValue) - // } else { - // XCTFail("Could not decode 'true' as a Bool") - // } - } - - func test_codingOfNil() { - let x: Int? = nil - test_codingOf(value: x, toAndFrom: "null") - } - - func test_codingOfInt8() { - test_codingOf(value: Int8(-42), toAndFrom: "-42") - } - - func test_codingOfUInt8() { - test_codingOf(value: UInt8(42), toAndFrom: "42") - } - - func test_codingOfInt16() { - test_codingOf(value: Int16(-30042), toAndFrom: "-30042") - } - - func test_codingOfUInt16() { - test_codingOf(value: UInt16(30042), toAndFrom: "30042") - } - - func test_codingOfInt32() { - test_codingOf(value: Int32(-2_000_000_042), toAndFrom: "-2000000042") - } - - func test_codingOfUInt32() { - test_codingOf(value: UInt32(2_000_000_042), toAndFrom: "2000000042") - } - - func test_codingOfInt64() { - #if !arch(arm) - test_codingOf(value: Int64(-9_000_000_000_000_000_042), toAndFrom: "-9000000000000000042") - #endif - } - - func test_codingOfUInt64() { - #if !arch(arm) - test_codingOf(value: UInt64(9_000_000_000_000_000_042), toAndFrom: "9000000000000000042") - #endif - } - - func test_codingOfInt() { - let intSize = MemoryLayout.size - switch intSize { - case 4: // 32-bit - test_codingOf(value: Int(-2_000_000_042), toAndFrom: "-2000000042") - case 8: // 64-bit - #if arch(arm) - break - #else - test_codingOf(value: Int(-9_000_000_000_000_000_042), toAndFrom: "-9000000000000000042") - #endif - default: - XCTFail("Unexpected UInt size: \(intSize)") - } - } - - func test_codingOfUInt() { - let uintSize = MemoryLayout.size - switch uintSize { - case 4: // 32-bit - test_codingOf(value: UInt(2_000_000_042), toAndFrom: "2000000042") - case 8: // 64-bit - #if arch(arm) - break - #else - test_codingOf(value: UInt(9_000_000_000_000_000_042), toAndFrom: "9000000000000000042") - #endif - default: - XCTFail("Unexpected UInt size: \(uintSize)") - } - } - - func test_codingOfFloat() { - test_codingOf(value: Float(1.5), toAndFrom: "1.5") - - // Check value too large fails to decode. - // XCTAssertThrowsError(try YAMLDecoder().decode(Float.self, from: "1e100".data(using: .utf8)!)) - } - - func test_codingOfDouble() { - test_codingOf(value: Double(1.5), toAndFrom: "1.5") - - // Check value too large fails to decode. - // XCTAssertThrowsError(try YAMLDecoder().decode(Double.self, from: "100e323".data(using: .utf8)!)) - } - - func test_codingOfDecimal() { - test_codingOf(value: Decimal.pi, toAndFrom: "3.14159265358979323846264338327950288419") - - // Check value too large fails to decode. - // XCTAssertThrowsError(try YAMLDecoder().decode(Decimal.self, from: "100e200".data(using: .utf8)!)) - } - - func test_codingOfString() { - test_codingOf(value: "Hello, world!", toAndFrom: "Hello, world!") - } - - func test_codingOfURL() { - test_codingOf(value: URL(string: "https://swift.org")!, toAndFrom: "https://swift.org") - } - - // UInt and Int - func test_codingOfUIntMinMax() { - - struct MyValue: Codable, Equatable { - var int64Min = Int64.min - var int64Max = Int64.max - var uint64Min = UInt64.min - var uint64Max = UInt64.max - } - - let myValue = MyValue() - _testRoundTrip( - of: myValue, - expectedYAML: [ - "uint64Min: 0", - "uint64Max: 18446744073709551615", - "int64Min: -9223372036854775808", - "int64Max: 9223372036854775807", - ]) - } - - func test_CamelCaseEncoding() throws { - struct MyTestData: Codable, Equatable { - let thisIsAString: String - let thisIsABool: Bool - let thisIsAnInt: Int - let thisIsAnInt8: Int8 - let thisIsAnInt16: Int16 - let thisIsAnInt32: Int32 - let thisIsAnInt64: Int64 - let thisIsAUint: UInt - let thisIsAUint8: UInt8 - let thisIsAUint16: UInt16 - let thisIsAUint32: UInt32 - let thisIsAUint64: UInt64 - let thisIsAFloat: Float - let thisIsADouble: Double - let thisIsADate: Date - let thisIsAnArray: [Int] - let thisIsADictionary: [String: Bool] - } - - let data = MyTestData( - thisIsAString: "Hello", - thisIsABool: true, - thisIsAnInt: 1, - thisIsAnInt8: 2, - thisIsAnInt16: 3, - thisIsAnInt32: 4, - thisIsAnInt64: 5, - thisIsAUint: 6, - thisIsAUint8: 7, - thisIsAUint16: 8, - thisIsAUint32: 9, - thisIsAUint64: 10, - thisIsAFloat: 11, - thisIsADouble: 12, - thisIsADate: Date.init(timeIntervalSince1970: 0), - thisIsAnArray: [1, 2, 3], - thisIsADictionary: ["trueValue": true, "falseValue": false] - ) - - let encoder = YAMLEncoder() - encoder.keyEncodingStrategy = .camelCase - encoder.dateEncodingStrategy = .iso8601 - let encodedData = try encoder.encode(data) - let yaml = String(decoding: encodedData, as:UTF8.self) - - XCTAssertTrue(yaml.contains("ThisIsABool: true")) - XCTAssertTrue(yaml.contains("ThisIsAnInt: 1")) - XCTAssertTrue(yaml.contains("ThisIsAnInt8: 2")) - XCTAssertTrue(yaml.contains("ThisIsAnInt16: 3")) - XCTAssertTrue(yaml.contains("ThisIsAnInt32: 4")) - XCTAssertTrue(yaml.contains("ThisIsAnInt64: 5")) - XCTAssertTrue(yaml.contains("ThisIsAUint: 6")) - XCTAssertTrue(yaml.contains("ThisIsAUint8: 7")) - XCTAssertTrue(yaml.contains("ThisIsAUint16: 8")) - XCTAssertTrue(yaml.contains("ThisIsAUint32: 9")) - XCTAssertTrue(yaml.contains("ThisIsAUint64: 10")) - XCTAssertTrue(yaml.contains("ThisIsAFloat: 11")) - XCTAssertTrue(yaml.contains("ThisIsADouble: 12")) - XCTAssertTrue(yaml.contains("ThisIsADate: 1970-01-01T00:00:00Z")) - XCTAssertTrue(yaml.contains("ThisIsAnArray:")) - XCTAssertTrue(yaml.contains("- 1")) - XCTAssertTrue(yaml.contains("- 2")) - XCTAssertTrue(yaml.contains("- 3")) - } - - func test_DictionaryCamelCaseEncoding() throws { - let camelCaseDictionary = ["camelCaseKey": ["nestedDictionary": 1]] - - let encoder = YAMLEncoder() - encoder.keyEncodingStrategy = .camelCase - let encodedData = try encoder.encode(camelCaseDictionary) - - let yaml = String(decoding: encodedData, as: UTF8.self) - - print(yaml) - XCTAssertTrue(yaml.contains("CamelCaseKey:")) - XCTAssertTrue(yaml.contains(" NestedDictionary: 1")) - } - - func test_OutputFormattingValues() { - XCTAssertEqual(YAMLEncoder.OutputFormatting.withoutEscapingSlashes.rawValue, 8) - } - - func test_SR17581_codingEmptyDictionaryWithNonstringKeyDoesRoundtrip() throws { - struct Something: Codable { - struct Key: Codable, Hashable { - var x: String - } - - var dict: [Key: String] - - enum CodingKeys: String, CodingKey { - case dict - } - - init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - self.dict = try container.decode([Key: String].self, forKey: .dict) - } - - func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(dict, forKey: .dict) - } - - init(dict: [Key: String]) { - self.dict = dict - } - } - - // let toEncode = Something(dict: [:]) - // let data = try YAMLEncoder().encode(toEncode) - // let result = try YAMLDecoder().decode(Something.self, from: data) - // XCTAssertEqual(result.dict.count, 0) - } - - // MARK: - Helper Functions - private var _yamlEmptyDictionary: [String] { - return [""] - } - - private func _testEncodeFailure(of value: T) { - do { - _ = try YAMLEncoder().encode(value) - XCTFail("Encode of top-level \(T.self) was expected to fail.") - } catch {} - } - - private func _testRoundTrip( - of value: T, - expectedYAML yaml: [String] = [], - dateEncodingStrategy: YAMLEncoder.DateEncodingStrategy = .deferredToDate, - dataEncodingStrategy: YAMLEncoder.DataEncodingStrategy = .base64, - nonConformingFloatEncodingStrategy: YAMLEncoder.NonConformingFloatEncodingStrategy = .throw - ) where T: Codable, T: Equatable { - var payload: Data! = nil - do { - let encoder = YAMLEncoder() - encoder.dateEncodingStrategy = dateEncodingStrategy - encoder.dataEncodingStrategy = dataEncodingStrategy - encoder.nonConformingFloatEncodingStrategy = nonConformingFloatEncodingStrategy - payload = try encoder.encode(value) - } catch { - XCTFail("Failed to encode \(T.self) to YAML: \(error)") - } - - // We do not compare expectedYAML to payload directly, because they might have values like - // {"name": "Bob", "age": 22} - // and - // {"age": 22, "name": "Bob"} - // which if compared as Data would not be equal, but the contained YAML values are equal. - // So we wrap them in a YAML type, which compares data as if it were a json. - - let payloadYAMLObject = String(decoding: payload, as: UTF8.self) - let result = yaml.allSatisfy { payloadYAMLObject.contains($0) || $0 == "" } - XCTAssertTrue(result, "Produced YAML not identical to expected YAML.") - - if !result { - print("===========") - print(payloadYAMLObject) - print("-----------") - print(yaml.filter { !payloadYAMLObject.contains($0) }.compactMap { $0 }) - print("===========") - } - } - - func test_codingOf(value: T, toAndFrom stringValue: String) { - _testRoundTrip( - of: TopLevelObjectWrapper(value), - expectedYAML: ["value: \(stringValue)"]) - - _testRoundTrip( - of: TopLevelArrayWrapper(value), - expectedYAML: ["\(stringValue)"]) - } + func test_encodingTopLevelEmptyClass() { + let empty = EmptyClass() + self._testRoundTrip(of: empty, expectedYAML: self._yamlEmptyDictionary) + } + + // MARK: - Encoding Top-Level Single-Value Types + + func test_encodingTopLevelSingleValueEnum() { + self._testRoundTrip(of: Switch.off) + self._testRoundTrip(of: Switch.on) + + self._testRoundTrip(of: TopLevelArrayWrapper(Switch.off)) + self._testRoundTrip(of: TopLevelArrayWrapper(Switch.on)) + } + + func test_encodingTopLevelSingleValueStruct() { + self._testRoundTrip(of: Timestamp(3_141_592_653)) + self._testRoundTrip(of: TopLevelArrayWrapper(Timestamp(3_141_592_653))) + } + + func test_encodingTopLevelSingleValueClass() { + self._testRoundTrip(of: Counter()) + self._testRoundTrip(of: TopLevelArrayWrapper(Counter())) + } + + // MARK: - Encoding Top-Level Structured Types + + func test_encodingTopLevelStructuredStruct() { + // Address is a struct type with multiple fields. + let address = Address.testValue + self._testRoundTrip(of: address) + } + + func test_encodingTopLevelStructuredClass() { + // Person is a class with multiple fields. + let expectedYAML = ["name: Johnny Appleseed", "email: appleseed@apple.com"] + let person = Person.testValue + self._testRoundTrip(of: person, expectedYAML: expectedYAML) + } + + func test_encodingTopLevelStructuredSingleStruct() { + // Numbers is a struct which encodes as an array through a single value container. + let numbers = Numbers.testValue + self._testRoundTrip(of: numbers) + } + + func test_encodingTopLevelStructuredSingleClass() { + // Mapping is a class which encodes as a dictionary through a single value container. + let mapping = Mapping.testValue + self._testRoundTrip(of: mapping) + } + + func test_encodingTopLevelDeepStructuredType() { + // Company is a type with fields which are Codable themselves. + let company = Company.testValue + self._testRoundTrip(of: company) + } + + // MARK: - Output Formatting Tests + + func test_encodingOutputFormattingDefault() { + let expectedYAML = ["name: Johnny Appleseed", "email: appleseed@apple.com"] + let person = Person.testValue + self._testRoundTrip(of: person, expectedYAML: expectedYAML) + } + + func test_encodingOutputFormattingPrettyPrinted() throws { + let expectedYAML = ["name: Johnny Appleseed", "email: appleseed@apple.com"] + let person = Person.testValue + self._testRoundTrip(of: person, expectedYAML: expectedYAML) + + let encoder = YAMLEncoder() + encoder.outputFormatting = [.sortedKeys] + + let emptyArray: [Int] = [] + let arrayOutput = try encoder.encode(emptyArray) + XCTAssertEqual(String(decoding: arrayOutput, as: UTF8.self), "") + + let emptyDictionary: [String: Int] = [:] + let dictionaryOutput = try encoder.encode(emptyDictionary) + XCTAssertEqual(String(decoding: dictionaryOutput, as: UTF8.self), "") + + struct DataType: Encodable { + let array = [1, 2, 3] + let dictionary: [String: Int] = [:] + let emptyAray: [Int] = [] + let secondArray: [Int] = [4, 5, 6] + let secondDictionary: [String: Int] = ["one": 1, "two": 2, "three": 3] + let singleElement: [Int] = [1] + let subArray: [String: [Int]] = ["array": []] + let subDictionary: [String: [String: Int]] = ["dictionary": [:]] + } + + let dataOutput = try encoder.encode([DataType(), DataType()]) + XCTAssertEqual( + String(decoding: dataOutput, as: UTF8.self), + """ + + - + array: + - 1 + - 2 + - 3 + dictionary: + emptyAray: + secondArray: + - 4 + - 5 + - 6 + secondDictionary: + one: 1 + three: 3 + two: 2 + singleElement: + - 1 + subArray: + array: + subDictionary: + dictionary: + - + array: + - 1 + - 2 + - 3 + dictionary: + emptyAray: + secondArray: + - 4 + - 5 + - 6 + secondDictionary: + one: 1 + three: 3 + two: 2 + singleElement: + - 1 + subArray: + array: + subDictionary: + dictionary: + """ + ) + } + + func test_encodingOutputFormattingSortedKeys() { + let expectedYAML = ["name: Johnny Appleseed", "email: appleseed@apple.com"] + let person = Person.testValue + self._testRoundTrip(of: person, expectedYAML: expectedYAML) + } + + func test_encodingOutputFormattingPrettyPrintedSortedKeys() { + let expectedYAML = ["name: Johnny Appleseed", "email: appleseed@apple.com"] + let person = Person.testValue + self._testRoundTrip(of: person, expectedYAML: expectedYAML) + } + + // MARK: - Date Strategy Tests + + func test_encodingDate() { + // We can't encode a top-level Date, so it'll be wrapped in an array. + self._testRoundTrip(of: TopLevelArrayWrapper(Date())) + } + + func test_encodingDateSecondsSince1970() { + let seconds = 1000.0 + let expectedYAML = ["1000"] + + let d = Date(timeIntervalSince1970: seconds) + self._testRoundTrip( + of: d, + expectedYAML: expectedYAML, + dateEncodingStrategy: .secondsSince1970 + ) + } + + func test_encodingDateMillisecondsSince1970() { + let seconds = 1000.0 + let expectedYAML = ["1000000"] + + self._testRoundTrip( + of: Date(timeIntervalSince1970: seconds), + expectedYAML: expectedYAML, + dateEncodingStrategy: .millisecondsSince1970 + ) + } + + func test_encodingDateISO8601() { + let formatter = ISO8601DateFormatter() + formatter.formatOptions = .withInternetDateTime + + let timestamp = Date(timeIntervalSince1970: 1000) + let expectedYAML = ["\(formatter.string(from: timestamp))"] + + // We can't encode a top-level Date, so it'll be wrapped in an array. + self._testRoundTrip( + of: TopLevelArrayWrapper(timestamp), + expectedYAML: expectedYAML, + dateEncodingStrategy: .iso8601 + ) + } + + func test_encodingDateFormatted() { + let formatter = DateFormatter() + formatter.dateStyle = .full + formatter.timeStyle = .full + + let timestamp = Date(timeIntervalSince1970: 1000) + let expectedYAML = ["\(formatter.string(from: timestamp))"] + + // We can't encode a top-level Date, so it'll be wrapped in an array. + self._testRoundTrip( + of: TopLevelArrayWrapper(timestamp), + expectedYAML: expectedYAML, + dateEncodingStrategy: .formatted(formatter) + ) + } + + func test_encodingDateCustom() { + let timestamp = Date() + + // We'll encode a number instead of a date. + let encode = { (_: Date, _ encoder: Encoder) throws in + var container = encoder.singleValueContainer() + try container.encode(42) + } + // let decode = { (_: Decoder) throws -> Date in return timestamp } + + // We can't encode a top-level Date, so it'll be wrapped in an array. + let expectedYAML = ["42"] + self._testRoundTrip( + of: TopLevelArrayWrapper(timestamp), + expectedYAML: expectedYAML, + dateEncodingStrategy: .custom(encode) + ) + } + + func test_encodingDateCustomEmpty() { + let timestamp = Date() + + // Encoding nothing should encode an empty keyed container ({}). + let encode = { (_: Date, _: Encoder) throws in } + // let decode = { (_: Decoder) throws -> Date in return timestamp } + + // We can't encode a top-level Date, so it'll be wrapped in an array. + let expectedYAML = [""] + self._testRoundTrip( + of: TopLevelArrayWrapper(timestamp), + expectedYAML: expectedYAML, + dateEncodingStrategy: .custom(encode) + ) + } + + // MARK: - Data Strategy Tests + + func test_encodingBase64Data() { + let data = Data([0xDE, 0xAD, 0xBE, 0xEF]) + + // We can't encode a top-level Data, so it'll be wrapped in an array. + let expectedYAML = ["3q2+7w=="] + self._testRoundTrip(of: TopLevelArrayWrapper(data), expectedYAML: expectedYAML) + } + + func test_encodingCustomData() { + // We'll encode a number instead of data. + let encode = { (_: Data, _ encoder: Encoder) throws in + var container = encoder.singleValueContainer() + try container.encode(42) + } + // let decode = { (_: Decoder) throws -> Data in return Data() } + + // We can't encode a top-level Data, so it'll be wrapped in an array. + let expectedYAML = ["42"] + self._testRoundTrip( + of: TopLevelArrayWrapper(Data()), + expectedYAML: expectedYAML, + dataEncodingStrategy: .custom(encode) + ) + } + + func test_encodingCustomDataEmpty() { + // Encoding nothing should encode an empty keyed container ({}). + let encode = { (_: Data, _: Encoder) throws in } + // let decode = { (_: Decoder) throws -> Data in return Data() } + + // We can't encode a top-level Data, so it'll be wrapped in an array. + let expectedYAML = [""] + self._testRoundTrip( + of: TopLevelArrayWrapper(Data()), + expectedYAML: expectedYAML, + dataEncodingStrategy: .custom(encode) + ) + } + + // MARK: - Non-Conforming Floating Point Strategy Tests + + func test_encodingNonConformingFloats() { + self._testEncodeFailure(of: TopLevelArrayWrapper(Float.infinity)) + self._testEncodeFailure(of: TopLevelArrayWrapper(-Float.infinity)) + self._testEncodeFailure(of: TopLevelArrayWrapper(Float.nan)) + + self._testEncodeFailure(of: TopLevelArrayWrapper(Double.infinity)) + self._testEncodeFailure(of: TopLevelArrayWrapper(-Double.infinity)) + self._testEncodeFailure(of: TopLevelArrayWrapper(Double.nan)) + } + + func test_encodingNonConformingFloatStrings() { + let encodingStrategy: YAMLEncoder.NonConformingFloatEncodingStrategy = .convertToString( + positiveInfinity: "INF", negativeInfinity: "-INF", nan: "NaN" + ) + // let decodingStrategy: YAMLDecoder.NonConformingFloatDecodingStrategy = .convertFromString(positiveInfinity: "INF", negativeInfinity: "-INF", nan: "NaN") + + self._testRoundTrip( + of: TopLevelArrayWrapper(Float.infinity), + expectedYAML: ["INF"], + nonConformingFloatEncodingStrategy: encodingStrategy + ) + self._testRoundTrip( + of: TopLevelArrayWrapper(-Float.infinity), + expectedYAML: ["-INF"], + nonConformingFloatEncodingStrategy: encodingStrategy + ) + + // Since Float.nan != Float.nan, we have to use a placeholder that'll encode NaN but actually round-trip. + self._testRoundTrip( + of: TopLevelArrayWrapper(FloatNaNPlaceholder()), + expectedYAML: ["NaN"], + nonConformingFloatEncodingStrategy: encodingStrategy + ) + + self._testRoundTrip( + of: TopLevelArrayWrapper(Double.infinity), + expectedYAML: ["INF"], + nonConformingFloatEncodingStrategy: encodingStrategy + ) + self._testRoundTrip( + of: TopLevelArrayWrapper(-Double.infinity), + expectedYAML: ["-INF"], + nonConformingFloatEncodingStrategy: encodingStrategy + ) + + // Since Double.nan != Double.nan, we have to use a placeholder that'll encode NaN but actually round-trip. + self._testRoundTrip( + of: TopLevelArrayWrapper(DoubleNaNPlaceholder()), + expectedYAML: ["NaN"], + nonConformingFloatEncodingStrategy: encodingStrategy + ) + } + + // MARK: - Encoder Features + + func test_nestedContainerCodingPaths() { + let encoder = YAMLEncoder() + do { + _ = try encoder.encode(NestedContainersTestType()) + } catch { + XCTFail("Caught error during encoding nested container types: \(error)") + } + } + + func test_superEncoderCodingPaths() { + let encoder = YAMLEncoder() + do { + _ = try encoder.encode(NestedContainersTestType(testSuperEncoder: true)) + } catch { + XCTFail("Caught error during encoding nested container types: \(error)") + } + } + + func test_notFoundSuperDecoder() { + struct NotFoundSuperDecoderTestType: Decodable { + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + _ = try container.superDecoder(forKey: .superDecoder) + } + + private enum CodingKeys: String, CodingKey { + case superDecoder = "super" + } + } + // let decoder = YAMLDecoder() + // do { + // let _ = try decoder.decode(NotFoundSuperDecoderTestType.self, from: Data(#"{}"#.utf8)) + // } catch { + // XCTFail("Caught error during decoding empty super decoder: \(error)") + // } + } + + // MARK: - Test encoding and decoding of built-in Codable types + + func test_codingOfBool() { + self.test_codingOf(value: Bool(true), toAndFrom: "true") + self.test_codingOf(value: Bool(false), toAndFrom: "false") + + // do { + // _ = try YAMLDecoder().decode([Bool].self, from: "[1]".data(using: .utf8)!) + // XCTFail("Coercing non-boolean numbers into Bools was expected to fail") + // } catch { } + + // Check that a Bool false or true isn't converted to 0 or 1 + // struct Foo: Decodable { + // var intValue: Int? + // var int8Value: Int8? + // var int16Value: Int16? + // var int32Value: Int32? + // var int64Value: Int64? + // var uintValue: UInt? + // var uint8Value: UInt8? + // var uint16Value: UInt16? + // var uint32Value: UInt32? + // var uint64Value: UInt64? + // var floatValue: Float? + // var doubleValue: Double? + // var decimalValue: Decimal? + // let boolValue: Bool + // } + + // func testValue(_ valueName: String) { + // do { + // let jsonData = "{ \"\(valueName)\": false }".data(using: .utf8)! + // _ = try YAMLDecoder().decode(Foo.self, from: jsonData) + // XCTFail("Decoded 'false' as non Bool for \(valueName)") + // } catch {} + // do { + // let jsonData = "{ \"\(valueName)\": true }".data(using: .utf8)! + // _ = try YAMLDecoder().decode(Foo.self, from: jsonData) + // XCTFail("Decoded 'true' as non Bool for \(valueName)") + // } catch {} + // } + + // testValue("intValue") + // testValue("int8Value") + // testValue("int16Value") + // testValue("int32Value") + // testValue("int64Value") + // testValue("uintValue") + // testValue("uint8Value") + // testValue("uint16Value") + // testValue("uint32Value") + // testValue("uint64Value") + // testValue("floatValue") + // testValue("doubleValue") + // testValue("decimalValue") + // let falseJsonData = "{ \"boolValue\": false }".data(using: .utf8)! + // if let falseFoo = try? YAMLDecoder().decode(Foo.self, from: falseJsonData) { + // XCTAssertFalse(falseFoo.boolValue) + // } else { + // XCTFail("Could not decode 'false' as a Bool") + // } + + // let trueJsonData = "{ \"boolValue\": true }".data(using: .utf8)! + // if let trueFoo = try? YAMLDecoder().decode(Foo.self, from: trueJsonData) { + // XCTAssertTrue(trueFoo.boolValue) + // } else { + // XCTFail("Could not decode 'true' as a Bool") + // } + } + + func test_codingOfNil() { + let x: Int? = nil + self.test_codingOf(value: x, toAndFrom: "null") + } + + func test_codingOfInt8() { + self.test_codingOf(value: Int8(-42), toAndFrom: "-42") + } + + func test_codingOfUInt8() { + self.test_codingOf(value: UInt8(42), toAndFrom: "42") + } + + func test_codingOfInt16() { + self.test_codingOf(value: Int16(-30042), toAndFrom: "-30042") + } + + func test_codingOfUInt16() { + self.test_codingOf(value: UInt16(30042), toAndFrom: "30042") + } + + func test_codingOfInt32() { + self.test_codingOf(value: Int32(-2_000_000_042), toAndFrom: "-2000000042") + } + + func test_codingOfUInt32() { + self.test_codingOf(value: UInt32(2_000_000_042), toAndFrom: "2000000042") + } + + func test_codingOfInt64() { + #if !arch(arm) + self.test_codingOf(value: Int64(-9_000_000_000_000_000_042), toAndFrom: "-9000000000000000042") + #endif + } + + func test_codingOfUInt64() { + #if !arch(arm) + self.test_codingOf(value: UInt64(9_000_000_000_000_000_042), toAndFrom: "9000000000000000042") + #endif + } + + func test_codingOfInt() { + let intSize = MemoryLayout.size + switch intSize { + case 4: // 32-bit + self.test_codingOf(value: Int(-2_000_000_042), toAndFrom: "-2000000042") + case 8: // 64-bit + #if arch(arm) + break + #else + self.test_codingOf(value: Int(-9_000_000_000_000_000_042), toAndFrom: "-9000000000000000042") + #endif + default: + XCTFail("Unexpected UInt size: \(intSize)") + } + } + + func test_codingOfUInt() { + let uintSize = MemoryLayout.size + switch uintSize { + case 4: // 32-bit + self.test_codingOf(value: UInt(2_000_000_042), toAndFrom: "2000000042") + case 8: // 64-bit + #if arch(arm) + break + #else + self.test_codingOf(value: UInt(9_000_000_000_000_000_042), toAndFrom: "9000000000000000042") + #endif + default: + XCTFail("Unexpected UInt size: \(uintSize)") + } + } + + func test_codingOfFloat() { + self.test_codingOf(value: Float(1.5), toAndFrom: "1.5") + + // Check value too large fails to decode. + // XCTAssertThrowsError(try YAMLDecoder().decode(Float.self, from: "1e100".data(using: .utf8)!)) + } + + func test_codingOfDouble() { + self.test_codingOf(value: Double(1.5), toAndFrom: "1.5") + + // Check value too large fails to decode. + // XCTAssertThrowsError(try YAMLDecoder().decode(Double.self, from: "100e323".data(using: .utf8)!)) + } + + func test_codingOfDecimal() { + self.test_codingOf(value: Decimal.pi, toAndFrom: "3.14159265358979323846264338327950288419") + + // Check value too large fails to decode. + // XCTAssertThrowsError(try YAMLDecoder().decode(Decimal.self, from: "100e200".data(using: .utf8)!)) + } + + func test_codingOfString() { + self.test_codingOf(value: "Hello, world!", toAndFrom: "Hello, world!") + } + + func test_codingOfURL() { + self.test_codingOf(value: URL(string: "https://swift.org")!, toAndFrom: "https://swift.org") + } + + // UInt and Int + func test_codingOfUIntMinMax() { + struct MyValue: Codable, Equatable { + var int64Min = Int64.min + var int64Max = Int64.max + var uint64Min = UInt64.min + var uint64Max = UInt64.max + } + + let myValue = MyValue() + self._testRoundTrip( + of: myValue, + expectedYAML: [ + "uint64Min: 0", + "uint64Max: 18446744073709551615", + "int64Min: -9223372036854775808", + "int64Max: 9223372036854775807", + ] + ) + } + + func test_CamelCaseEncoding() throws { + struct MyTestData: Codable, Equatable { + let thisIsAString: String + let thisIsABool: Bool + let thisIsAnInt: Int + let thisIsAnInt8: Int8 + let thisIsAnInt16: Int16 + let thisIsAnInt32: Int32 + let thisIsAnInt64: Int64 + let thisIsAUint: UInt + let thisIsAUint8: UInt8 + let thisIsAUint16: UInt16 + let thisIsAUint32: UInt32 + let thisIsAUint64: UInt64 + let thisIsAFloat: Float + let thisIsADouble: Double + let thisIsADate: Date + let thisIsAnArray: [Int] + let thisIsADictionary: [String: Bool] + } + + let data = MyTestData( + thisIsAString: "Hello", + thisIsABool: true, + thisIsAnInt: 1, + thisIsAnInt8: 2, + thisIsAnInt16: 3, + thisIsAnInt32: 4, + thisIsAnInt64: 5, + thisIsAUint: 6, + thisIsAUint8: 7, + thisIsAUint16: 8, + thisIsAUint32: 9, + thisIsAUint64: 10, + thisIsAFloat: 11, + thisIsADouble: 12, + thisIsADate: Date(timeIntervalSince1970: 0), + thisIsAnArray: [1, 2, 3], + thisIsADictionary: ["trueValue": true, "falseValue": false] + ) + + let encoder = YAMLEncoder() + encoder.keyEncodingStrategy = .camelCase + encoder.dateEncodingStrategy = .iso8601 + let encodedData = try encoder.encode(data) + let yaml = String(decoding: encodedData, as: UTF8.self) + + XCTAssertTrue(yaml.contains("ThisIsABool: true")) + XCTAssertTrue(yaml.contains("ThisIsAnInt: 1")) + XCTAssertTrue(yaml.contains("ThisIsAnInt8: 2")) + XCTAssertTrue(yaml.contains("ThisIsAnInt16: 3")) + XCTAssertTrue(yaml.contains("ThisIsAnInt32: 4")) + XCTAssertTrue(yaml.contains("ThisIsAnInt64: 5")) + XCTAssertTrue(yaml.contains("ThisIsAUint: 6")) + XCTAssertTrue(yaml.contains("ThisIsAUint8: 7")) + XCTAssertTrue(yaml.contains("ThisIsAUint16: 8")) + XCTAssertTrue(yaml.contains("ThisIsAUint32: 9")) + XCTAssertTrue(yaml.contains("ThisIsAUint64: 10")) + XCTAssertTrue(yaml.contains("ThisIsAFloat: 11")) + XCTAssertTrue(yaml.contains("ThisIsADouble: 12")) + XCTAssertTrue(yaml.contains("ThisIsADate: 1970-01-01T00:00:00Z")) + XCTAssertTrue(yaml.contains("ThisIsAnArray:")) + XCTAssertTrue(yaml.contains("- 1")) + XCTAssertTrue(yaml.contains("- 2")) + XCTAssertTrue(yaml.contains("- 3")) + } + + func test_DictionaryCamelCaseEncoding() throws { + let camelCaseDictionary = ["camelCaseKey": ["nestedDictionary": 1]] + + let encoder = YAMLEncoder() + encoder.keyEncodingStrategy = .camelCase + let encodedData = try encoder.encode(camelCaseDictionary) + + let yaml = String(decoding: encodedData, as: UTF8.self) + + print(yaml) + XCTAssertTrue(yaml.contains("CamelCaseKey:")) + XCTAssertTrue(yaml.contains(" NestedDictionary: 1")) + } + + func test_OutputFormattingValues() { + XCTAssertEqual(YAMLEncoder.OutputFormatting.withoutEscapingSlashes.rawValue, 8) + } + + func test_SR17581_codingEmptyDictionaryWithNonstringKeyDoesRoundtrip() throws { + struct Something: Codable { + struct Key: Codable, Hashable { + var x: String + } + + var dict: [Key: String] + + enum CodingKeys: String, CodingKey { + case dict + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.dict = try container.decode([Key: String].self, forKey: .dict) + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.dict, forKey: .dict) + } + + init(dict: [Key: String]) { + self.dict = dict + } + } + + // let toEncode = Something(dict: [:]) + // let data = try YAMLEncoder().encode(toEncode) + // let result = try YAMLDecoder().decode(Something.self, from: data) + // XCTAssertEqual(result.dict.count, 0) + } + + // MARK: - Helper Functions + + private var _yamlEmptyDictionary: [String] { + [""] + } + + private func _testEncodeFailure(of value: T) { + do { + _ = try YAMLEncoder().encode(value) + XCTFail("Encode of top-level \(T.self) was expected to fail.") + } catch {} + } + + private func _testRoundTrip( + of value: T, + expectedYAML yaml: [String] = [], + dateEncodingStrategy: YAMLEncoder.DateEncodingStrategy = .deferredToDate, + dataEncodingStrategy: YAMLEncoder.DataEncodingStrategy = .base64, + nonConformingFloatEncodingStrategy: YAMLEncoder.NonConformingFloatEncodingStrategy = .throw + ) where T: Codable, T: Equatable { + var payload: Data! = nil + do { + let encoder = YAMLEncoder() + encoder.dateEncodingStrategy = dateEncodingStrategy + encoder.dataEncodingStrategy = dataEncodingStrategy + encoder.nonConformingFloatEncodingStrategy = nonConformingFloatEncodingStrategy + payload = try encoder.encode(value) + } catch { + XCTFail("Failed to encode \(T.self) to YAML: \(error)") + } + + // We do not compare expectedYAML to payload directly, because they might have values like + // {"name": "Bob", "age": 22} + // and + // {"age": 22, "name": "Bob"} + // which if compared as Data would not be equal, but the contained YAML values are equal. + // So we wrap them in a YAML type, which compares data as if it were a json. + + let payloadYAMLObject = String(decoding: payload, as: UTF8.self) + let result = yaml.allSatisfy { payloadYAMLObject.contains($0) || $0 == "" } + XCTAssertTrue(result, "Produced YAML not identical to expected YAML.") + + if !result { + print("===========") + print(payloadYAMLObject) + print("-----------") + print(yaml.filter { !payloadYAMLObject.contains($0) }.compactMap { $0 }) + print("===========") + } + } + + func test_codingOf(value: T, toAndFrom stringValue: String) { + self._testRoundTrip( + of: TopLevelObjectWrapper(value), + expectedYAML: ["value: \(stringValue)"] + ) + + self._testRoundTrip( + of: TopLevelArrayWrapper(value), + expectedYAML: ["\(stringValue)"] + ) + } } // MARK: - Helper Global Functions + func expectEqualPaths(_ lhs: [CodingKey?], _ rhs: [CodingKey?], _ prefix: String) { - if lhs.count != rhs.count { - XCTFail("\(prefix) [CodingKey?].count mismatch: \(lhs.count) != \(rhs.count)") - return - } - - for (k1, k2) in zip(lhs, rhs) { - switch (k1, k2) { - case (nil, nil): continue - case (let _k1?, nil): - XCTFail("\(prefix) CodingKey mismatch: \(type(of: _k1)) != nil") - return - case (nil, let _k2?): - XCTFail("\(prefix) CodingKey mismatch: nil != \(type(of: _k2))") - return - default: break - } - - let key1 = k1! - let key2 = k2! - - switch (key1.intValue, key2.intValue) { - case (nil, nil): break - case (let i1?, nil): - XCTFail("\(prefix) CodingKey.intValue mismatch: \(type(of: key1))(\(i1)) != nil") - return - case (nil, let i2?): - XCTFail("\(prefix) CodingKey.intValue mismatch: nil != \(type(of: key2))(\(i2))") - return - case (let i1?, let i2?): - guard i1 == i2 else { - XCTFail( - "\(prefix) CodingKey.intValue mismatch: \(type(of: key1))(\(i1)) != \(type(of: key2))(\(i2))" - ) + if lhs.count != rhs.count { + XCTFail("\(prefix) [CodingKey?].count mismatch: \(lhs.count) != \(rhs.count)") return - } } - XCTAssertEqual( - key1.stringValue, - key2.stringValue, - "\(prefix) CodingKey.stringValue mismatch: \(type(of: key1))('\(key1.stringValue)') != \(type(of: key2))('\(key2.stringValue)')" - ) - } + for (k1, k2) in zip(lhs, rhs) { + switch (k1, k2) { + case (nil, nil): continue + case (let _k1?, nil): + XCTFail("\(prefix) CodingKey mismatch: \(type(of: _k1)) != nil") + return + case (nil, let _k2?): + XCTFail("\(prefix) CodingKey mismatch: nil != \(type(of: _k2))") + return + default: break + } + + let key1 = k1! + let key2 = k2! + + switch (key1.intValue, key2.intValue) { + case (nil, nil): break + case (let i1?, nil): + XCTFail("\(prefix) CodingKey.intValue mismatch: \(type(of: key1))(\(i1)) != nil") + return + case (nil, let i2?): + XCTFail("\(prefix) CodingKey.intValue mismatch: nil != \(type(of: key2))(\(i2))") + return + case (let i1?, let i2?): + guard i1 == i2 else { + XCTFail( + "\(prefix) CodingKey.intValue mismatch: \(type(of: key1))(\(i1)) != \(type(of: key2))(\(i2))" + ) + return + } + } + + XCTAssertEqual( + key1.stringValue, + key2.stringValue, + "\(prefix) CodingKey.stringValue mismatch: \(type(of: key1))('\(key1.stringValue)') != \(type(of: key2))('\(key2.stringValue)')" + ) + } } // MARK: - Test Types + /* FIXME: Import from %S/Inputs/Coding/SharedTypes.swift somehow. */ // MARK: - Empty Types + private struct EmptyStruct: Codable, Equatable { - static func == (_ lhs: EmptyStruct, _ rhs: EmptyStruct) -> Bool { - return true - } + static func == (_: EmptyStruct, _: EmptyStruct) -> Bool { + true + } } private class EmptyClass: Codable, Equatable { - static func == (_ lhs: EmptyClass, _ rhs: EmptyClass) -> Bool { - return true - } + static func == (_: EmptyClass, _: EmptyClass) -> Bool { + true + } } // MARK: - Single-Value Types + /// A simple on-off switch type that encodes as a single Bool value. private enum Switch: Codable { - case off - case on - - init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - switch try container.decode(Bool.self) { - case false: self = .off - case true: self = .on + case off + case on + + init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + switch try container.decode(Bool.self) { + case false: self = .off + case true: self = .on + } } - } - func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() - switch self { - case .off: try container.encode(false) - case .on: try container.encode(true) + func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + switch self { + case .off: try container.encode(false) + case .on: try container.encode(true) + } } - } } /// A simple timestamp type that encodes as a single Double value. private struct Timestamp: Codable, Equatable { - let value: Double + let value: Double - init(_ value: Double) { - self.value = value - } + init(_ value: Double) { + self.value = value + } - init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - value = try container.decode(Double.self) - } + init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + self.value = try container.decode(Double.self) + } - func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() - try container.encode(self.value) - } + func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(self.value) + } - static func == (_ lhs: Timestamp, _ rhs: Timestamp) -> Bool { - return lhs.value == rhs.value - } + static func == (_ lhs: Timestamp, _ rhs: Timestamp) -> Bool { + lhs.value == rhs.value + } } /// A simple referential counter type that encodes as a single Int value. private final class Counter: Codable, Equatable { - var count: Int = 0 + var count: Int = 0 - init() {} + init() {} - init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - count = try container.decode(Int.self) - } + init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + self.count = try container.decode(Int.self) + } - func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() - try container.encode(self.count) - } + func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(self.count) + } - static func == (_ lhs: Counter, _ rhs: Counter) -> Bool { - return lhs === rhs || lhs.count == rhs.count - } + static func == (_ lhs: Counter, _ rhs: Counter) -> Bool { + lhs === rhs || lhs.count == rhs.count + } } // MARK: - Structured Types + /// A simple address type that encodes as a dictionary of values. private struct Address: Codable, Equatable { - let street: String - let city: String - let state: String - let zipCode: Int - let country: String - - init(street: String, city: String, state: String, zipCode: Int, country: String) { - self.street = street - self.city = city - self.state = state - self.zipCode = zipCode - self.country = country - } - - static func == (_ lhs: Address, _ rhs: Address) -> Bool { - return lhs.street == rhs.street && lhs.city == rhs.city && lhs.state == rhs.state - && lhs.zipCode == rhs.zipCode && lhs.country == rhs.country - } - - static var testValue: Address { - return Address( - street: "1 Infinite Loop", - city: "Cupertino", - state: "CA", - zipCode: 95014, - country: "United States") - } + let street: String + let city: String + let state: String + let zipCode: Int + let country: String + + init(street: String, city: String, state: String, zipCode: Int, country: String) { + self.street = street + self.city = city + self.state = state + self.zipCode = zipCode + self.country = country + } + + static func == (_ lhs: Address, _ rhs: Address) -> Bool { + lhs.street == rhs.street && lhs.city == rhs.city && lhs.state == rhs.state + && lhs.zipCode == rhs.zipCode && lhs.country == rhs.country + } + + static var testValue: Address { + Address( + street: "1 Infinite Loop", + city: "Cupertino", + state: "CA", + zipCode: 95014, + country: "United States" + ) + } } /// A simple person class that encodes as a dictionary of values. private class Person: Codable, Equatable { - let name: String - let email: String - - // FIXME: This property is present only in order to test the expected result of Codable synthesis in the compiler. - // We want to test against expected encoded output (to ensure this generates an encodeIfPresent call), but we need an output format for that. - // Once we have a VerifyingEncoder for compiler unit tests, we should move this test there. - let website: URL? - - init(name: String, email: String, website: URL? = nil) { - self.name = name - self.email = email - self.website = website - } - - static func == (_ lhs: Person, _ rhs: Person) -> Bool { - return lhs.name == rhs.name && lhs.email == rhs.email && lhs.website == rhs.website - } - - static var testValue: Person { - return Person(name: "Johnny Appleseed", email: "appleseed@apple.com") - } + let name: String + let email: String + + // FIXME: This property is present only in order to test the expected result of Codable synthesis in the compiler. + // We want to test against expected encoded output (to ensure this generates an encodeIfPresent call), but we need an output format for that. + // Once we have a VerifyingEncoder for compiler unit tests, we should move this test there. + let website: URL? + + init(name: String, email: String, website: URL? = nil) { + self.name = name + self.email = email + self.website = website + } + + static func == (_ lhs: Person, _ rhs: Person) -> Bool { + lhs.name == rhs.name && lhs.email == rhs.email && lhs.website == rhs.website + } + + static var testValue: Person { + Person(name: "Johnny Appleseed", email: "appleseed@apple.com") + } } /// A simple company struct which encodes as a dictionary of nested values. private struct Company: Codable, Equatable { - let address: Address - var employees: [Person] + let address: Address + var employees: [Person] - init(address: Address, employees: [Person]) { - self.address = address - self.employees = employees - } + init(address: Address, employees: [Person]) { + self.address = address + self.employees = employees + } - static func == (_ lhs: Company, _ rhs: Company) -> Bool { - return lhs.address == rhs.address && lhs.employees == rhs.employees - } + static func == (_ lhs: Company, _ rhs: Company) -> Bool { + lhs.address == rhs.address && lhs.employees == rhs.employees + } - static var testValue: Company { - return Company(address: Address.testValue, employees: [Person.testValue]) - } + static var testValue: Company { + Company(address: Address.testValue, employees: [Person.testValue]) + } } // MARK: - Helper Types @@ -1016,382 +1049,413 @@ private struct Company: Codable, Equatable { /// A key type which can take on any string or integer value. /// This needs to mirror _YAMLKey. private struct _TestKey: CodingKey { - var stringValue: String - var intValue: Int? - - init?(stringValue: String) { - self.stringValue = stringValue - self.intValue = nil - } - - init?(intValue: Int) { - self.stringValue = "\(intValue)" - self.intValue = intValue - } - - init(index: Int) { - self.stringValue = "Index \(index)" - self.intValue = index - } + var stringValue: String + var intValue: Int? + + init?(stringValue: String) { + self.stringValue = stringValue + self.intValue = nil + } + + init?(intValue: Int) { + self.stringValue = "\(intValue)" + self.intValue = intValue + } + + init(index: Int) { + self.stringValue = "Index \(index)" + self.intValue = index + } } /// Wraps a type T so that it can be encoded at the top level of a payload. private struct TopLevelArrayWrapper: Codable, Equatable where T: Codable, T: Equatable { - let value: T - - init(_ value: T) { - self.value = value - } - - func encode(to encoder: Encoder) throws { - var container = encoder.unkeyedContainer() - try container.encode(value) - } - - init(from decoder: Decoder) throws { - var container = try decoder.unkeyedContainer() - value = try container.decode(T.self) - assert(container.isAtEnd) - } - - static func == (_ lhs: TopLevelArrayWrapper, _ rhs: TopLevelArrayWrapper) -> Bool { - return lhs.value == rhs.value - } + let value: T + + init(_ value: T) { + self.value = value + } + + func encode(to encoder: Encoder) throws { + var container = encoder.unkeyedContainer() + try container.encode(self.value) + } + + init(from decoder: Decoder) throws { + var container = try decoder.unkeyedContainer() + self.value = try container.decode(T.self) + assert(container.isAtEnd) + } + + static func == (_ lhs: TopLevelArrayWrapper, _ rhs: TopLevelArrayWrapper) -> Bool { + lhs.value == rhs.value + } } private struct FloatNaNPlaceholder: Codable, Equatable { - init() {} - - func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() - try container.encode(Float.nan) - } - - init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - let float = try container.decode(Float.self) - if !float.isNaN { - throw DecodingError.dataCorrupted( - DecodingError.Context( - codingPath: decoder.codingPath, debugDescription: "Couldn't decode NaN.")) - } - } - - static func == (_ lhs: FloatNaNPlaceholder, _ rhs: FloatNaNPlaceholder) -> Bool { - return true - } + init() {} + + func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(Float.nan) + } + + init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + let float = try container.decode(Float.self) + if !float.isNaN { + throw DecodingError.dataCorrupted( + DecodingError.Context( + codingPath: decoder.codingPath, debugDescription: "Couldn't decode NaN." + )) + } + } + + static func == (_: FloatNaNPlaceholder, _: FloatNaNPlaceholder) -> Bool { + true + } } private struct DoubleNaNPlaceholder: Codable, Equatable { - init() {} - - func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() - try container.encode(Double.nan) - } - - init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - let double = try container.decode(Double.self) - if !double.isNaN { - throw DecodingError.dataCorrupted( - DecodingError.Context( - codingPath: decoder.codingPath, debugDescription: "Couldn't decode NaN.")) - } - } - - static func == (_ lhs: DoubleNaNPlaceholder, _ rhs: DoubleNaNPlaceholder) -> Bool { - return true - } + init() {} + + func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(Double.nan) + } + + init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + let double = try container.decode(Double.self) + if !double.isNaN { + throw DecodingError.dataCorrupted( + DecodingError.Context( + codingPath: decoder.codingPath, debugDescription: "Couldn't decode NaN." + )) + } + } + + static func == (_: DoubleNaNPlaceholder, _: DoubleNaNPlaceholder) -> Bool { + true + } } /// A type which encodes as an array directly through a single value container. struct Numbers: Codable, Equatable { - let values = [4, 8, 15, 16, 23, 42] - - init() {} - - init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - let decodedValues = try container.decode([Int].self) - guard decodedValues == values else { - throw DecodingError.dataCorrupted( - DecodingError.Context( - codingPath: decoder.codingPath, debugDescription: "The Numbers are wrong!")) + let values = [4, 8, 15, 16, 23, 42] + + init() {} + + init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + let decodedValues = try container.decode([Int].self) + guard decodedValues == self.values else { + throw DecodingError.dataCorrupted( + DecodingError.Context( + codingPath: decoder.codingPath, debugDescription: "The Numbers are wrong!" + )) + } } - } - func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() - try container.encode(values) - } + func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(self.values) + } - static func == (_ lhs: Numbers, _ rhs: Numbers) -> Bool { - return lhs.values == rhs.values - } + static func == (_ lhs: Numbers, _ rhs: Numbers) -> Bool { + lhs.values == rhs.values + } - static var testValue: Numbers { - return Numbers() - } + static var testValue: Numbers { + Numbers() + } } /// A type which encodes as a dictionary directly through a single value container. private final class Mapping: Codable, Equatable { - let values: [String: URL] - - init(values: [String: URL]) { - self.values = values - } - - init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - values = try container.decode([String: URL].self) - } - - func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() - try container.encode(values) - } - - static func == (_ lhs: Mapping, _ rhs: Mapping) -> Bool { - return lhs === rhs || lhs.values == rhs.values - } - - static var testValue: Mapping { - return Mapping(values: [ - "Apple": URL(string: "http://apple.com")!, - "localhost": URL(string: "http://127.0.0.1")!, - ]) - } + let values: [String: URL] + + init(values: [String: URL]) { + self.values = values + } + + init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + self.values = try container.decode([String: URL].self) + } + + func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(self.values) + } + + static func == (_ lhs: Mapping, _ rhs: Mapping) -> Bool { + lhs === rhs || lhs.values == rhs.values + } + + static var testValue: Mapping { + Mapping(values: [ + "Apple": URL(string: "http://apple.com")!, + "localhost": URL(string: "http://127.0.0.1")!, + ]) + } } struct NestedContainersTestType: Encodable { - let testSuperEncoder: Bool - - init(testSuperEncoder: Bool = false) { - self.testSuperEncoder = testSuperEncoder - } - - enum TopLevelCodingKeys: Int, CodingKey { - case a - case b - case c - } - - enum IntermediateCodingKeys: Int, CodingKey { - case one - case two - } - - func encode(to encoder: Encoder) throws { - if self.testSuperEncoder { - var topLevelContainer = encoder.container(keyedBy: TopLevelCodingKeys.self) - expectEqualPaths(encoder.codingPath, [], "Top-level Encoder's codingPath changed.") - expectEqualPaths( - topLevelContainer.codingPath, [], - "New first-level keyed container has non-empty codingPath.") - - let superEncoder = topLevelContainer.superEncoder(forKey: .a) - expectEqualPaths(encoder.codingPath, [], "Top-level Encoder's codingPath changed.") - expectEqualPaths( - topLevelContainer.codingPath, [], "First-level keyed container's codingPath changed.") - expectEqualPaths( - superEncoder.codingPath, [TopLevelCodingKeys.a], - "New superEncoder had unexpected codingPath.") - _testNestedContainers(in: superEncoder, baseCodingPath: [TopLevelCodingKeys.a]) - } else { - _testNestedContainers(in: encoder, baseCodingPath: []) - } - } - - func _testNestedContainers(in encoder: Encoder, baseCodingPath: [CodingKey?]) { - expectEqualPaths(encoder.codingPath, baseCodingPath, "New encoder has non-empty codingPath.") - - // codingPath should not change upon fetching a non-nested container. - var firstLevelContainer = encoder.container(keyedBy: TopLevelCodingKeys.self) - expectEqualPaths(encoder.codingPath, baseCodingPath, "Top-level Encoder's codingPath changed.") - expectEqualPaths( - firstLevelContainer.codingPath, baseCodingPath, - "New first-level keyed container has non-empty codingPath.") - - // Nested Keyed Container - do { - // Nested container for key should have a new key pushed on. - var secondLevelContainer = firstLevelContainer.nestedContainer( - keyedBy: IntermediateCodingKeys.self, forKey: .a) - expectEqualPaths( - encoder.codingPath, baseCodingPath, "Top-level Encoder's codingPath changed.") - expectEqualPaths( - firstLevelContainer.codingPath, baseCodingPath, - "First-level keyed container's codingPath changed.") - expectEqualPaths( - secondLevelContainer.codingPath, baseCodingPath + [TopLevelCodingKeys.a], - "New second-level keyed container had unexpected codingPath.") - - // Inserting a keyed container should not change existing coding paths. - let thirdLevelContainerKeyed = secondLevelContainer.nestedContainer( - keyedBy: IntermediateCodingKeys.self, forKey: .one) - expectEqualPaths( - encoder.codingPath, baseCodingPath, "Top-level Encoder's codingPath changed.") - expectEqualPaths( - firstLevelContainer.codingPath, baseCodingPath, - "First-level keyed container's codingPath changed.") - expectEqualPaths( - secondLevelContainer.codingPath, baseCodingPath + [TopLevelCodingKeys.a], - "Second-level keyed container's codingPath changed.") - expectEqualPaths( - thirdLevelContainerKeyed.codingPath, - baseCodingPath + [TopLevelCodingKeys.a, IntermediateCodingKeys.one], - "New third-level keyed container had unexpected codingPath.") - - // Inserting an unkeyed container should not change existing coding paths. - let thirdLevelContainerUnkeyed = secondLevelContainer.nestedUnkeyedContainer(forKey: .two) - expectEqualPaths( - encoder.codingPath, baseCodingPath + [], "Top-level Encoder's codingPath changed.") - expectEqualPaths( - firstLevelContainer.codingPath, baseCodingPath + [], - "First-level keyed container's codingPath changed.") - expectEqualPaths( - secondLevelContainer.codingPath, baseCodingPath + [TopLevelCodingKeys.a], - "Second-level keyed container's codingPath changed.") - expectEqualPaths( - thirdLevelContainerUnkeyed.codingPath, - baseCodingPath + [TopLevelCodingKeys.a, IntermediateCodingKeys.two], - "New third-level unkeyed container had unexpected codingPath.") - } - - // Nested Unkeyed Container - do { - // Nested container for key should have a new key pushed on. - var secondLevelContainer = firstLevelContainer.nestedUnkeyedContainer(forKey: .b) - expectEqualPaths( - encoder.codingPath, baseCodingPath, "Top-level Encoder's codingPath changed.") - expectEqualPaths( - firstLevelContainer.codingPath, baseCodingPath, - "First-level keyed container's codingPath changed.") - expectEqualPaths( - secondLevelContainer.codingPath, baseCodingPath + [TopLevelCodingKeys.b], - "New second-level keyed container had unexpected codingPath.") - - // Appending a keyed container should not change existing coding paths. - let thirdLevelContainerKeyed = secondLevelContainer.nestedContainer( - keyedBy: IntermediateCodingKeys.self) - expectEqualPaths( - encoder.codingPath, baseCodingPath, "Top-level Encoder's codingPath changed.") - expectEqualPaths( - firstLevelContainer.codingPath, baseCodingPath, - "First-level keyed container's codingPath changed.") - expectEqualPaths( - secondLevelContainer.codingPath, baseCodingPath + [TopLevelCodingKeys.b], - "Second-level unkeyed container's codingPath changed.") - expectEqualPaths( - thirdLevelContainerKeyed.codingPath, - baseCodingPath + [TopLevelCodingKeys.b, _TestKey(index: 0)], - "New third-level keyed container had unexpected codingPath.") - - // Appending an unkeyed container should not change existing coding paths. - let thirdLevelContainerUnkeyed = secondLevelContainer.nestedUnkeyedContainer() - expectEqualPaths( - encoder.codingPath, baseCodingPath, "Top-level Encoder's codingPath changed.") - expectEqualPaths( - firstLevelContainer.codingPath, baseCodingPath, - "First-level keyed container's codingPath changed.") - expectEqualPaths( - secondLevelContainer.codingPath, baseCodingPath + [TopLevelCodingKeys.b], - "Second-level unkeyed container's codingPath changed.") - expectEqualPaths( - thirdLevelContainerUnkeyed.codingPath, - baseCodingPath + [TopLevelCodingKeys.b, _TestKey(index: 1)], - "New third-level unkeyed container had unexpected codingPath.") - } - } + let testSuperEncoder: Bool + + init(testSuperEncoder: Bool = false) { + self.testSuperEncoder = testSuperEncoder + } + + enum TopLevelCodingKeys: Int, CodingKey { + case a + case b + case c + } + + enum IntermediateCodingKeys: Int, CodingKey { + case one + case two + } + + func encode(to encoder: Encoder) throws { + if self.testSuperEncoder { + var topLevelContainer = encoder.container(keyedBy: TopLevelCodingKeys.self) + expectEqualPaths(encoder.codingPath, [], "Top-level Encoder's codingPath changed.") + expectEqualPaths( + topLevelContainer.codingPath, [], + "New first-level keyed container has non-empty codingPath." + ) + + let superEncoder = topLevelContainer.superEncoder(forKey: .a) + expectEqualPaths(encoder.codingPath, [], "Top-level Encoder's codingPath changed.") + expectEqualPaths( + topLevelContainer.codingPath, [], "First-level keyed container's codingPath changed." + ) + expectEqualPaths( + superEncoder.codingPath, [TopLevelCodingKeys.a], + "New superEncoder had unexpected codingPath." + ) + self._testNestedContainers(in: superEncoder, baseCodingPath: [TopLevelCodingKeys.a]) + } else { + self._testNestedContainers(in: encoder, baseCodingPath: []) + } + } + + func _testNestedContainers(in encoder: Encoder, baseCodingPath: [CodingKey?]) { + expectEqualPaths(encoder.codingPath, baseCodingPath, "New encoder has non-empty codingPath.") + + // codingPath should not change upon fetching a non-nested container. + var firstLevelContainer = encoder.container(keyedBy: TopLevelCodingKeys.self) + expectEqualPaths(encoder.codingPath, baseCodingPath, "Top-level Encoder's codingPath changed.") + expectEqualPaths( + firstLevelContainer.codingPath, baseCodingPath, + "New first-level keyed container has non-empty codingPath." + ) + + // Nested Keyed Container + do { + // Nested container for key should have a new key pushed on. + var secondLevelContainer = firstLevelContainer.nestedContainer( + keyedBy: IntermediateCodingKeys.self, forKey: .a + ) + expectEqualPaths( + encoder.codingPath, baseCodingPath, "Top-level Encoder's codingPath changed." + ) + expectEqualPaths( + firstLevelContainer.codingPath, baseCodingPath, + "First-level keyed container's codingPath changed." + ) + expectEqualPaths( + secondLevelContainer.codingPath, baseCodingPath + [TopLevelCodingKeys.a], + "New second-level keyed container had unexpected codingPath." + ) + + // Inserting a keyed container should not change existing coding paths. + let thirdLevelContainerKeyed = secondLevelContainer.nestedContainer( + keyedBy: IntermediateCodingKeys.self, forKey: .one + ) + expectEqualPaths( + encoder.codingPath, baseCodingPath, "Top-level Encoder's codingPath changed." + ) + expectEqualPaths( + firstLevelContainer.codingPath, baseCodingPath, + "First-level keyed container's codingPath changed." + ) + expectEqualPaths( + secondLevelContainer.codingPath, baseCodingPath + [TopLevelCodingKeys.a], + "Second-level keyed container's codingPath changed." + ) + expectEqualPaths( + thirdLevelContainerKeyed.codingPath, + baseCodingPath + [TopLevelCodingKeys.a, IntermediateCodingKeys.one], + "New third-level keyed container had unexpected codingPath." + ) + + // Inserting an unkeyed container should not change existing coding paths. + let thirdLevelContainerUnkeyed = secondLevelContainer.nestedUnkeyedContainer(forKey: .two) + expectEqualPaths( + encoder.codingPath, baseCodingPath + [], "Top-level Encoder's codingPath changed." + ) + expectEqualPaths( + firstLevelContainer.codingPath, baseCodingPath + [], + "First-level keyed container's codingPath changed." + ) + expectEqualPaths( + secondLevelContainer.codingPath, baseCodingPath + [TopLevelCodingKeys.a], + "Second-level keyed container's codingPath changed." + ) + expectEqualPaths( + thirdLevelContainerUnkeyed.codingPath, + baseCodingPath + [TopLevelCodingKeys.a, IntermediateCodingKeys.two], + "New third-level unkeyed container had unexpected codingPath." + ) + } + + // Nested Unkeyed Container + do { + // Nested container for key should have a new key pushed on. + var secondLevelContainer = firstLevelContainer.nestedUnkeyedContainer(forKey: .b) + expectEqualPaths( + encoder.codingPath, baseCodingPath, "Top-level Encoder's codingPath changed." + ) + expectEqualPaths( + firstLevelContainer.codingPath, baseCodingPath, + "First-level keyed container's codingPath changed." + ) + expectEqualPaths( + secondLevelContainer.codingPath, baseCodingPath + [TopLevelCodingKeys.b], + "New second-level keyed container had unexpected codingPath." + ) + + // Appending a keyed container should not change existing coding paths. + let thirdLevelContainerKeyed = secondLevelContainer.nestedContainer( + keyedBy: IntermediateCodingKeys.self) + expectEqualPaths( + encoder.codingPath, baseCodingPath, "Top-level Encoder's codingPath changed." + ) + expectEqualPaths( + firstLevelContainer.codingPath, baseCodingPath, + "First-level keyed container's codingPath changed." + ) + expectEqualPaths( + secondLevelContainer.codingPath, baseCodingPath + [TopLevelCodingKeys.b], + "Second-level unkeyed container's codingPath changed." + ) + expectEqualPaths( + thirdLevelContainerKeyed.codingPath, + baseCodingPath + [TopLevelCodingKeys.b, _TestKey(index: 0)], + "New third-level keyed container had unexpected codingPath." + ) + + // Appending an unkeyed container should not change existing coding paths. + let thirdLevelContainerUnkeyed = secondLevelContainer.nestedUnkeyedContainer() + expectEqualPaths( + encoder.codingPath, baseCodingPath, "Top-level Encoder's codingPath changed." + ) + expectEqualPaths( + firstLevelContainer.codingPath, baseCodingPath, + "First-level keyed container's codingPath changed." + ) + expectEqualPaths( + secondLevelContainer.codingPath, baseCodingPath + [TopLevelCodingKeys.b], + "Second-level unkeyed container's codingPath changed." + ) + expectEqualPaths( + thirdLevelContainerUnkeyed.codingPath, + baseCodingPath + [TopLevelCodingKeys.b, _TestKey(index: 1)], + "New third-level unkeyed container had unexpected codingPath." + ) + } + } } // MARK: - Helpers private struct YAML: Equatable { - private var jsonObject: Any - - fileprivate init(data: Data) throws { - self.jsonObject = try JSONSerialization.jsonObject(with: data, options: []) - } - - static func == (lhs: YAML, rhs: YAML) -> Bool { - switch (lhs.jsonObject, rhs.jsonObject) { - case let (lhs, rhs) as ([AnyHashable: Any], [AnyHashable: Any]): - return NSDictionary(dictionary: lhs) == NSDictionary(dictionary: rhs) - case let (lhs, rhs) as ([Any], [Any]): - return NSArray(array: lhs) == NSArray(array: rhs) - default: - return false - } - } + private var jsonObject: Any + + fileprivate init(data: Data) throws { + self.jsonObject = try JSONSerialization.jsonObject(with: data, options: []) + } + + static func == (lhs: YAML, rhs: YAML) -> Bool { + switch (lhs.jsonObject, rhs.jsonObject) { + case (let lhs, let rhs) as ([AnyHashable: Any], [AnyHashable: Any]): + return NSDictionary(dictionary: lhs) == NSDictionary(dictionary: rhs) + case (let lhs, let rhs) as ([Any], [Any]): + return NSArray(array: lhs) == NSArray(array: rhs) + default: + return false + } + } } // MARK: - Run Tests extension TestYAMLEncoder { - static var allTests: [(String, (TestYAMLEncoder) -> () throws -> Void)] { - return [ - ("test_encodingTopLevelFragments", test_encodingTopLevelFragments), - ("test_encodingTopLevelEmptyStruct", test_encodingTopLevelEmptyStruct), - ("test_encodingTopLevelEmptyClass", test_encodingTopLevelEmptyClass), - ("test_encodingTopLevelSingleValueEnum", test_encodingTopLevelSingleValueEnum), - ("test_encodingTopLevelSingleValueStruct", test_encodingTopLevelSingleValueStruct), - ("test_encodingTopLevelSingleValueClass", test_encodingTopLevelSingleValueClass), - ("test_encodingTopLevelStructuredStruct", test_encodingTopLevelStructuredStruct), - ("test_encodingTopLevelStructuredClass", test_encodingTopLevelStructuredClass), - ("test_encodingTopLevelStructuredSingleStruct", test_encodingTopLevelStructuredSingleStruct), - ("test_encodingTopLevelStructuredSingleClass", test_encodingTopLevelStructuredSingleClass), - ("test_encodingTopLevelDeepStructuredType", test_encodingTopLevelDeepStructuredType), - ("test_encodingOutputFormattingDefault", test_encodingOutputFormattingDefault), - ("test_encodingOutputFormattingPrettyPrinted", test_encodingOutputFormattingPrettyPrinted), - ("test_encodingOutputFormattingSortedKeys", test_encodingOutputFormattingSortedKeys), - ( - "test_encodingOutputFormattingPrettyPrintedSortedKeys", - test_encodingOutputFormattingPrettyPrintedSortedKeys - ), - ("test_encodingDate", test_encodingDate), - ("test_encodingDateSecondsSince1970", test_encodingDateSecondsSince1970), - ("test_encodingDateMillisecondsSince1970", test_encodingDateMillisecondsSince1970), - ("test_encodingDateISO8601", test_encodingDateISO8601), - ("test_encodingDateFormatted", test_encodingDateFormatted), - ("test_encodingDateCustom", test_encodingDateCustom), - ("test_encodingDateCustomEmpty", test_encodingDateCustomEmpty), - ("test_encodingBase64Data", test_encodingBase64Data), - ("test_encodingCustomData", test_encodingCustomData), - ("test_encodingCustomDataEmpty", test_encodingCustomDataEmpty), - ("test_encodingNonConformingFloats", test_encodingNonConformingFloats), - ("test_encodingNonConformingFloatStrings", test_encodingNonConformingFloatStrings), - // ("test_encodeDecodeNumericTypesBaseline", test_encodeDecodeNumericTypesBaseline), - ("test_nestedContainerCodingPaths", test_nestedContainerCodingPaths), - ("test_superEncoderCodingPaths", test_superEncoderCodingPaths), - ("test_notFoundSuperDecoder", test_notFoundSuperDecoder), - ("test_codingOfBool", test_codingOfBool), - ("test_codingOfNil", test_codingOfNil), - ("test_codingOfInt8", test_codingOfInt8), - ("test_codingOfUInt8", test_codingOfUInt8), - ("test_codingOfInt16", test_codingOfInt16), - ("test_codingOfUInt16", test_codingOfUInt16), - ("test_codingOfInt32", test_codingOfInt32), - ("test_codingOfUInt32", test_codingOfUInt32), - ("test_codingOfInt64", test_codingOfInt64), - ("test_codingOfUInt64", test_codingOfUInt64), - ("test_codingOfInt", test_codingOfInt), - ("test_codingOfUInt", test_codingOfUInt), - ("test_codingOfFloat", test_codingOfFloat), - ("test_codingOfDouble", test_codingOfDouble), - ("test_codingOfDecimal", test_codingOfDecimal), - ("test_codingOfString", test_codingOfString), - ("test_codingOfURL", test_codingOfURL), - ("test_codingOfUIntMinMax", test_codingOfUIntMinMax), - ("test_snake_case_encoding", test_CamelCaseEncoding), - ("test_dictionary_snake_case_encoding", test_DictionaryCamelCaseEncoding), - ("test_OutputFormattingValues", test_OutputFormattingValues), - ( - "test_SR17581_codingEmptyDictionaryWithNonstringKeyDoesRoundtrip", - test_SR17581_codingEmptyDictionaryWithNonstringKeyDoesRoundtrip - ), - ] - } + static var allTests: [(String, (TestYAMLEncoder) -> () throws -> Void)] { + [ + ("test_encodingTopLevelFragments", test_encodingTopLevelFragments), + ("test_encodingTopLevelEmptyStruct", test_encodingTopLevelEmptyStruct), + ("test_encodingTopLevelEmptyClass", test_encodingTopLevelEmptyClass), + ("test_encodingTopLevelSingleValueEnum", test_encodingTopLevelSingleValueEnum), + ("test_encodingTopLevelSingleValueStruct", test_encodingTopLevelSingleValueStruct), + ("test_encodingTopLevelSingleValueClass", test_encodingTopLevelSingleValueClass), + ("test_encodingTopLevelStructuredStruct", test_encodingTopLevelStructuredStruct), + ("test_encodingTopLevelStructuredClass", test_encodingTopLevelStructuredClass), + ("test_encodingTopLevelStructuredSingleStruct", test_encodingTopLevelStructuredSingleStruct), + ("test_encodingTopLevelStructuredSingleClass", test_encodingTopLevelStructuredSingleClass), + ("test_encodingTopLevelDeepStructuredType", test_encodingTopLevelDeepStructuredType), + ("test_encodingOutputFormattingDefault", test_encodingOutputFormattingDefault), + ("test_encodingOutputFormattingPrettyPrinted", test_encodingOutputFormattingPrettyPrinted), + ("test_encodingOutputFormattingSortedKeys", test_encodingOutputFormattingSortedKeys), + ( + "test_encodingOutputFormattingPrettyPrintedSortedKeys", + test_encodingOutputFormattingPrettyPrintedSortedKeys + ), + ("test_encodingDate", test_encodingDate), + ("test_encodingDateSecondsSince1970", test_encodingDateSecondsSince1970), + ("test_encodingDateMillisecondsSince1970", test_encodingDateMillisecondsSince1970), + ("test_encodingDateISO8601", test_encodingDateISO8601), + ("test_encodingDateFormatted", test_encodingDateFormatted), + ("test_encodingDateCustom", test_encodingDateCustom), + ("test_encodingDateCustomEmpty", test_encodingDateCustomEmpty), + ("test_encodingBase64Data", test_encodingBase64Data), + ("test_encodingCustomData", test_encodingCustomData), + ("test_encodingCustomDataEmpty", test_encodingCustomDataEmpty), + ("test_encodingNonConformingFloats", test_encodingNonConformingFloats), + ("test_encodingNonConformingFloatStrings", test_encodingNonConformingFloatStrings), + // ("test_encodeDecodeNumericTypesBaseline", test_encodeDecodeNumericTypesBaseline), + ("test_nestedContainerCodingPaths", test_nestedContainerCodingPaths), + ("test_superEncoderCodingPaths", test_superEncoderCodingPaths), + ("test_notFoundSuperDecoder", test_notFoundSuperDecoder), + ("test_codingOfBool", test_codingOfBool), + ("test_codingOfNil", test_codingOfNil), + ("test_codingOfInt8", test_codingOfInt8), + ("test_codingOfUInt8", test_codingOfUInt8), + ("test_codingOfInt16", test_codingOfInt16), + ("test_codingOfUInt16", test_codingOfUInt16), + ("test_codingOfInt32", test_codingOfInt32), + ("test_codingOfUInt32", test_codingOfUInt32), + ("test_codingOfInt64", test_codingOfInt64), + ("test_codingOfUInt64", test_codingOfUInt64), + ("test_codingOfInt", test_codingOfInt), + ("test_codingOfUInt", test_codingOfUInt), + ("test_codingOfFloat", test_codingOfFloat), + ("test_codingOfDouble", test_codingOfDouble), + ("test_codingOfDecimal", test_codingOfDecimal), + ("test_codingOfString", test_codingOfString), + ("test_codingOfURL", test_codingOfURL), + ("test_codingOfUIntMinMax", test_codingOfUIntMinMax), + ("test_snake_case_encoding", test_CamelCaseEncoding), + ("test_dictionary_snake_case_encoding", test_DictionaryCamelCaseEncoding), + ("test_OutputFormattingValues", test_OutputFormattingValues), + ( + "test_SR17581_codingEmptyDictionaryWithNonstringKeyDoesRoundtrip", + test_SR17581_codingEmptyDictionaryWithNonstringKeyDoesRoundtrip + ), + ] + } }