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 63750ab..cd7392c 100644 --- a/Examples/SAM/Package.swift +++ b/Examples/SAM/Package.swift @@ -18,58 +18,58 @@ 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"), - .package(url: "../../../swift-aws-lambda-sam-dsl", 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"), + .package(url: "../../../swift-aws-lambda-sam-dsl", 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 @@ -77,6 +77,6 @@ if ProcessInfo.processInfo.environment["LAMBDA_USE_LOCAL_DEPS"] != nil { package.dependencies = [ // use local package in ../.. .package(name: "swift-aws-lambda-runtime", path: "../../../swift-aws-lambda-runtime"), - .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 031ec4f..93c0f2e 100644 --- a/Package.swift +++ b/Package.swift @@ -15,12 +15,12 @@ let package = Package( .library(name: "AWSLambdaDeploymentDescriptor", type: .dynamic, targets: ["AWSLambdaDeploymentDescriptor"]), ], dependencies: [ - .package(url: "https://github.com/jpsim/Yams.git", from: "5.1.2") + .package(url: "https://github.com/jpsim/Yams.git", from: "5.1.2"), ], targets: [ .target( name: "AWSLambdaDeploymentDescriptor", - dependencies: [ .product(name: "Yams", package: "Yams")], + dependencies: [.product(name: "Yams", package: "Yams")], path: "Sources/AWSLambdaDeploymentDescriptor", swiftSettings: [.enableExperimentalFeature("StrictConcurrency=complete")] ), diff --git a/Package@swift-5.10.swift b/Package@swift-5.10.swift index 4242830..b42a035 100644 --- a/Package@swift-5.10.swift +++ b/Package@swift-5.10.swift @@ -15,15 +15,30 @@ let package = Package( .library(name: "AWSLambdaDeploymentDescriptor", type: .dynamic, targets: ["AWSLambdaDeploymentDescriptor"]), ], dependencies: [ - .package(url: "https://github.com/jpsim/Yams.git", from: "5.1.2") + .package(url: "https://github.com/apple/swift-syntax.git", branch: "main"), + .package(url: "https://github.com/jpsim/Yams.git", from: "5.1.2"), ], targets: [ .target( name: "AWSLambdaDeploymentDescriptor", - dependencies: [ .product(name: "Yams", package: "Yams")], + dependencies: [.product(name: "Yams", package: "Yams")], path: "Sources/AWSLambdaDeploymentDescriptor", - swiftSettings: [.enableExperimentalFeature("StrictConcurrency=complete")] + swiftSettings: [.enableExperimentalFeature("StrictConcurrency=complete")] ), + + // JSON Schema Generator + .executableTarget( + name: "AWSLambdaDeploymentDescriptorGenerator", + dependencies: [ + .target(name: "AWSLambdaDeploymentDescriptor"), + .product(name: "SwiftSyntax", package: "swift-syntax"), + .product(name: "SwiftSyntaxBuilder", package: "swift-syntax"), + ], + path: "Sources/AWSLambdaDeploymentDescriptorGenerator", + exclude: ["Generated", "Resources"], + swiftSettings: [.enableExperimentalFeature("StrictConcurrency=complete")] + ), + .plugin( name: "AWSLambdaDeployer", capability: .command( @@ -39,7 +54,16 @@ let package = Package( dependencies: [ .byName(name: "AWSLambdaDeploymentDescriptor"), ], - swiftSettings: [.enableExperimentalFeature("StrictConcurrency=complete")] + swiftSettings: [.enableExperimentalFeature("StrictConcurrency=complete")] + ), + + // test the SAM JSON Schema reader + .testTarget( + name: "AWSLambdaDeploymentDescriptorGeneratorTests", + dependencies: [ + .byName(name: "AWSLambdaDeploymentDescriptorGenerator"), + ], + swiftSettings: [.enableExperimentalFeature("StrictConcurrency=complete")] ), ] ) diff --git a/Plugins/AWSLambdaDeployer/Plugin.swift b/Plugins/AWSLambdaDeployer/Plugin.swift index 2dbe1e6..9b0054b 100644 --- a/Plugins/AWSLambdaDeployer/Plugin.swift +++ b/Plugins/AWSLambdaDeployer/Plugin.swift @@ -19,13 +19,12 @@ 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 #if swift(>=6.0) let samDeploymentDescriptorFilePath = "\(context.package.directoryURL)/template.yaml" @@ -36,32 +35,32 @@ struct AWSLambdaDeployer: CommandPlugin { 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 #if swift(>=6.0) try self.compileSharedLibrary(projectDirectory: context.package.directoryURL, - buildConfiguration: configuration.buildConfiguration, - swiftExecutable: swiftExecutablePath, - verboseLogging: configuration.verboseLogging) + buildConfiguration: configuration.buildConfiguration, + swiftExecutable: swiftExecutablePath, + verboseLogging: configuration.verboseLogging) #else - try self.compileSharedLibrary(projectDirectory: URL(fileURLWithPath:context.package.directory.string), - buildConfiguration: configuration.buildConfiguration, - swiftExecutable: swiftExecutablePath, - verboseLogging: configuration.verboseLogging) + try self.compileSharedLibrary(projectDirectory: URL(fileURLWithPath: context.package.directory.string), + buildConfiguration: configuration.buildConfiguration, + swiftExecutable: swiftExecutablePath, + verboseLogging: configuration.verboseLogging) #endif // generate the deployment descriptor @@ -73,7 +72,7 @@ struct AWSLambdaDeployer: CommandPlugin { samDeploymentDescriptorFilePath: samDeploymentDescriptorFilePath, archivePath: configuration.archiveDirectory, force: configuration.force, - verboseLogging: configuration.verboseLogging) + verboseLogging: configuration.verboseLogging) #else try self.generateDeploymentDescriptor(projectDirectory: URL(fileURLWithPath: context.package.directory.string), buildConfiguration: configuration.buildConfiguration, @@ -95,18 +94,17 @@ struct AWSLambdaDeployer: CommandPlugin { force: configuration.force, verboseLogging: configuration.verboseLogging) #else - try self.checkOrCreateSAMConfigFile(projetDirectory: URL(fileURLWithPath:context.package.directory.string), + try self.checkOrCreateSAMConfigFile(projetDirectory: URL(fileURLWithPath: context.package.directory.string), buildConfiguration: configuration.buildConfiguration, region: awsRegion, stackName: configuration.stackName, force: configuration.force, verboseLogging: configuration.verboseLogging) - #endif + #endif // validate the template try self.validate(samExecutablePath: samExecutablePath, samDeploymentDescriptorFilePath: samDeploymentDescriptorFilePath, verboseLogging: configuration.verboseLogging) - // deploy the functions if !configuration.noDeploy { @@ -114,18 +112,17 @@ 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) } } - private func compileSharedLibrary(projectDirectory: URL, buildConfiguration: PackageManager.BuildConfiguration, swiftExecutable: URL, @@ -134,15 +131,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: URL, @@ -156,7 +152,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 @@ -164,39 +160,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.absoluteString)\"", "-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 @@ -204,27 +200,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) @@ -234,24 +229,22 @@ struct AWSLambdaDeployer: CommandPlugin { print("Unexpected error : \(error)") throw DeployerPluginError.error(error) } - } - + private func findExecutable(context: PluginContext, executableName: String, helpMessage: String, verboseLogging: Bool) throws -> URL { - guard let executable = try? context.tool(named: executableName) else { print("Can not find `\(executableName)` executable.") print(helpMessage) throw DeployerPluginError.toolNotFound(executableName) } -#if swift(>=6.0) + #if swift(>=6.0) let url = executable.url -#else + #else let url = URL(fileURLWithPath: executable.path.string) -#endif + #endif if verboseLogging { print("-------------------------------------------------------------------------") print("\(executableName) executable : \(url)") @@ -259,23 +252,23 @@ struct AWSLambdaDeployer: CommandPlugin { } return url } - + private func validate(samExecutablePath: URL, 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) @@ -286,56 +279,51 @@ struct AWSLambdaDeployer: CommandPlugin { throw DeployerPluginError.error(error) } } - + private func checkOrCreateSAMConfigFile(projetDirectory: URL, 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: URL, 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) @@ -355,30 +343,28 @@ image_repositories = [] throw DeployerPluginError.error(error) } } - + private func listEndpoints(samExecutablePath: URL, 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 @@ -387,16 +373,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! @@ -413,11 +398,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 @@ -427,14 +411,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! @@ -446,42 +431,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. + """) } } @@ -495,16 +480,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 @@ -516,22 +500,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 @@ -544,11 +528,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 { @@ -558,20 +542,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") @@ -579,29 +563,29 @@ private struct Configuration: CustomStringConvertible { print(self) } } - + var description: String { -#if swift(>=6.0) + #if swift(>=6.0) let pluginDirectoryURL = self.context.pluginWorkDirectoryURL let directoryURL = self.context.package.directoryURL -#else + #else let pluginDirectoryURL = URL(fileURLWithPath: self.context.pluginWorkDirectory.string) let directoryURL = URL(fileURLWithPath: self.context.package.directory.string) -#endif - return """ - { - 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: \(pluginDirectoryURL) - Project directory: \(directoryURL) - } - """ + #endif + return """ + { + 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: \(pluginDirectoryURL) + Project directory: \(directoryURL) + } + """ } } @@ -611,7 +595,7 @@ private enum DeployerPluginError: Error, CustomStringConvertible { case deployswiftDoesNotExist case noRegionFound(String) case error(Error) - + var description: String { switch self { case .invalidArgument(let description): @@ -627,4 +611,3 @@ private enum DeployerPluginError: Error, CustomStringConvertible { } } } - diff --git a/Plugins/AWSLambdaDeployer/PluginUtils.swift b/Plugins/AWSLambdaDeployer/PluginUtils.swift index 45d2519..9299bc3 100644 --- a/Plugins/AWSLambdaDeployer/PluginUtils.swift +++ b/Plugins/AWSLambdaDeployer/PluginUtils.swift @@ -56,7 +56,7 @@ struct Utils { } let pipe = Pipe() - pipe.fileHandleForReading.readabilityHandler = { fileHandle in + pipe.fileHandleForReading.readabilityHandler = { fileHandle in outputQueue.async { outputHandler(fileHandle.availableData) } @@ -70,7 +70,7 @@ struct Utils { if let customWorkingDirectory { process.currentDirectoryURL = customWorkingDirectory } - process.terminationHandler = { _ in + process.terminationHandler = { _ in outputQueue.async { outputHandler(try? pipe.fileHandleForReading.readToEnd()) } @@ -104,6 +104,7 @@ struct Utils { } } } + enum ProcessLogLevel: Comparable { case silent case output(outputIndent: Int) diff --git a/Scripts/ProcessCoverage.swift b/Scripts/ProcessCoverage.swift index d75f3ea..71f0353 100755 --- a/Scripts/ProcessCoverage.swift +++ b/Scripts/ProcessCoverage.swift @@ -1,9 +1,9 @@ -#!/usr/bin/swift +#!/usr/bin/swift -/** -Taken from https://github.com/remko/age-plugin-se/blob/main/Scripts/ProcessCoverage.swift -under MIT License -**/ +/** + Taken from https://github.com/remko/age-plugin-se/blob/main/Scripts/ProcessCoverage.swift + under MIT License + **/ // // Postprocesses an LLVM coverage report, as output by `swift test --enable-coverage`. @@ -23,35 +23,34 @@ let htmlOutputPath = CommandLine.arguments[3] let badgeOutputPath = CommandLine.arguments[4] var report = try JSONDecoder().decode( - CoverageReport.self, - from: try Data(contentsOf: URL(fileURLWithPath: inputPath)) + CoverageReport.self, + from: Data(contentsOf: URL(fileURLWithPath: inputPath)) ) // Filter out data we don't need // Ideally, this wouldn't be necessary, and we could specify not to record coverage for // these files for di in report.data.indices { - report.data[di].files.removeAll(where: { f in - f.filename.contains("Tests/") || f.filename.contains(".build/") - }) - // Update (some) totals - (report.data[di].totals.lines.covered, report.data[di].totals.lines.count) = - report.data[di].files.reduce( - (0, 0), - { acc, next in - ( - acc.0 + next.summary.lines.covered, - acc.1 + next.summary.lines.count - ) - }) - report.data[di].totals.lines.percent = - 100 * Float(report.data[di].totals.lines.covered) / Float(report.data[di].totals.lines.count) + report.data[di].files.removeAll(where: { f in + f.filename.contains("Tests/") || f.filename.contains(".build/") + }) + // Update (some) totals + (report.data[di].totals.lines.covered, report.data[di].totals.lines.count) = + report.data[di].files.reduce( + (0, 0)) { acc, next in + ( + acc.0 + next.summary.lines.covered, + acc.1 + next.summary.lines.count + ) + } + report.data[di].totals.lines.percent = + 100 * Float(report.data[di].totals.lines.covered) / Float(report.data[di].totals.lines.count) } // Write out filtered report -FileManager.default.createFile( - atPath: outputPath, - contents: try JSONEncoder().encode(report) +try FileManager.default.createFile( + atPath: outputPath, + contents: JSONEncoder().encode(report) ) //////////////////////////////////////////////////////////////////////////////// @@ -62,24 +61,27 @@ var totalCovered = 0 var totalCount = 0 print("Code coverage (lines):") for d in report.data { - for f in d.files { - let filename = f.filename.stripPrefix(FileManager.default.currentDirectoryPath + "/") - let lines = String(format: "%d/%d", f.summary.lines.covered, f.summary.lines.count) - let percent = String( - format: "(%.01f%%)", Float(f.summary.lines.covered * 100) / Float(f.summary.lines.count)) - print( - " \(filename.rightPadded(toLength: 24)) \(lines.leftPadded(toLength: 10)) \(percent.leftPadded(toLength: 8))" - ) - } - totalCovered += d.totals.lines.covered - totalCount += d.totals.lines.count + for f in d.files { + let filename = f.filename.stripPrefix(FileManager.default.currentDirectoryPath + "/") + let lines = String(format: "%d/%d", f.summary.lines.covered, f.summary.lines.count) + let percent = String( + format: "(%.01f%%)", Float(f.summary.lines.covered * 100) / Float(f.summary.lines.count) + ) + print( + " \(filename.rightPadded(toLength: 24)) \(lines.leftPadded(toLength: 10)) \(percent.leftPadded(toLength: 8))" + ) + } + totalCovered += d.totals.lines.covered + totalCount += d.totals.lines.count } + let lines = String(format: "%d/%d", totalCovered, totalCount) let percent = String( - format: "(%.01f%%)", Float(totalCovered * 100) / Float(totalCount)) + format: "(%.01f%%)", Float(totalCovered * 100) / Float(totalCount) +) print(" ---") print( - " \("TOTAL".rightPadded(toLength: 24)) \(lines.leftPadded(toLength: 10)) \(percent.leftPadded(toLength: 8))" + " \("TOTAL".rightPadded(toLength: 24)) \(lines.leftPadded(toLength: 10)) \(percent.leftPadded(toLength: 8))" ) //////////////////////////////////////////////////////////////////////////////// @@ -88,44 +90,45 @@ print( let percentRounded = Int((Float(totalCovered * 100) / Float(totalCount)).rounded()) FileManager.default.createFile( - atPath: badgeOutputPath, - contents: Data( - """ - - Coverage - \(percent)% - - - - - - - - - - - - - - - - - - Coverage - - - - - - - \(percentRounded)% - - - - - """.utf8 - )) + atPath: badgeOutputPath, + contents: Data( + """ + + Coverage - \(percent)% + + + + + + + + + + + + + + + + + + Coverage + + + + + + + \(percentRounded)% + + + + + """.utf8 + ) +) //////////////////////////////////////////////////////////////////////////////// // HTML Report @@ -135,69 +138,69 @@ var out = "" var files = "" var fileID = 0 for d in report.data { - for f in d.files { - let filename = f.filename.stripPrefix(FileManager.default.currentDirectoryPath + "/") - let percent = String( - format: "%.01f", Float(f.summary.lines.covered * 100) / Float(f.summary.lines.count)) - files += "" - out += "
"
-    var segments = f.segments
-    for (index, line) in try
-      (String(contentsOfFile: f.filename).split(omittingEmptySubsequences: false) { $0.isNewline })
-      .enumerated()
-    {
-      var l = line
-      var columnOffset = 0
-      while let segment = segments.first {
-        if segment.line != index + 1 {
-          break
-        }
-        var endIndex = l.utf8.index(l.startIndex, offsetBy: segment.column - 1 - columnOffset)
-        if endIndex > l.endIndex {
-          endIndex = l.endIndex
+    for f in d.files {
+        let filename = f.filename.stripPrefix(FileManager.default.currentDirectoryPath + "/")
+        let percent = String(
+            format: "%.01f", Float(f.summary.lines.covered * 100) / Float(f.summary.lines.count)
+        )
+        files += ""
+        out += "
"
+        var segments = f.segments
+        for (index, line) in try
+            (String(contentsOfFile: f.filename).split(omittingEmptySubsequences: false) { $0.isNewline })
+            .enumerated() {
+            var l = line
+            var columnOffset = 0
+            while let segment = segments.first {
+                if segment.line != index + 1 {
+                    break
+                }
+                var endIndex = l.utf8.index(l.startIndex, offsetBy: segment.column - 1 - columnOffset)
+                if endIndex > l.endIndex {
+                    endIndex = l.endIndex
+                }
+                columnOffset = segment.column - 1
+                let spanClass = !segment.hasCount ? "" : segment.count > 0 ? "c" : "nc"
+                out +=
+                    String(l[l.startIndex ..< endIndex]).htmlEscaped
+                    + ""
+                l = l[endIndex ..< l.endIndex]
+                segments.removeFirst(1)
+            }
+            out += String(l).htmlEscaped + "\n"
         }
-        columnOffset = segment.column - 1
-        let spanClass = !segment.hasCount ? "" : segment.count > 0 ? "c" : "nc"
-        out +=
-          String(l[l.startIndex.."
-        l = l[endIndex..
-  
-    
-      
-      Coverage
-      
-    
-    
-      
-  """ + out + """
+    """
+    
+    
+      
+        
+        Coverage
+        
+      
+      
+        
+    """ + out + """