From 1719e6d141c038649fa02b573a71f4e8e805498d Mon Sep 17 00:00:00 2001 From: Andrew Barba Date: Sun, 7 Jul 2024 11:55:44 -0400 Subject: [PATCH 1/9] Switch to HTTPTypes --- Package.resolved | 69 ++-- Package.swift | 14 +- Sources/Vercel/Crypto.swift | 190 ----------- .../{Handlers => }/ExpressHandler.swift | 2 +- Sources/Vercel/Fetch.swift | 58 ++++ Sources/Vercel/Fetch/Fetch.swift | 133 -------- Sources/Vercel/Fetch/FetchRequest.swift | 106 ------ Sources/Vercel/Fetch/FetchResponse.swift | 79 ----- Sources/Vercel/Handlers/RequestHandler.swift | 41 --- Sources/Vercel/IncomingRequest.swift | 62 ++++ Sources/Vercel/InvokeEvent.swift | 20 +- Sources/Vercel/JWT/JWT.swift | 267 --------------- Sources/Vercel/JWT/JWTError.swift | 44 --- ...{Response.swift => OutgoingResponse.swift} | 101 +++--- Sources/Vercel/Request.swift | 97 ------ Sources/Vercel/RequestHandler.swift | 59 ++++ Sources/Vercel/Router/Router.swift | 33 +- Sources/Vercel/Types.swift | 306 ------------------ Sources/Vercel/Utils.swift | 4 +- Sources/VercelVapor/VaporHandler.swift | 42 ++- Tests/VercelTests/ClaimTests.swift | 15 - Tests/VercelTests/JWTTests.swift | 85 ----- 22 files changed, 327 insertions(+), 1500 deletions(-) delete mode 100644 Sources/Vercel/Crypto.swift rename Sources/Vercel/{Handlers => }/ExpressHandler.swift (92%) create mode 100644 Sources/Vercel/Fetch.swift delete mode 100644 Sources/Vercel/Fetch/Fetch.swift delete mode 100644 Sources/Vercel/Fetch/FetchRequest.swift delete mode 100644 Sources/Vercel/Fetch/FetchResponse.swift delete mode 100644 Sources/Vercel/Handlers/RequestHandler.swift create mode 100644 Sources/Vercel/IncomingRequest.swift delete mode 100644 Sources/Vercel/JWT/JWT.swift delete mode 100644 Sources/Vercel/JWT/JWTError.swift rename Sources/Vercel/{Response.swift => OutgoingResponse.swift} (72%) delete mode 100644 Sources/Vercel/Request.swift create mode 100644 Sources/Vercel/RequestHandler.swift delete mode 100644 Sources/Vercel/Types.swift delete mode 100644 Tests/VercelTests/ClaimTests.swift delete mode 100644 Tests/VercelTests/JWTTests.swift diff --git a/Package.resolved b/Package.resolved index 8fd9bec..1f903f7 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,12 +1,13 @@ { + "originHash" : "84f7d17fd7066cb70f9a164cb7eb38ed34d7c9c01d51ee9136540e1b8e32ba71", "pins" : [ { "identity" : "async-http-client", "kind" : "remoteSourceControl", "location" : "https://github.com/swift-server/async-http-client.git", "state" : { - "revision" : "291438696abdd48d2a83b52465c176efbd94512b", - "version" : "1.20.1" + "revision" : "0ae99db85b2b9d1e79b362bd31fd1ffe492f7c47", + "version" : "1.21.2" } }, { @@ -23,8 +24,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/vapor/console-kit.git", "state" : { - "revision" : "a31f44ebfbd15a2cc0fda705279676773ac16355", - "version" : "4.14.1" + "revision" : "9f7932f22ab6f64aafadc14491e694179b7d0f6f", + "version" : "4.14.3" } }, { @@ -32,8 +33,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/vapor/multipart-kit.git", "state" : { - "revision" : "12ee56f25bd3fc4c2d09c2aa16e69de61dc786e8", - "version" : "4.6.0" + "revision" : "a31236f24bfd2ea2f520a74575881f6731d7ae68", + "version" : "4.7.0" } }, { @@ -41,8 +42,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/vapor/routing-kit.git", "state" : { - "revision" : "2a92a7eac411a82fb3a03731be5e76773ebe1b3e", - "version" : "4.9.0" + "revision" : "8c9a227476555c55837e569be71944e02a056b72", + "version" : "4.9.1" } }, { @@ -77,17 +78,17 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-collections.git", "state" : { - "revision" : "94cf62b3ba8d4bed62680a282d4c25f9c63c2efb", - "version" : "1.1.0" + "revision" : "ee97538f5b81ae89698fd95938896dec5217b148", + "version" : "1.1.1" } }, { "identity" : "swift-crypto", "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-crypto", + "location" : "https://github.com/apple/swift-crypto.git", "state" : { - "revision" : "f0525da24dc3c6cbb2b6b338b65042bc91cbc4bb", - "version" : "3.3.0" + "revision" : "46072478ca365fe48370993833cb22de9b41567f", + "version" : "3.5.2" } }, { @@ -95,8 +96,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-http-types", "state" : { - "revision" : "12358d55a3824bd5fed310b999ea8cf83a9a1a65", - "version" : "1.0.3" + "revision" : "1ddbea1ee34354a6a2532c60f98501c35ae8edfa", + "version" : "1.2.0" } }, { @@ -104,8 +105,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-log.git", "state" : { - "revision" : "e97a6fcb1ab07462881ac165fdbb37f067e205d5", - "version" : "1.5.4" + "revision" : "9cb486020ebf03bfa5b5df985387a14a98744537", + "version" : "1.6.1" } }, { @@ -113,8 +114,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-metrics.git", "state" : { - "revision" : "971ba26378ab69c43737ee7ba967a896cb74c0d1", - "version" : "2.4.1" + "revision" : "e0165b53d49b413dd987526b641e05e246782685", + "version" : "2.5.0" } }, { @@ -122,8 +123,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio.git", "state" : { - "revision" : "fc63f0cf4e55a4597407a9fc95b16a2bc44b4982", - "version" : "2.64.0" + "revision" : "fc79798d5a150d61361a27ce0c51169b889e23de", + "version" : "2.68.0" } }, { @@ -131,8 +132,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio-extras.git", "state" : { - "revision" : "a3b640d7dc567225db7c94386a6e71aded1bfa63", - "version" : "1.22.0" + "revision" : "05c36b57453d23ea63785d58a7dbc7b70ba1745e", + "version" : "1.23.0" } }, { @@ -140,8 +141,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio-http2.git", "state" : { - "revision" : "0904bf0feb5122b7e5c3f15db7df0eabe623dd87", - "version" : "1.30.0" + "revision" : "a0224f3d20438635dd59c9fcc593520d80d131d0", + "version" : "1.33.0" } }, { @@ -149,8 +150,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio-ssl.git", "state" : { - "revision" : "7c381eb6083542b124a6c18fae742f55001dc2b5", - "version" : "2.26.0" + "revision" : "2b09805797f21c380f7dc9bedaab3157c5508efb", + "version" : "2.27.0" } }, { @@ -158,8 +159,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio-transport-services.git", "state" : { - "revision" : "6cbe0ed2b394f21ab0d46b9f0c50c6be964968ce", - "version" : "1.20.1" + "revision" : "38ac8221dd20674682148d6451367f89c2652980", + "version" : "1.21.0" } }, { @@ -176,8 +177,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-system.git", "state" : { - "revision" : "025bcb1165deab2e20d4eaba79967ce73013f496", - "version" : "1.2.1" + "revision" : "6a9e38e7bd22a3b8ba80bddf395623cf68f57807", + "version" : "1.3.1" } }, { @@ -185,8 +186,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/vapor/vapor", "state" : { - "revision" : "11cdb29614a5c7f8c5289f3c97b3398c3d89b395", - "version" : "4.92.5" + "revision" : "ebbe71c89aa1b76a0920277760b12be7a2ec7c70", + "version" : "4.102.0" } }, { @@ -199,5 +200,5 @@ } } ], - "version" : 2 + "version" : 3 } diff --git a/Package.swift b/Package.swift index d7166ff..e9c0023 100644 --- a/Package.swift +++ b/Package.swift @@ -1,9 +1,9 @@ -// swift-tools-version: 5.9 +// swift-tools-version: 5.10 import PackageDescription let package = Package( - name: "Vercel", + name: "swift-vercel", platforms: [ .macOS(.v12) ], @@ -13,10 +13,8 @@ let package = Package( .plugin(name: "VercelPackager", targets: ["VercelPackager"]), ], dependencies: [ - .package(url: "https://github.com/apple/swift-crypto", from: "3.0.0"), - .package( - url: "https://github.com/swift-server/swift-aws-lambda-runtime", from: "1.0.0-alpha.2"), - .package(url: "https://github.com/swift-server/async-http-client", from: "1.20.1"), + .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime", from: "1.0.0-alpha.2"), + .package(url: "https://github.com/apple/swift-http-types.git", from: "1.0.0"), .package(url: "https://github.com/vapor/vapor", from: "4.0.0"), ], targets: [ @@ -24,8 +22,8 @@ let package = Package( name: "Vercel", dependencies: [ .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"), - .product(name: "AsyncHTTPClient", package: "async-http-client"), - .product(name: "Crypto", package: "swift-crypto"), + .product(name: "HTTPTypes", package: "swift-http-types"), + .product(name: "HTTPTypesFoundation", package: "swift-http-types"), ], swiftSettings: [ .enableExperimentalFeature("StrictConcurrency") diff --git a/Sources/Vercel/Crypto.swift b/Sources/Vercel/Crypto.swift deleted file mode 100644 index 17f3f3c..0000000 --- a/Sources/Vercel/Crypto.swift +++ /dev/null @@ -1,190 +0,0 @@ -// -// Crypto.swift -// -// -// Created by Andrew Barba on 2/8/23. -// - -import Crypto - -public enum Crypto {} - -// MARK: - Hashing - -extension Crypto { - - public static func hash(_ input: String, using hash: T.Type) -> T.Digest where T: HashFunction { - return T.hash(data: Data(input.utf8)) - } - - public static func hash(_ input: [UInt8], using hash: T.Type) -> T.Digest where T: HashFunction { - return T.hash(data: Data(input)) - } - - public static func hash(_ input: Data, using hash: T.Type) -> T.Digest where T: HashFunction { - return T.hash(data: input) - } - - public static func sha256(_ input: String) -> SHA256.Digest { - return hash(input, using: SHA256.self) - } - - public static func sha256(_ input: [UInt8]) -> SHA256.Digest { - return hash(input, using: SHA256.self) - } - - public static func sha256(_ input: Data) -> SHA256.Digest { - return hash(input, using: SHA256.self) - } - - public static func sha384(_ input: String) -> SHA384.Digest { - return hash(input, using: SHA384.self) - } - - public static func sha384(_ input: [UInt8]) -> SHA384.Digest { - return hash(input, using: SHA384.self) - } - - public static func sha384(_ input: Data) -> SHA384.Digest { - return hash(input, using: SHA384.self) - } - - public static func sha512(_ input: String) -> SHA512.Digest { - return hash(input, using: SHA512.self) - } - - public static func sha512(_ input: [UInt8]) -> SHA512.Digest { - return hash(input, using: SHA512.self) - } - - public static func sha512(_ input: Data) -> SHA512.Digest { - return hash(input, using: SHA512.self) - } -} - -// MARK: - HMAC - -extension Crypto { - public enum Auth { - public enum Hash { - case sha256 - case sha384 - case sha512 - } - - public static func code(for input: String, secret: String, using hash: Hash) -> Data { - let data = Data(input.utf8) - let key = SymmetricKey(data: Data(secret.utf8)) - switch hash { - case .sha256: - return HMAC.authenticationCode(for: data, using: key).data - case .sha384: - return HMAC.authenticationCode(for: data, using: key).data - case .sha512: - return HMAC.authenticationCode(for: data, using: key).data - } - } - - public static func verify(_ input: String, signature: Data, secret: String, using hash: Hash) -> Bool { - let computed = code(for: input, secret: secret, using: hash) - return computed.toHexString() == signature.toHexString() - } - } -} - -// MARK: - ECDSA - -extension Crypto { - public enum ECDSA { - public enum Algorithm { - case p256 - case p384 - case p521 - } - - public static func signature(for input: String, secret: String, using algorithm: Algorithm) throws -> Data { - switch algorithm { - case .p256: - let pk = try P256.Signing.PrivateKey(pemRepresentation: secret) - return try pk.signature(for: Crypto.sha256(input)).rawRepresentation - case .p384: - let pk = try P384.Signing.PrivateKey(pemRepresentation: secret) - return try pk.signature(for: Crypto.sha384(input)).rawRepresentation - case .p521: - let pk = try P521.Signing.PrivateKey(pemRepresentation: secret) - return try pk.signature(for: Crypto.sha512(input)).rawRepresentation - } - } - - public static func verify(_ input: String, signature: Data, key: String, using algorithm: Algorithm) throws -> Bool { - switch algorithm { - case .p256: - let publicKey = try P256.Signing.PublicKey(pemRepresentation: key) - let ecdsaSignature = try P256.Signing.ECDSASignature(rawRepresentation: signature) - return publicKey.isValidSignature(ecdsaSignature, for: Crypto.sha256(input)) - case .p384: - let publicKey = try P384.Signing.PublicKey(pemRepresentation: key) - let ecdsaSignature = try P384.Signing.ECDSASignature(rawRepresentation: signature) - return publicKey.isValidSignature(ecdsaSignature, for: Crypto.sha384(input)) - case .p521: - let publicKey = try P521.Signing.PublicKey(pemRepresentation: key) - let ecdsaSignature = try P521.Signing.ECDSASignature(rawRepresentation: signature) - return publicKey.isValidSignature(ecdsaSignature, for: Crypto.sha512(input)) - } - } - } -} - -// MARK: - Utils - -extension DataProtocol { - public var bytes: [UInt8] { - return .init(self) - } - - public var data: Data { - return .init(self) - } - - public func toHexString() -> String { - return reduce("") {$0 + String(format: "%02x", $1)} - } -} - -extension Digest { - public var bytes: [UInt8] { - return .init(self) - } - - public var data: Data { - return .init(self) - } - - public func toHexString() -> String { - return reduce("") {$0 + String(format: "%02x", $1)} - } -} - -extension HashedAuthenticationCode { - public var bytes: [UInt8] { - return .init(self) - } - - public var data: Data { - return .init(self) - } - - public func toHexString() -> String { - return reduce("") {$0 + String(format: "%02x", $1)} - } -} - -extension Array where Element == UInt8 { - public var data: Data { - return .init(self) - } - - public func toHexString() -> String { - return reduce("") {$0 + String(format: "%02x", $1)} - } -} diff --git a/Sources/Vercel/Handlers/ExpressHandler.swift b/Sources/Vercel/ExpressHandler.swift similarity index 92% rename from Sources/Vercel/Handlers/ExpressHandler.swift rename to Sources/Vercel/ExpressHandler.swift index d682102..c68d213 100644 --- a/Sources/Vercel/Handlers/ExpressHandler.swift +++ b/Sources/Vercel/ExpressHandler.swift @@ -29,7 +29,7 @@ extension ExpressHandler { await Shared.default.setRouter(router) } - public func onRequest(_ req: Request) async throws -> Response { + public func onRequest(_ req: IncomingRequest) async throws -> OutgoingResponse { guard let router = await Shared.default.router else { return .status(.serviceUnavailable).send("Express router not configured") } diff --git a/Sources/Vercel/Fetch.swift b/Sources/Vercel/Fetch.swift new file mode 100644 index 0000000..f09fdd6 --- /dev/null +++ b/Sources/Vercel/Fetch.swift @@ -0,0 +1,58 @@ +// +// Fetch.swift +// +// +// Created by Andrew Barba on 1/22/23. +// + +import Foundation +import HTTPTypes +import HTTPTypesFoundation +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif + +public enum FetchError: Error, Sendable { + case invalidRequest + case invalidResponse + case timeout + case invalidLambdaContext +} + +public struct FetchResponse: Sendable { + let response: HTTPResponse + let data: Data? +} + +public func fetch(_ httpRequest: HTTPRequest) async throws -> FetchResponse { + return try await withCheckedThrowingContinuation { continuation in + guard let urlRequest = URLRequest(httpRequest: httpRequest) else { + continuation.resume(throwing: FetchError.invalidRequest) + return + } + let task = URLSession.shared.dataTask(with: urlRequest) { data, urlResponse, error in + if let error { + continuation.resume(throwing: error) + return + } + guard let response = (urlResponse as? HTTPURLResponse)?.httpResponse else { + continuation.resume(throwing: FetchError.invalidResponse) + return + } + continuation.resume(returning: .init(response: response, data: data)) + } + task.resume() + } +} + +public func fetch(_ url: URL) async throws -> FetchResponse { + let request = HTTPRequest(url: url) + return try await fetch(request) +} + +public func fetch(_ urlPath: String) async throws -> FetchResponse { + guard let url = URL(string: urlPath) else { + throw FetchError.invalidRequest + } + return try await fetch(url) +} diff --git a/Sources/Vercel/Fetch/Fetch.swift b/Sources/Vercel/Fetch/Fetch.swift deleted file mode 100644 index 144eb6d..0000000 --- a/Sources/Vercel/Fetch/Fetch.swift +++ /dev/null @@ -1,133 +0,0 @@ -// -// Fetch.swift -// -// -// Created by Andrew Barba on 1/22/23. -// - -import Foundation -import AsyncHTTPClient - -public enum FetchError: Error, Sendable { - case invalidResponse - case invalidURL - case timeout - case invalidLambdaContext -} - -public func fetch(_ request: FetchRequest) async throws -> FetchResponse { - // Build url components from request url - guard var urlComponents = URLComponents(string: request.url.absoluteString) else { - throw FetchError.invalidURL - } - - // Set default scheme - urlComponents.scheme = urlComponents.scheme ?? "http" - - // Set default host - urlComponents.host = urlComponents.host ?? "localhost" - - // Set default query params - urlComponents.queryItems = urlComponents.queryItems ?? [] - - // Build search params - for (key, value) in request.searchParams { - urlComponents.queryItems?.append(.init(name: key, value: value)) - } - - // Parse final url - guard let url = urlComponents.url else { - throw FetchError.invalidURL - } - - // Set request resources - var httpRequest = HTTPClientRequest(url: url.absoluteString) - - // Set request method - httpRequest.method = .init(rawValue: request.method.rawValue) - - // Set default content type based on body - if let contentType = request.body?.defaultContentType { - let name = HTTPHeaderKey.contentType.rawValue - httpRequest.headers.add(name: name, value: request.headers[name] ?? contentType) - } - - // Set headers - for (key, value) in request.headers { - httpRequest.headers.add(name: key, value: value) - } - - // Write bytes to body - switch request.body { - case .bytes(let bytes): - httpRequest.body = .bytes(bytes) - case .data(let data): - httpRequest.body = .bytes(data) - case .text(let text): - httpRequest.body = .bytes(text.utf8, length: .known(text.utf8.count)) - case .json(let json): - httpRequest.body = .bytes(json) - case .none: - break - } - - let httpClient = request.httpClient ?? HTTPClient.vercelClient - - let response = try await httpClient.execute(httpRequest, timeout: request.timeout ?? .seconds(60)) - - return FetchResponse( - body: response.body, - headers: response.headers.reduce(into: [:]) { $0[$1.name] = $1.value }, - status: .init(response.status.code), - url: url - ) -} - -public func fetch(_ url: URL, _ options: FetchRequest.Options = .options()) async throws -> FetchResponse { - let request = FetchRequest(url, options) - return try await fetch(request) -} - -public func fetch(_ urlPath: String, _ options: FetchRequest.Options = .options()) async throws -> FetchResponse { - guard let url = URL(string: urlPath) else { - throw FetchError.invalidURL - } - let request = FetchRequest(url, options) - return try await fetch(request) -} - -extension HTTPClient { - - fileprivate static let vercelClient = HTTPClient( - eventLoopGroup: HTTPClient.defaultEventLoopGroup, - configuration: .vercelConfiguration - ) -} - -extension HTTPClient.Configuration { - /// The ``HTTPClient/Configuration`` for ``HTTPClient/shared`` which tries to mimic the platform's default or prevalent browser as closely as possible. - /// - /// Don't rely on specific values of this configuration as they're subject to change. You can rely on them being somewhat sensible though. - /// - /// - note: At present, this configuration is nowhere close to a real browser configuration but in case of disagreements we will choose values that match - /// the default browser as closely as possible. - /// - /// Platform's default/prevalent browsers that we're trying to match (these might change over time): - /// - macOS: Safari - /// - iOS: Safari - /// - Android: Google Chrome - /// - Linux (non-Android): Google Chrome - fileprivate static var vercelConfiguration: HTTPClient.Configuration { - // To start with, let's go with these values. Obtained from Firefox's config. - return HTTPClient.Configuration( - certificateVerification: .fullVerification, - redirectConfiguration: .follow(max: 20, allowCycles: false), - timeout: Timeout(connect: .seconds(90), read: .seconds(90)), - connectionPool: .seconds(600), - proxy: nil, - ignoreUncleanSSLShutdown: false, - decompression: .enabled(limit: .ratio(10)), - backgroundActivityLogger: nil - ) - } -} diff --git a/Sources/Vercel/Fetch/FetchRequest.swift b/Sources/Vercel/Fetch/FetchRequest.swift deleted file mode 100644 index a98ec2d..0000000 --- a/Sources/Vercel/Fetch/FetchRequest.swift +++ /dev/null @@ -1,106 +0,0 @@ -// -// FetchRequest.swift -// -// -// Created by Andrew Barba on 1/22/23. -// - -import AsyncHTTPClient -import NIOCore - -public struct FetchRequest: Sendable { - - public var url: URL - - public var method: HTTPMethod - - public var headers: [String: String] - - public var searchParams: [String: String] - - public var body: Body? - - public var timeout: TimeAmount? = nil - - public var httpClient: HTTPClient? = nil - - public init(_ url: URL, _ options: Options = .options()) { - self.url = url - self.method = options.method - self.headers = options.headers - self.searchParams = options.searchParams - self.body = options.body - self.timeout = options.timeout - self.httpClient = options.httpClient - } -} - -extension FetchRequest { - - public struct Options { - - public var method: HTTPMethod = .GET - - public var body: Body? = nil - - public var headers: [String: String] = [:] - - public var searchParams: [String: String] = [:] - - public var timeout: TimeAmount? = nil - - public var httpClient: HTTPClient? = nil - - public static func options( - method: HTTPMethod = .GET, - body: Body? = nil, - headers: [String: String] = [:], - searchParams: [String: String] = [:], - timeout: TimeAmount? = nil, - httpClient: HTTPClient? = nil - ) -> Options { - return Options( - method: method, - body: body, - headers: headers, - searchParams: searchParams, - timeout: timeout, - httpClient: httpClient - ) - } - } -} - -extension FetchRequest { - - public enum Body: Sendable { - case bytes(_ bytes: [UInt8]) - case data(_ data: Data) - case text(_ text: String) - case json(_ json: Data) - - public static func json(_ value: T, encoder: JSONEncoder = .init()) throws -> Body where T: Encodable { - let data = try encoder.encode(value) - return Body.json(data) - } - - public static func json(_ jsonObject: [String: Any]) throws -> Body { - let data = try JSONSerialization.data(withJSONObject: jsonObject, options: []) - return Body.json(data) - } - - public static func json(_ jsonArray: [Any]) throws -> Body { - let data = try JSONSerialization.data(withJSONObject: jsonArray, options: []) - return Body.json(data) - } - - public var defaultContentType: String? { - switch self { - case .json: - return "application/json" - default: - return nil - } - } - } -} diff --git a/Sources/Vercel/Fetch/FetchResponse.swift b/Sources/Vercel/Fetch/FetchResponse.swift deleted file mode 100644 index 6dec1bf..0000000 --- a/Sources/Vercel/Fetch/FetchResponse.swift +++ /dev/null @@ -1,79 +0,0 @@ -// -// FetchResponse.swift -// -// -// Created by Andrew Barba on 1/22/23. -// - -import AsyncHTTPClient -import NIOCore -import NIOFoundationCompat - -public struct FetchResponse: Sendable { - - public let body: HTTPClientResponse.Body - - public let headers: [String: String] - - public let status: Int - - public let url: URL -} - -extension FetchResponse { - - public var ok: Bool { - return status >= 200 && status <= 299 - } -} - -extension FetchResponse { - - public func decode(decoder: JSONDecoder = .init()) async throws -> T where T: Decodable & Sendable { - let bytes = try await self.bytes() - return try decoder.decode(T.self, from: bytes) - } - - public func decode(_ type: T.Type, decoder: JSONDecoder = .init()) async throws -> T where T: Decodable & Sendable { - let bytes = try await self.bytes() - return try decoder.decode(type, from: bytes) - } - - public func json() async throws -> Any { - let bytes = try await self.bytes() - return try JSONSerialization.jsonObject(with: bytes) - } - - public func jsonObject() async throws -> [String: Any] { - let bytes = try await self.bytes() - return try JSONSerialization.jsonObject(with: bytes) as! [String: Any] - } - - public func jsonArray() async throws -> [Any] { - let bytes = try await self.bytes() - return try JSONSerialization.jsonObject(with: bytes) as! [Any] - } - - public func formValues() async throws -> [String: String] { - let query = try await self.text() - let components = URLComponents(string: "?\(query)") - let queryItems = components?.queryItems ?? [] - return queryItems.reduce(into: [:]) { values, item in - values[item.name] = item.value - } - } - - public func text() async throws -> String { - var bytes = try await self.bytes() - return bytes.readString(length: bytes.readableBytes) ?? "" - } - - public func data() async throws -> Data { - var bytes = try await self.bytes() - return bytes.readData(length: bytes.readableBytes) ?? .init() - } - - public func bytes(upTo maxBytes: Int = .max) async throws -> ByteBuffer { - return try await body.collect(upTo: maxBytes) - } -} diff --git a/Sources/Vercel/Handlers/RequestHandler.swift b/Sources/Vercel/Handlers/RequestHandler.swift deleted file mode 100644 index 8cf539f..0000000 --- a/Sources/Vercel/Handlers/RequestHandler.swift +++ /dev/null @@ -1,41 +0,0 @@ -// -// RequestHandler.swift -// -// -// Created by Andrew Barba on 1/21/23. -// - -import AWSLambdaRuntime -import NIOCore - -public protocol RequestHandler: Sendable & EventLoopLambdaHandler where Event == InvokeEvent, Output == Response { - - func onRequest(_ req: Request) async throws -> Response - - static func setup(context: LambdaInitializationContext) async throws - - init() -} - -extension RequestHandler { - - public func handle(_ event: InvokeEvent, context: LambdaContext) -> EventLoopFuture { - return context.eventLoop.makeFutureWithTask { - let data = Data(event.body.utf8) - let payload = try JSONDecoder().decode(InvokeEvent.Payload.self, from: data) - let req = Request(payload, in: context) - return try await Request.$current.withValue(req) { - return try await onRequest(req) - } - } - } - - public static func setup(context: LambdaInitializationContext) async throws {} - - public static func makeHandler(context: LambdaInitializationContext) -> EventLoopFuture { - return context.eventLoop.makeFutureWithTask { - try await setup(context: context) - return Self() - } - } -} diff --git a/Sources/Vercel/IncomingRequest.swift b/Sources/Vercel/IncomingRequest.swift new file mode 100644 index 0000000..f6ea37c --- /dev/null +++ b/Sources/Vercel/IncomingRequest.swift @@ -0,0 +1,62 @@ +// +// IncomingRequest.swift +// +// +// Created by Andrew Barba on 7/7/24. +// + +import AWSLambdaRuntime +import HTTPTypes + +public struct IncomingRequest: Sendable { + + public let request: HTTPRequest + + public let body: String? + + public let context: LambdaContext + + public internal(set) var pathParameters: Parameters = .init() +} + +extension IncomingRequest { + + public var url: URL { + request.url! + } + + public var path: String { + request.url!.path + } + + public var rawPath: String { + request.path! + } + + public var method: HTTPRequest.Method { + request.method + } + + public var headerFields: HTTPFields { + request.headerFields + } +} + +extension IncomingRequest { + + public var vercelID: String { + let field = HTTPField.Name("x-vercel-id")! + return request.headerFields[field] ?? "dev1:dev1::00000-0000000000000-000000000000" + } + + public var vercelClientIPAddress: String { + let field = HTTPField.Name("x-vercel-forwarded-for")! + return request.headerFields[field] ?? "127.0.0.1" + } +} + +extension IncomingRequest { + + @TaskLocal + public static var current: Self? +} diff --git a/Sources/Vercel/InvokeEvent.swift b/Sources/Vercel/InvokeEvent.swift index 609beb5..52d4b5e 100644 --- a/Sources/Vercel/InvokeEvent.swift +++ b/Sources/Vercel/InvokeEvent.swift @@ -6,11 +6,13 @@ // import Foundation +import HTTPTypes +import HTTPTypesFoundation public struct InvokeEvent: Codable, Sendable { public struct Payload: Codable, Sendable { - public let method: HTTPMethod - public let headers: HTTPHeaders + public let method: HTTPRequest.Method + public let headers: HTTPFields public let path: String public let body: String? public let encoding: String? @@ -18,3 +20,17 @@ public struct InvokeEvent: Codable, Sendable { public let body: String } + +public struct InvokeResponse: Codable, Sendable { + public enum Encoding: String, Codable, Sendable { + case base64 + } + + public var statusCode: Int + public var headers: HTTPFields? + public var body: String? + public var encoding: Encoding? + public var cookies: [String]? +} + +extension HTTPRequest.Method: Codable {} diff --git a/Sources/Vercel/JWT/JWT.swift b/Sources/Vercel/JWT/JWT.swift deleted file mode 100644 index a21c525..0000000 --- a/Sources/Vercel/JWT/JWT.swift +++ /dev/null @@ -1,267 +0,0 @@ -// -// JWT.swift -// -// -// Created by Andrew Barba on 11/27/22. -// - -public struct JWT: Sendable { - - public let token: String - - public let algorithm: Algorithm - - public let header: [String: Sendable] - - public let payload: [String: Sendable] - - public let signature: Data - - public func claim(name: String) -> Claim { - return .init(value: payload[name]) - } - - public subscript(key: String) -> Claim { - return claim(name: key) - } - - public init(token: String) throws { - // Verify token parts - let parts = token.components(separatedBy: ".") - guard parts.count == 3 else { - throw JWTError.invalidToken - } - - // Parse header - let header = try decodeJWTPart(parts[0]) - - // Parse algorithm - guard let alg = header["alg"] as? String, let algorithm = Algorithm(rawValue: alg) else { - throw JWTError.unsupportedAlgorithm - } - - // Parse payload - let payload = try decodeJWTPart(parts[1]) - - // Parse signature - let signature = try base64UrlDecode(parts[2]) - - self.header = header - self.payload = payload - self.signature = signature - self.algorithm = algorithm - self.token = token - } - - public init( - claims: [String: Sendable], - secret: String, - algorithm: Algorithm = .hs256, - issuedAt: Date = .init(), - expiresAt: Date? = nil, - issuer: String? = nil, - subject: String? = nil, - identifier: String? = nil - ) throws { - let header: [String: Sendable] = [ - "alg": algorithm.rawValue, - "typ": "JWT" - ] - - var properties: [String: Sendable] = [ - "iat": floor(issuedAt.timeIntervalSince1970) - ] - - if let expiresAt { - properties["exp"] = ceil(expiresAt.timeIntervalSince1970) - } - - if let subject { - properties["sub"] = subject - } - - if let issuer { - properties["iss"] = issuer - } - - if let identifier { - properties["jti"] = identifier - } - - let payload = claims.merging(properties, uniquingKeysWith: { $1 }) - - let _header = try encodeJWTPart(header) - - let _payload = try encodeJWTPart(payload) - - let input = "\(_header).\(_payload)" - - let signature = try generateSignature(input, secret: secret, using: algorithm) - - let _signature = try base64UrlEncode(signature) - - self.header = header - self.payload = payload - self.signature = signature - self.algorithm = algorithm - self.token = "\(_header).\(_payload).\(_signature)" - } -} - -extension JWT { - - public var expiresAt: Date? { - claim(name: "exp").date - } - - public var issuer: String? { - claim(name: "iss").string - } - - public var subject: String? { - claim(name: "sub").string - } - - public var audience: [String]? { - claim(name: "aud").array - } - - public var issuedAt: Date? { - claim(name: "iat").date - } - - public var notBefore: Date? { - claim(name: "nbf").date - } - - public var identifier: String? { - claim(name: "jti").string - } - - public var expired: Bool { - guard let expiresAt = self.expiresAt else { - return false - } - return Date() > expiresAt - } -} - -extension JWT { - - @discardableResult - public func verify( - key: String, - issuer: String? = nil, - subject: String? = nil, - expiration: Bool = true - ) throws -> Self { - // Build input - let input = token.components(separatedBy: ".").prefix(2).joined(separator: ".") - - // Ensure the signatures match - try verifySignature(input, signature: signature, key: key, using: algorithm) - - // Ensure the jwt is not expired - if expiration, self.expired == true { - throw JWTError.expiredToken - } - - // Check for a matching issuer - if let issuer, issuer != self.issuer { - throw JWTError.invalidIssuer - } - - // Check for a matching subject - if let subject, subject != self.subject { - throw JWTError.invalidSubject - } - - return self - } -} - -extension JWT { - public enum Algorithm: String, Sendable { - case hs256 = "HS256" - case hs384 = "HS384" - case hs512 = "HS512" - case es256 = "ES256" - case es384 = "ES384" - case es512 = "ES512" - } -} - -private func decodeJWTPart(_ value: String) throws -> [String: Sendable] { - let bodyData = try base64UrlDecode(value) - guard let json = try JSONSerialization.jsonObject(with: bodyData, options: []) as? [String: Sendable] else { - throw JWTError.invalidJSON - } - return json -} - -private func encodeJWTPart(_ value: [String: Any]) throws -> String { - let data = try JSONSerialization.data(withJSONObject: value, options: [.sortedKeys]) - return try base64UrlEncode(data) -} - -private func generateSignature(_ input: String, secret: String, using algorithm: JWT.Algorithm) throws -> Data { - switch algorithm { - case .hs256: - return Crypto.Auth.code(for: input, secret: secret, using: .sha256) - case .hs384: - return Crypto.Auth.code(for: input, secret: secret, using: .sha384) - case .hs512: - return Crypto.Auth.code(for: input, secret: secret, using: .sha512) - case .es256: - return try Crypto.ECDSA.signature(for: input, secret: secret, using: .p256) - case .es384: - return try Crypto.ECDSA.signature(for: input, secret: secret, using: .p384) - case .es512: - return try Crypto.ECDSA.signature(for: input, secret: secret, using: .p521) - } -} - -private func verifySignature(_ input: String, signature: Data, key: String, using algorithm: JWT.Algorithm) throws { - let verified: Bool - switch algorithm { - case .es256: - verified = try Crypto.ECDSA.verify(input, signature: signature, key: key, using: .p256) - case .es384: - verified = try Crypto.ECDSA.verify(input, signature: signature, key: key, using: .p384) - case .es512: - verified = try Crypto.ECDSA.verify(input, signature: signature, key: key, using: .p521) - case .hs256: - verified = Crypto.Auth.verify(input, signature: signature, secret: key, using: .sha256) - case .hs384: - verified = Crypto.Auth.verify(input, signature: signature, secret: key, using: .sha384) - case .hs512: - verified = Crypto.Auth.verify(input, signature: signature, secret: key, using: .sha512) - } - guard verified else { - throw JWTError.invalidSignature - } -} - -private func base64UrlDecode(_ value: String) throws -> Data { - var base64 = value - .replacingOccurrences(of: "-", with: "+") - .replacingOccurrences(of: "_", with: "/") - let length = Double(base64.lengthOfBytes(using: .utf8)) - let requiredLength = 4 * ceil(length / 4.0) - let paddingLength = Int(requiredLength - length) - if paddingLength > 0 { - base64 += Array(repeating: "=", count: paddingLength).joined() - } - guard let data = Data(base64Encoded: base64, options: .ignoreUnknownCharacters) else { - throw JWTError.invalidBase64URL - } - return data -} - -private func base64UrlEncode(_ value: Data) throws -> String { - return value - .base64EncodedString() - .trimmingCharacters(in: ["="]) - .replacingOccurrences(of: "+", with: "-") - .replacingOccurrences(of: "/", with: "_") -} diff --git a/Sources/Vercel/JWT/JWTError.swift b/Sources/Vercel/JWT/JWTError.swift deleted file mode 100644 index 13620dc..0000000 --- a/Sources/Vercel/JWT/JWTError.swift +++ /dev/null @@ -1,44 +0,0 @@ -// -// JWTError.swift -// -// -// Created by Andrew Barba on 2/6/23. -// - -public enum JWTError: Error { - case invalidToken - case invalidData - case invalidBase64URL - case invalidJSON - case invalidSignature - case invalidIssuer - case invalidSubject - case expiredToken - case unsupportedAlgorithm -} - -extension JWTError: LocalizedError { - - public var errorDescription: String? { - switch self { - case .invalidToken: - return "Invalid token" - case .invalidData: - return "Invalid data" - case .invalidBase64URL: - return "Invalid base64 URL" - case .invalidJSON: - return "Invalid JSON" - case .invalidSignature: - return "Signatures do not match" - case .invalidIssuer: - return "Issuers do not match" - case .invalidSubject: - return "Subjects do not match" - case .expiredToken: - return "Expired token" - case .unsupportedAlgorithm: - return "Unsupported algorithm" - } - } -} diff --git a/Sources/Vercel/Response.swift b/Sources/Vercel/OutgoingResponse.swift similarity index 72% rename from Sources/Vercel/Response.swift rename to Sources/Vercel/OutgoingResponse.swift index 135c769..e125d21 100644 --- a/Sources/Vercel/Response.swift +++ b/Sources/Vercel/OutgoingResponse.swift @@ -5,102 +5,99 @@ // Created by Andrew Barba on 1/21/23. // -public struct Response: Codable, Sendable { - public enum Encoding: String, Codable, Sendable { - case base64 - } +import HTTPTypes - public var statusCode: HTTPResponseStatus - public var headers: HTTPHeaders? +public struct OutgoingResponse: Sendable { + public var response: HTTPResponse + public var body: String? - public var encoding: Encoding? - public var cookies: [String]? + + public var encoding: InvokeResponse.Encoding? public var didSend: Bool { body != nil } public init( - statusCode: HTTPResponseStatus = .ok, - headers: HTTPHeaders? = nil, + status: HTTPResponse.Status = .ok, + headerFields: HTTPFields = [:], body: String? = nil, - encoding: Encoding? = nil, - cookies: [String]? = nil + encoding: InvokeResponse.Encoding? = nil ) { - self.statusCode = statusCode - self.headers = headers + self.response = .init(status: status, headerFields: headerFields) self.body = body self.encoding = encoding - self.cookies = cookies } public func with( - statusCode: HTTPResponseStatus? = nil, - headers: HTTPHeaders? = nil, + status: HTTPResponse.Status? = nil, + headerFields: HTTPFields? = nil, body: String? = nil, - encoding: Encoding? = nil, - cookies: [String]? = nil + encoding: InvokeResponse.Encoding? = nil ) -> Self { return .init( - statusCode: statusCode ?? self.statusCode, - headers: headers ?? self.headers, + status: status ?? self.response.status, + headerFields: headerFields ?? self.response.headerFields, body: body ?? self.body, - encoding: encoding ?? self.encoding, - cookies: cookies ?? self.cookies + encoding: encoding ?? self.encoding ) } } // MARK: - Static Init -extension Response { +extension OutgoingResponse { - public static func status(_ statusCode: HTTPResponseStatus) -> Self { - return .init(statusCode: statusCode) + public static func status(_ statusCode: HTTPResponse.Status) -> Self { + return .init(status: statusCode) } - public static func status(_ statusCode: UInt) -> Self { - return .init(statusCode: .init(code: statusCode)) + public static func status(_ status: Int) -> Self { + return .init(status: .init(code: status)) } } // MARK: - Status -extension Response { +extension OutgoingResponse { - public func status(_ statusCode: HTTPResponseStatus) -> Self { - return with(statusCode: statusCode) + public func status(_ status: HTTPResponse.Status) -> Self { + return with(status: status) } - public func status(_ statusCode: UInt) -> Self { - return with(statusCode: .init(code: statusCode)) + public func status(_ status: Int) -> Self { + return with(status: .init(code: status)) } } // MARK: - Headers -extension Response { +extension OutgoingResponse { public func header(_ key: String) -> String? { - return headers?[key]?.value + guard let field = HTTPField.Name(key) else { + return nil + } + return response.headerFields[field] } - public func header(_ key: HTTPHeaderKey) -> String? { - return header(key.rawValue) + public func header(_ field: HTTPField.Name) -> String? { + return response.headerFields[field] } public func header(_ key: String, _ value: String?) -> Self { - var headers = self.headers ?? [:] - if let value { - headers[key] = .init(value) - } else { - headers[key] = nil + guard let field = HTTPField.Name(key) else { + return self } - return with(headers: headers) + var headerFields = response.headerFields + headerFields[field] = value + return with(headerFields: headerFields) } - public func header(_ key: HTTPHeaderKey, _ value: String?) -> Self { - return header(key.rawValue, value) + public func header(_ field: HTTPField.Name, _ value: String?) -> Self { + var headerFields = response.headerFields + headerFields[field] = value + return with(headerFields: headerFields) } public func contentType(_ value: String) -> Self { @@ -121,7 +118,7 @@ extension Response { // MARK: - Send -extension Response { +extension OutgoingResponse { public func send(_ text: String) -> Self { return with(body: text) @@ -167,10 +164,10 @@ extension Response { public func send() -> Self { if body == nil { - return with(statusCode: .noContent, body: "") + return with(status: .noContent, body: "") } if let body, body.isEmpty { - return with(statusCode: .noContent, body: "") + return with(status: .noContent, body: "") } return self } @@ -178,7 +175,7 @@ extension Response { // MARK: - Redirect -extension Response { +extension OutgoingResponse { public func redirect(_ location: String, permanent: Bool = false) -> Self { return status(permanent ? .permanentRedirect : .temporaryRedirect) @@ -189,11 +186,11 @@ extension Response { // MARK: - Cors -extension Response { +extension OutgoingResponse { public func cors( origin: String = "*", - methods: [HTTPMethod] = [.GET, .HEAD, .PUT, .PATCH, .POST, .DELETE, .QUERY], + methods: [HTTPRequest.Method] = [.get, .head, .put, .patch, .post, .delete], allowHeaders: [String]? = nil, allowCredentials: Bool? = nil, exposeHeaders: [String]? = nil, @@ -210,7 +207,7 @@ extension Response { // MARK: - Cookie -extension Response { +extension OutgoingResponse { public enum CookieOption { public enum SameSite: String { diff --git a/Sources/Vercel/Request.swift b/Sources/Vercel/Request.swift deleted file mode 100644 index 128e89f..0000000 --- a/Sources/Vercel/Request.swift +++ /dev/null @@ -1,97 +0,0 @@ -// -// Request.swift -// -// -// Created by Andrew Barba on 1/21/23. -// - -import AWSLambdaRuntime - -public struct Request: Sendable { - public let context: LambdaContext - public let method: HTTPMethod - public let headers: HTTPHeaders - public let path: String - public let searchParams: [String: String] - public let rawBody: Data? - - /// Private instance var to prevent decodable from failing - public internal(set) var pathParams: Parameters = .init() - - public var body: String? { - if let rawBody = rawBody { - return String(bytes: rawBody, encoding: .utf8) - } - - return nil - } - - public var search: String? { - var components: [(String, String)] = [] - - for (key, value) in searchParams { - if let encodedKey = key.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed), - let encodedValue = value.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) { - components.append((encodedKey, encodedValue)) - } else { - return nil - } - } - - return components.map { "\($0)=\($1)" }.joined(separator: "&") - } - - internal init(_ payload: InvokeEvent.Payload, in context: LambdaContext) { - self.method = payload.method - self.headers = payload.headers - self.path = payload.path - self.context = context - - if let encoding = payload.encoding, let body = payload.body, encoding == "base64" { - rawBody = Data(base64Encoded: body) - } else { - rawBody = payload.body?.data(using: .utf8) - } - - self.searchParams = URLComponents(string: payload.path)? - .queryItems? - .reduce(into: [:]) { $0[$1.name] = $1.value } ?? [:] - } -} - -extension Request { - - public var id: String { - header(.xVercelId) ?? "dev1:dev1::00000-0000000000000-000000000000" - } - - public var host: String { - header(.host) ?? "localhost" - } - - public var userAgent: String { - header(.userAgent) ?? "unknown" - } - - public var clientIPAddress: String { - header(.xVercelForwardedFor) ?? "127.0.0.1" - } - - public var url: URL { - return .init(string: "https://\(host)\(path)")! - } - - public func header(_ key: String) -> String? { - return headers[key]?.value - } - - public func header(_ key: HTTPHeaderKey) -> String? { - return headers[key.rawValue]?.value - } -} - -extension Request { - - @TaskLocal - public static var current: Request? -} diff --git a/Sources/Vercel/RequestHandler.swift b/Sources/Vercel/RequestHandler.swift new file mode 100644 index 0000000..a8bcbbd --- /dev/null +++ b/Sources/Vercel/RequestHandler.swift @@ -0,0 +1,59 @@ +// +// RequestHandler.swift +// +// +// Created by Andrew Barba on 1/21/23. +// + +import AWSLambdaRuntime +import HTTPTypes +import NIOCore + +public protocol RequestHandler: Sendable & EventLoopLambdaHandler where Event == InvokeEvent, Output == InvokeResponse { + + func onRequest(_ req: IncomingRequest) async throws -> OutgoingResponse + + static func setup(context: LambdaInitializationContext) async throws + + init() +} + +extension RequestHandler { + + public func handle(_ event: InvokeEvent, context: LambdaContext) -> EventLoopFuture { + return context.eventLoop.makeFutureWithTask { + let data = Data(event.body.utf8) + let payload = try JSONDecoder().decode(InvokeEvent.Payload.self, from: data) + let hostField = HTTPField.Name("host")! + let request = IncomingRequest( + request: .init( + method: payload.method, + scheme: "https", + authority: payload.headers[hostField], + path: payload.path, + headerFields: payload.headers + ), + body: payload.body, + context: context + ) + return try await IncomingRequest.$current.withValue(request) { + let response = try await onRequest(request) + return .init( + statusCode: response.response.status.code, + headers: response.response.headerFields, + body: response.body, + encoding: response.encoding + ) + } + } + } + + public static func setup(context: LambdaInitializationContext) async throws {} + + public static func makeHandler(context: LambdaInitializationContext) -> EventLoopFuture { + return context.eventLoop.makeFutureWithTask { + try await setup(context: context) + return Self() + } + } +} diff --git a/Sources/Vercel/Router/Router.swift b/Sources/Vercel/Router/Router.swift index 344da33..f6c286e 100644 --- a/Sources/Vercel/Router/Router.swift +++ b/Sources/Vercel/Router/Router.swift @@ -5,9 +5,11 @@ // Created by Andrew Barba on 1/22/23. // +import HTTPTypes + public actor Router { - public typealias Handler = (Request, Response) async throws -> Response + public typealias Handler = (IncomingRequest, OutgoingResponse) async throws -> OutgoingResponse public let prefix: String @@ -21,7 +23,7 @@ public actor Router { } @discardableResult - private func add(method: HTTPMethod, path: String, handler: @escaping Handler) -> Self { + private func add(method: HTTPRequest.Method, path: String, handler: @escaping Handler) -> Self { let pathComponents = path.components(separatedBy: "/").filter { $0.isEmpty == false } let prefixComponents = prefix.components(separatedBy: "/").filter { $0.isEmpty == false } let combinedComponents = [method.rawValue] + prefixComponents + pathComponents @@ -29,9 +31,9 @@ public actor Router { return self } - private func handler(for req: inout Request) -> Handler? { + private func handler(for req: inout IncomingRequest) -> Handler? { let pathComponents = req.url.pathComponents.dropFirst() - return router.route(path: [req.method.rawValue] + pathComponents, parameters: &req.pathParams) + return router.route(path: [req.method.rawValue] + pathComponents, parameters: &req.pathParameters) } } @@ -39,43 +41,44 @@ extension Router { @discardableResult public func get(_ path: String, _ handler: @escaping Handler) -> Self { - add(method: .HEAD, path: path, handler: handler) - return add(method: .GET, path: path, handler: handler) + add(method: .head, path: path, handler: handler) + return add(method: .get, path: path, handler: handler) } @discardableResult public func post(_ path: String, _ handler: @escaping Handler) -> Self { - return add(method: .POST, path: path, handler: handler) + return add(method: .post, path: path, handler: handler) } @discardableResult public func put (_ path: String, _ handler: @escaping Handler) -> Self { - return add(method: .PUT, path: path, handler: handler) + return add(method: .put, path: path, handler: handler) } @discardableResult public func delete(_ path: String, _ handler: @escaping Handler) -> Self { - return add(method: .DELETE, path: path, handler: handler) + return add(method: .delete, path: path, handler: handler) } @discardableResult public func options(_ path: String, _ handler: @escaping Handler) -> Self { - return add(method: .OPTIONS, path: path, handler: handler) + return add(method: .options, path: path, handler: handler) } @discardableResult public func patch(_ path: String, _ handler: @escaping Handler) -> Self { - return add(method: .PATCH, path: path, handler: handler) + return add(method: .patch, path: path, handler: handler) } @discardableResult public func head(_ path: String, _ handler: @escaping Handler) -> Self { - return add(method: .HEAD, path: path, handler: handler) + return add(method: .head, path: path, handler: handler) } @discardableResult public func all(_ path: String, _ handler: @escaping Handler) -> Self { - for method in HTTPMethod.allCases { + let methods: [HTTPRequest.Method] = [.get, .post, .put, .delete, .options, .patch, .head] + for method in methods { add(method: method, path: path, handler: handler) } return self @@ -93,9 +96,9 @@ extension Router { extension Router { - public func run(_ req: Request) async throws -> Response { + public func run(_ req: IncomingRequest) async throws -> OutgoingResponse { // Create base response - var res = Response(statusCode: .ok) + var res = OutgoingResponse(status: .ok) // Run all middleware for middlewareHandler in middleware { diff --git a/Sources/Vercel/Types.swift b/Sources/Vercel/Types.swift deleted file mode 100644 index 85335fa..0000000 --- a/Sources/Vercel/Types.swift +++ /dev/null @@ -1,306 +0,0 @@ -// -// Types.swift -// -// -// Created by Andrew Barba on 1/21/23. -// - -public enum HTTPMethod: String, CaseIterable, Sendable, Codable { - case GET - case HEAD - case POST - case PUT - case DELETE - case CONNECT - case OPTIONS - case TRACE - case PATCH - case QUERY -} - -public typealias HTTPHeaders = [String: HTTPHeaderValue] - -public struct HTTPHeaderValue: Codable, Sendable { - - public let values: [String] - - public var value: String { - values[0] - } - - public init(_ value: String) { - self.values = [value] - } - - public init(_ values: [String]) { - self.values = values - } - - public init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - if let values = try? container.decode([String].self) { - self.values = values - return - } - if let value = try? container.decode(String.self) { - self.values = [value] - return - } - throw DecodingError.dataCorruptedError(in: container, debugDescription: "Failed to decode HTTP header value") - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() - try container.encode(value) - } -} - -public enum HTTPHeaderKey: String, Sendable { - case accept = "accept" - case acceptCharset = "accept-charset" - case acceptEncoding = "accept-encoding" - case acceptLanguage = "accept-language" - case acceptRanges = "accept-ranges" - case accessControlAllowCredentials = "access-control-allow-credentials" - case accessControlAllowHeaders = "access-control-allow-headers" - case accessControlAllowMethods = "access-control-allow-methods" - case accessControlAllowOrigin = "access-control-allow-origin" - case accessControlExposeHeaders = "access-control-expose-headers" - case accessControlMaxAge = "access-control-max-age" - case altSvc = "alt-svc" - case age = "age" - case authorization = "authorization" - case cacheControl = "cache-control" - case cdnCacheControl = "cdn-cache-control" - case connection = "connection" - case contentDisposition = "content-disposition" - case contentEncoding = "content-encoding" - case contentLanguage = "content-language" - case contentLength = "content-length" - case contentRange = "content-range" - case contentSecurityPolicy = "content-security-policy" - case contentType = "content-type" - case cookie = "cookie" - case crossOriginResourcePolicy = "cross-origin-resource-policy" - case date = "date" - case etag = "etag" - case expires = "expires" - case fastlyCacheKey = "fastly-xqd-cache-key" - case forwarded = "forwarded" - case from = "from" - case host = "host" - case keepAlive = "keep-alive" - case lastModified = "last-modified" - case link = "link" - case location = "location" - case pragma = "pragma" - case range = "range" - case referer = "referer" - case refererPolicy = "referer-policy" - case server = "server" - case setCookie = "set-cookie" - case surrogateControl = "surrogate-control" - case surrogateKey = "surrogate-key" - case trailer = "trailer" - case transferEncoding = "transfer-encoding" - case upgrade = "upgrade" - case userAgent = "user-agent" - case vary = "vary" - case vercelCdnCacheControl = "vercel-cdn-cache-control" - case via = "via" - case xCache = "x-cache" - case xCacheHits = "x-cache-hits" - case xCompressHint = "x-compress-hint" - case xVercelForwardedFor = "x-vercel-forwarded-for" - case xVercelId = "x-vercel-id" - - public var stringValue: String { - rawValue - } -} - -public enum HTTPStatus: Int, Sendable { - - // Informational - case `continue` = 100 - case switchingProtocols = 101 - case processing = 102 - - // Success - case ok = 200 - case created = 201 - case accepted = 202 - case nonAuthoritativeInformation = 203 - case noContent = 204 - case resetContent = 205 - case partialContent = 206 - case multiStatus = 207 - case alreadyReported = 208 - case imUsed = 209 - - // Redirection - case multipleChoices = 300 - case movedPermanently = 301 - case found = 302 - case seeOther = 303 - case notModified = 304 - case useProxy = 305 - case switchProxy = 306 - case temporaryRedirect = 307 - case permanentRedirect = 308 - - // Client error - case badRequest = 400 - case unauthorised = 401 - case paymentRequired = 402 - case forbidden = 403 - case notFound = 404 - case methodNotAllowed = 405 - case notAcceptable = 406 - case proxyAuthenticationRequired = 407 - case requestTimeout = 408 - case conflict = 409 - case gone = 410 - case lengthRequired = 411 - case preconditionFailed = 412 - case requestEntityTooLarge = 413 - case requestURITooLong = 414 - case unsupportedMediaType = 415 - case requestedRangeNotSatisfiable = 416 - case expectationFailed = 417 - case iamATeapot = 418 - case authenticationTimeout = 419 - case methodFailureSpringFramework = 420 - case enhanceYourCalmTwitter = 4200 - case unprocessableEntity = 422 - case locked = 423 - case failedDependency = 424 - case methodFailureWebDaw = 4240 - case unorderedCollection = 425 - case upgradeRequired = 426 - case preconditionRequired = 428 - case tooManyRequests = 429 - case requestHeaderFieldsTooLarge = 431 - case noResponseNginx = 444 - case retryWithMicrosoft = 449 - case blockedByWindowsParentalControls = 450 - case redirectMicrosoft = 451 - case unavailableForLegalReasons = 4510 - case requestHeaderTooLargeNginx = 494 - case certErrorNginx = 495 - case noCertNginx = 496 - case httpToHttpsNginx = 497 - case clientClosedRequestNginx = 499 - - // Server error - case internalServerError = 500 - case notImplemented = 501 - case badGateway = 502 - case serviceUnavailable = 503 - case gatewayTimeout = 504 - case httpVersionNotSupported = 505 - case variantAlsoNegotiates = 506 - case insufficientStorage = 507 - case loopDetected = 508 - case bandwidthLimitExceeded = 509 - case notExtended = 510 - case networkAuthenticationRequired = 511 - case connectionTimedOut = 522 - case networkReadTimeoutErrorUnknown = 598 - case networkConnectTimeoutErrorUnknown = 599 -} - -public struct HTTPResponseStatus: Sendable { - public let code: UInt - public let reasonPhrase: String? - - public init(code: UInt, reasonPhrase: String? = nil) { - self.code = code - self.reasonPhrase = reasonPhrase - } - - public static var `continue`: HTTPResponseStatus { HTTPResponseStatus(code: 100) } - public static var switchingProtocols: HTTPResponseStatus { HTTPResponseStatus(code: 101) } - public static var processing: HTTPResponseStatus { HTTPResponseStatus(code: 102) } - public static var earlyHints: HTTPResponseStatus { HTTPResponseStatus(code: 103) } - - public static var ok: HTTPResponseStatus { HTTPResponseStatus(code: 200) } - public static var created: HTTPResponseStatus { HTTPResponseStatus(code: 201) } - public static var accepted: HTTPResponseStatus { HTTPResponseStatus(code: 202) } - public static var nonAuthoritativeInformation: HTTPResponseStatus { HTTPResponseStatus(code: 203) } - public static var noContent: HTTPResponseStatus { HTTPResponseStatus(code: 204) } - public static var resetContent: HTTPResponseStatus { HTTPResponseStatus(code: 205) } - public static var partialContent: HTTPResponseStatus { HTTPResponseStatus(code: 206) } - public static var multiStatus: HTTPResponseStatus { HTTPResponseStatus(code: 207) } - public static var alreadyReported: HTTPResponseStatus { HTTPResponseStatus(code: 208) } - public static var imUsed: HTTPResponseStatus { HTTPResponseStatus(code: 226) } - - public static var multipleChoices: HTTPResponseStatus { HTTPResponseStatus(code: 300) } - public static var movedPermanently: HTTPResponseStatus { HTTPResponseStatus(code: 301) } - public static var found: HTTPResponseStatus { HTTPResponseStatus(code: 302) } - public static var seeOther: HTTPResponseStatus { HTTPResponseStatus(code: 303) } - public static var notModified: HTTPResponseStatus { HTTPResponseStatus(code: 304) } - public static var useProxy: HTTPResponseStatus { HTTPResponseStatus(code: 305) } - public static var temporaryRedirect: HTTPResponseStatus { HTTPResponseStatus(code: 307) } - public static var permanentRedirect: HTTPResponseStatus { HTTPResponseStatus(code: 308) } - - public static var badRequest: HTTPResponseStatus { HTTPResponseStatus(code: 400) } - public static var unauthorized: HTTPResponseStatus { HTTPResponseStatus(code: 401) } - public static var paymentRequired: HTTPResponseStatus { HTTPResponseStatus(code: 402) } - public static var forbidden: HTTPResponseStatus { HTTPResponseStatus(code: 403) } - public static var notFound: HTTPResponseStatus { HTTPResponseStatus(code: 404) } - public static var methodNotAllowed: HTTPResponseStatus { HTTPResponseStatus(code: 405) } - public static var notAcceptable: HTTPResponseStatus { HTTPResponseStatus(code: 406) } - public static var proxyAuthenticationRequired: HTTPResponseStatus { HTTPResponseStatus(code: 407) } - public static var requestTimeout: HTTPResponseStatus { HTTPResponseStatus(code: 408) } - public static var conflict: HTTPResponseStatus { HTTPResponseStatus(code: 409) } - public static var gone: HTTPResponseStatus { HTTPResponseStatus(code: 410) } - public static var lengthRequired: HTTPResponseStatus { HTTPResponseStatus(code: 411) } - public static var preconditionFailed: HTTPResponseStatus { HTTPResponseStatus(code: 412) } - public static var payloadTooLarge: HTTPResponseStatus { HTTPResponseStatus(code: 413) } - public static var uriTooLong: HTTPResponseStatus { HTTPResponseStatus(code: 414) } - public static var unsupportedMediaType: HTTPResponseStatus { HTTPResponseStatus(code: 415) } - public static var rangeNotSatisfiable: HTTPResponseStatus { HTTPResponseStatus(code: 416) } - public static var expectationFailed: HTTPResponseStatus { HTTPResponseStatus(code: 417) } - public static var imATeapot: HTTPResponseStatus { HTTPResponseStatus(code: 418) } - public static var misdirectedRequest: HTTPResponseStatus { HTTPResponseStatus(code: 421) } - public static var unprocessableEntity: HTTPResponseStatus { HTTPResponseStatus(code: 422) } - public static var locked: HTTPResponseStatus { HTTPResponseStatus(code: 423) } - public static var failedDependency: HTTPResponseStatus { HTTPResponseStatus(code: 424) } - public static var upgradeRequired: HTTPResponseStatus { HTTPResponseStatus(code: 426) } - public static var preconditionRequired: HTTPResponseStatus { HTTPResponseStatus(code: 428) } - public static var tooManyRequests: HTTPResponseStatus { HTTPResponseStatus(code: 429) } - public static var requestHeaderFieldsTooLarge: HTTPResponseStatus { HTTPResponseStatus(code: 431) } - public static var unavailableForLegalReasons: HTTPResponseStatus { HTTPResponseStatus(code: 451) } - - public static var internalServerError: HTTPResponseStatus { HTTPResponseStatus(code: 500) } - public static var notImplemented: HTTPResponseStatus { HTTPResponseStatus(code: 501) } - public static var badGateway: HTTPResponseStatus { HTTPResponseStatus(code: 502) } - public static var serviceUnavailable: HTTPResponseStatus { HTTPResponseStatus(code: 503) } - public static var gatewayTimeout: HTTPResponseStatus { HTTPResponseStatus(code: 504) } - public static var httpVersionNotSupported: HTTPResponseStatus { HTTPResponseStatus(code: 505) } - public static var variantAlsoNegotiates: HTTPResponseStatus { HTTPResponseStatus(code: 506) } - public static var insufficientStorage: HTTPResponseStatus { HTTPResponseStatus(code: 507) } - public static var loopDetected: HTTPResponseStatus { HTTPResponseStatus(code: 508) } - public static var notExtended: HTTPResponseStatus { HTTPResponseStatus(code: 510) } - public static var networkAuthenticationRequired: HTTPResponseStatus { HTTPResponseStatus(code: 511) } -} - -extension HTTPResponseStatus: Equatable { - public static func == (lhs: Self, rhs: Self) -> Bool { - lhs.code == rhs.code - } -} - -extension HTTPResponseStatus: Codable { - public init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - self.code = try container.decode(UInt.self) - self.reasonPhrase = nil - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() - try container.encode(self.code) - } -} diff --git a/Sources/Vercel/Utils.swift b/Sources/Vercel/Utils.swift index 88ae74f..de0ac1e 100644 --- a/Sources/Vercel/Utils.swift +++ b/Sources/Vercel/Utils.swift @@ -9,13 +9,13 @@ import Foundation extension CharacterSet { - static let javascriptURLAllowed: CharacterSet = + public static let javascriptURLAllowed: CharacterSet = .alphanumerics.union(.init(charactersIn: "-_.!~*'()")) // as per RFC 3986 } extension DateFormatter { - static let httpDate: DateFormatter = { + public static let httpDate: DateFormatter = { let formatter = DateFormatter() formatter.timeZone = TimeZone(secondsFromGMT: 0) formatter.dateFormat = "EEE, dd MMM yyyy HH:mm:ss z" diff --git a/Sources/VercelVapor/VaporHandler.swift b/Sources/VercelVapor/VaporHandler.swift index f091076..021fb52 100644 --- a/Sources/VercelVapor/VaporHandler.swift +++ b/Sources/VercelVapor/VaporHandler.swift @@ -6,6 +6,7 @@ // import AWSLambdaRuntime +import HTTPTypes import Vapor import Vercel @@ -23,7 +24,7 @@ extension VaporHandler { } public static func setup(context: LambdaInitializationContext) async throws { - let app = Application(environment, .shared(context.eventLoop)) + let app = try await Application.make(environment, .shared(context.eventLoop)) // Request vapor application from user code try await configure(app: app) // Configure vercel server @@ -32,7 +33,7 @@ extension VaporHandler { await Shared.default.setApp(app) } - public func onRequest(_ req: Vercel.Request) async throws -> Vercel.Response { + public func onRequest(_ req: IncomingRequest) async throws -> OutgoingResponse { guard let app = await Shared.default.app else { return .status(.serviceUnavailable).send("Vapor application not configured") } @@ -55,49 +56,44 @@ private actor Shared { extension Vapor.Request { - static func from(request: Vercel.Request, for app: Application) throws -> Self { - let buffer = request.rawBody.map { data in + static func from(request: IncomingRequest, for app: Application) throws -> Self { + let buffer = request.body.map { data in var _buffer = request.context.allocator.buffer(capacity: data.count) - _buffer.writeBytes(data) + _buffer.writeBytes(data.utf8) return _buffer } - let nioHeaders = request.headers.reduce(into: NIOHTTP1.HTTPHeaders()) { - $0.add(name: $1.key, value: $1.value.value) - } - - var url: String = request.path - - if request.searchParams.count > 0, let search = request.search { - url += "?\(search)" + let nioHeaders = request.headerFields.reduce(into: NIOHTTP1.HTTPHeaders()) { + $0.add(name: $1.name.canonicalName, value: $1.value) } return try .init( application: app, method: .init(rawValue: request.method.rawValue), - url: .init(path: url), + url: .init(string: request.rawPath), version: HTTPVersion(major: 1, minor: 1), headers: nioHeaders, collectedBody: buffer, - remoteAddress: .init(ipAddress: request.clientIPAddress, port: 443), + remoteAddress: .init(ipAddress: request.vercelClientIPAddress, port: 443), logger: app.logger, on: app.eventLoopGroup.next() ) } } -extension Vercel.Response { +extension OutgoingResponse { static func from(response: Vapor.Response, on eventLoop: EventLoop) async throws -> Self { // Create status code - let statusCode = Vercel.HTTPResponseStatus( - code: response.status.code, + let status = HTTPResponse.Status( + code: .init(response.status.code), reasonPhrase: response.status.reasonPhrase ) // Create the headers - let headers: [String: HTTPHeaderValue] = response.headers.reduce(into: [:]) { - $0[$1.name] = .init($1.value) + let headerFields: HTTPFields = response.headers.reduce(into: [:]) { + let field = HTTPField.Name($1.name)! + $0[field] = .init($1.value) } // Stream the body to a future @@ -105,9 +101,9 @@ extension Vercel.Response { var buffer = $0 let byteLength = buffer?.readableBytes ?? 0 let bytes = buffer?.readBytes(length: byteLength) - return Vercel.Response( - statusCode: statusCode, - headers: headers, + return OutgoingResponse( + status: status, + headerFields: headerFields, body: bytes?.base64String(), encoding: bytes.map { _ in .base64 } ) diff --git a/Tests/VercelTests/ClaimTests.swift b/Tests/VercelTests/ClaimTests.swift deleted file mode 100644 index 7f95a5b..0000000 --- a/Tests/VercelTests/ClaimTests.swift +++ /dev/null @@ -1,15 +0,0 @@ -import XCTest -@testable import Vercel - -final class ClaimTests: XCTestCase { - func testClaimChaining() async throws { - let claim = Claim(value: [ - "a": [ - "b": [ - "c": 100 - ] - ] - ]) - XCTAssertEqual(claim["a"]["b"]["c"].int, 100) - } -} diff --git a/Tests/VercelTests/JWTTests.swift b/Tests/VercelTests/JWTTests.swift deleted file mode 100644 index c575883..0000000 --- a/Tests/VercelTests/JWTTests.swift +++ /dev/null @@ -1,85 +0,0 @@ -import XCTest -@testable import Vercel - -private let token = -""" -eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2Njk1OTE2MTEsIm5hbWUiOiJKb2huIERvZSIsInN1YiI6IjEyMzQ1Njc4OTAifQ.FUVIl48Ji1mWZa42K1OTG0x_2T0FYOXNACsmeNI2-Kc -""" - -private let fanoutToken = -""" -eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJleHAiOjE2NzUzNjU0MjgsImlzcyI6ImZhc3RseSJ9.QL2Pm1JnXV/vAYK7ijeD4U1CBjOTLihNMDZ+qfvjkKOTUiK1jyxGEwjZfeApijRaOtQT8fVkdPnKjF+tBiUzkA -""" - -public let fanoutPublicKey = - """ - -----BEGIN PUBLIC KEY----- - MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAECKo5A1ebyFcnmVV8SE5On+8G81Jy - BjSvcrx4VLetWCjuDAmppTo3xM/zz763COTCgHfp/6lPdCyYjjqc+GM7sw== - -----END PUBLIC KEY----- - """ - -final class JWTTests: XCTestCase { - - func testVerifySuccess() throws { - let jwt = try JWT(token: token) - try jwt.verify(key: "your-256-bit-secret", expiration: false) - } - - func testVerifyFanoutSuccess() throws { - let jwt = try JWT(token: fanoutToken) - try jwt.verify(key: fanoutPublicKey, expiration: false) - } - - func testVerifyFailure() throws { - let jwt = try JWT(token: token) - try XCTAssertThrowsError(jwt.verify(key: "bogus-secret")) - } - - func testSubject() throws { - let jwt = try JWT(token: token) - XCTAssertNotNil(jwt.subject) - XCTAssertEqual(jwt.subject, "1234567890") - } - - func testClaim() throws { - let jwt = try JWT(token: token) - XCTAssertNotNil(jwt["name"].string) - XCTAssertEqual(jwt["name"].string, "John Doe") - } - - func testCreate() throws { - let jwt = try JWT( - claims: ["name": "John Doe"], - secret: "your-256-bit-secret", - issuedAt: Date(timeIntervalSince1970: 1669591611), - subject: "1234567890" - ) - XCTAssertEqual(jwt.token, token) - } - - func testES256() throws { - let publicKey = """ - -----BEGIN PUBLIC KEY----- - MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEEVs/o5+uQbTjL3chynL4wXgUg2R9 - q9UU8I5mEovUf86QZ7kOBIjJwqnzD1omageEHWwHdBO6B+dFabmdT9POxg== - -----END PUBLIC KEY----- - """ - let privateKey = """ - -----BEGIN PRIVATE KEY----- - MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgevZzL1gdAFr88hb2 - OF/2NxApJCzGCEDdfSp6VQO30hyhRANCAAQRWz+jn65BtOMvdyHKcvjBeBSDZH2r - 1RTwjmYSi9R/zpBnuQ4EiMnCqfMPWiZqB4QdbAd0E7oH50VpuZ1P087G - -----END PRIVATE KEY----- - """ - let jwt1 = try JWT( - claims: ["name": "John Doe"], - secret: privateKey, - algorithm: .es256, - issuedAt: Date(timeIntervalSince1970: 1669591611), - subject: "1234567890" - ) - let jwt2 = try JWT(token: jwt1.token) - try jwt2.verify(key: publicKey, expiration: false) - } -} From 01cdc8f53f9e38f7c5d760e27b17bf231d49dd8a Mon Sep 17 00:00:00 2001 From: Andrew Barba Date: Sun, 7 Jul 2024 12:02:16 -0400 Subject: [PATCH 2/9] Optimize for size --- Plugins/VercelPackager/VercelOutput.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Plugins/VercelPackager/VercelOutput.swift b/Plugins/VercelPackager/VercelOutput.swift index 8acb188..20ac984 100644 --- a/Plugins/VercelPackager/VercelOutput.swift +++ b/Plugins/VercelPackager/VercelOutput.swift @@ -563,7 +563,7 @@ extension VercelOutput { "-v", "\(context.package.directory.string):/workspace", "-w", "/workspace", baseImage, - "bash", "-cl", "swift build -c release --static-swift-stdlib" + "bash", "-cl", "swift build -c release -Xswiftc -Osize --static-swift-stdlib" ] ) From a5caf8f48d04331d6175233dac79ae833ae8dc0f Mon Sep 17 00:00:00 2001 From: Andrew Barba Date: Sun, 7 Jul 2024 12:43:33 -0400 Subject: [PATCH 3/9] Fix header parsing --- Sources/Vercel/InvokeEvent.swift | 4 ++-- Sources/Vercel/RequestHandler.swift | 13 +++++++++---- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/Sources/Vercel/InvokeEvent.swift b/Sources/Vercel/InvokeEvent.swift index 52d4b5e..d77b868 100644 --- a/Sources/Vercel/InvokeEvent.swift +++ b/Sources/Vercel/InvokeEvent.swift @@ -12,7 +12,7 @@ import HTTPTypesFoundation public struct InvokeEvent: Codable, Sendable { public struct Payload: Codable, Sendable { public let method: HTTPRequest.Method - public let headers: HTTPFields + public let headers: [String: String] public let path: String public let body: String? public let encoding: String? @@ -27,7 +27,7 @@ public struct InvokeResponse: Codable, Sendable { } public var statusCode: Int - public var headers: HTTPFields? + public var headers: [String: String]? public var body: String? public var encoding: Encoding? public var cookies: [String]? diff --git a/Sources/Vercel/RequestHandler.swift b/Sources/Vercel/RequestHandler.swift index a8bcbbd..31c2156 100644 --- a/Sources/Vercel/RequestHandler.swift +++ b/Sources/Vercel/RequestHandler.swift @@ -24,23 +24,28 @@ extension RequestHandler { return context.eventLoop.makeFutureWithTask { let data = Data(event.body.utf8) let payload = try JSONDecoder().decode(InvokeEvent.Payload.self, from: data) - let hostField = HTTPField.Name("host")! + let headerFields: HTTPFields = payload.headers.reduce(into: .init()) { + $0.append(.init(name: .init($1.key)!, value: $1.value)) + } let request = IncomingRequest( request: .init( method: payload.method, scheme: "https", - authority: payload.headers[hostField], + authority: payload.headers["host"], path: payload.path, - headerFields: payload.headers + headerFields: headerFields ), body: payload.body, context: context ) return try await IncomingRequest.$current.withValue(request) { let response = try await onRequest(request) + let headers: [String: String] = response.response.headerFields.reduce(into: [:]) { + $0[$1.name.canonicalName] = $1.value + } return .init( statusCode: response.response.status.code, - headers: response.response.headerFields, + headers: headers, body: response.body, encoding: response.encoding ) From 50e830979393b34f33e16691d9ffa6f6ff75faa7 Mon Sep 17 00:00:00 2001 From: Andrew Barba Date: Sun, 7 Jul 2024 12:50:43 -0400 Subject: [PATCH 4/9] Export http types --- Sources/Vercel/Vercel.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Sources/Vercel/Vercel.swift b/Sources/Vercel/Vercel.swift index 99b68db..569a425 100644 --- a/Sources/Vercel/Vercel.swift +++ b/Sources/Vercel/Vercel.swift @@ -6,3 +6,5 @@ // @_exported import Foundation +@_exported import HTTPTypes +@_exported import HTTPTypesFoundation From a888bcacad6859a278855b01ae7d7c00cb08fc42 Mon Sep 17 00:00:00 2001 From: Andrew Barba Date: Sun, 7 Jul 2024 13:02:09 -0400 Subject: [PATCH 5/9] Add back fetch response helpers --- README.md | 2 +- Sources/Vercel/Fetch.swift | 7 +-- Sources/Vercel/FetchResponse.swift | 77 ++++++++++++++++++++++++++++++ 3 files changed, 79 insertions(+), 7 deletions(-) create mode 100644 Sources/Vercel/FetchResponse.swift diff --git a/README.md b/README.md index c293426..c7796d5 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ import Vercel @main struct App: RequestHandler { - func onRequest(_ req: Request) async throws -> Response { + func onRequest(_ req: IncomingRequest) async throws -> OutgoingResponse { let greeting = EdgeConfig.default.get("greeting").string! return .status(.ok).send("Hello, \(greeting)") } diff --git a/Sources/Vercel/Fetch.swift b/Sources/Vercel/Fetch.swift index f09fdd6..9333835 100644 --- a/Sources/Vercel/Fetch.swift +++ b/Sources/Vercel/Fetch.swift @@ -19,11 +19,6 @@ public enum FetchError: Error, Sendable { case invalidLambdaContext } -public struct FetchResponse: Sendable { - let response: HTTPResponse - let data: Data? -} - public func fetch(_ httpRequest: HTTPRequest) async throws -> FetchResponse { return try await withCheckedThrowingContinuation { continuation in guard let urlRequest = URLRequest(httpRequest: httpRequest) else { @@ -39,7 +34,7 @@ public func fetch(_ httpRequest: HTTPRequest) async throws -> FetchResponse { continuation.resume(throwing: FetchError.invalidResponse) return } - continuation.resume(returning: .init(response: response, data: data)) + continuation.resume(returning: .init(response: response, body: data)) } task.resume() } diff --git a/Sources/Vercel/FetchResponse.swift b/Sources/Vercel/FetchResponse.swift new file mode 100644 index 0000000..b4f4f76 --- /dev/null +++ b/Sources/Vercel/FetchResponse.swift @@ -0,0 +1,77 @@ +// +// FetchResponse.swift +// +// +// Created by Andrew Barba on 7/7/24. +// + +import HTTPTypes + +public struct FetchResponse: Sendable { + + public let response: HTTPResponse + + internal let body: Data? +} + +extension FetchResponse { + + public var status: HTTPResponse.Status { + response.status + } + + public var ok: Bool { + return status.code >= 200 && status.code <= 299 + } +} + +extension FetchResponse { + + public func decode(decoder: JSONDecoder = .init()) async throws -> T where T: Decodable & Sendable { + let data = try await data() + return try decoder.decode(T.self, from: data) + } + + public func decode(_ type: T.Type, decoder: JSONDecoder = .init()) async throws -> T where T: Decodable & Sendable { + let data = try await data() + return try decoder.decode(type, from: data) + } + + public func json() async throws -> Any { + let data = try await data() + return try JSONSerialization.jsonObject(with: data) + } + + public func jsonObject() async throws -> [String: Any] { + let data = try await data() + return try JSONSerialization.jsonObject(with: data) as! [String: Any] + } + + public func jsonArray() async throws -> [Any] { + let data = try await data() + return try JSONSerialization.jsonObject(with: data) as! [Any] + } + + public func formValues() async throws -> [String: String] { + let query = try await self.text() + let components = URLComponents(string: "?\(query)") + let queryItems = components?.queryItems ?? [] + return queryItems.reduce(into: [:]) { values, item in + values[item.name] = item.value + } + } + + public func text() async throws -> String { + let data = try await data() + return String(data: data, encoding: .utf8) ?? "" + } + + public func bytes() async throws -> [UInt8] { + let data = try await data() + return .init(data) + } + + public func data() async throws -> Data { + return self.body ?? .init() + } +} From b79fbb19ef94e865802141c4308ebc1651b642e2 Mon Sep 17 00:00:00 2001 From: Andrew Barba Date: Mon, 29 Jul 2024 14:53:40 -0400 Subject: [PATCH 6/9] Tests --- Sources/Vercel/Fetch.swift | 105 +++++++++++++++++-- Sources/Vercel/FetchRequest.swift | 74 ++++++++++++++ Sources/Vercel/IncomingRequest.swift | 62 ++++++++++- Sources/Vercel/RequestHandler.swift | 25 ++--- Sources/VercelVapor/VaporHandler.swift | 12 ++- Tests/VercelTests/EdgeConfigTests.swift | 1 + Tests/VercelTests/FetchTests.swift | 1 + Tests/VercelTests/RequestTests.swift | 130 +++++++++++++----------- 8 files changed, 311 insertions(+), 99 deletions(-) create mode 100644 Sources/Vercel/FetchRequest.swift diff --git a/Sources/Vercel/Fetch.swift b/Sources/Vercel/Fetch.swift index 9333835..3d08cd2 100644 --- a/Sources/Vercel/Fetch.swift +++ b/Sources/Vercel/Fetch.swift @@ -15,17 +15,74 @@ import FoundationNetworking public enum FetchError: Error, Sendable { case invalidRequest case invalidResponse + case invalidURL case timeout case invalidLambdaContext } -public func fetch(_ httpRequest: HTTPRequest) async throws -> FetchResponse { +public func fetch(_ request: FetchRequest) async throws -> FetchResponse { + // Build url components from request url + guard var urlComponents = URLComponents(string: request.url.absoluteString) else { + throw FetchError.invalidURL + } + + // Set default scheme + urlComponents.scheme = urlComponents.scheme ?? "http" + + // Set default host + urlComponents.host = urlComponents.host ?? "localhost" + + // Set default query params + urlComponents.queryItems = urlComponents.queryItems ?? [] + + // Build search params + for (key, value) in request.searchParams { + urlComponents.queryItems?.append(.init(name: key, value: value)) + } + + // Parse final url + guard let url = urlComponents.url else { + throw FetchError.invalidURL + } + + // Set request resources + var httpRequest = URLRequest(url: url) + + // Set request method + httpRequest.httpMethod = request.method.rawValue + + // Set the timeout interval + if let timeoutInterval = request.timeoutInterval { + httpRequest.timeoutInterval = timeoutInterval + } + + // Set default content type based on body + if let contentType = request.body?.defaultContentType { + let name = HTTPField.Name.contentType.canonicalName + httpRequest.setValue(request.headers[name] ?? contentType, forHTTPHeaderField: name) + } + + // Set headers + for (key, value) in request.headers { + httpRequest.setValue(value, forHTTPHeaderField: key) + } + + // Write bytes to body + switch request.body { + case .bytes(let bytes): + httpRequest.httpBody = Data(bytes) + case .data(let data): + httpRequest.httpBody = data + case .text(let text): + httpRequest.httpBody = Data(text.utf8) + case .json(let json): + httpRequest.httpBody = json + case .none: + break + } + return try await withCheckedThrowingContinuation { continuation in - guard let urlRequest = URLRequest(httpRequest: httpRequest) else { - continuation.resume(throwing: FetchError.invalidRequest) - return - } - let task = URLSession.shared.dataTask(with: urlRequest) { data, urlResponse, error in + let task = URLSession.shared.dataTask(with: httpRequest) { data, urlResponse, error in if let error { continuation.resume(throwing: error) return @@ -40,14 +97,42 @@ public func fetch(_ httpRequest: HTTPRequest) async throws -> FetchResponse { } } -public func fetch(_ url: URL) async throws -> FetchResponse { - let request = HTTPRequest(url: url) +public func fetch( + _ url: URL, + method: HTTPRequest.Method = .get, + body: FetchRequest.Body? = nil, + headers: [String: String] = [:], + searchParams: [String: String] = [:], + timeoutInterval: TimeInterval? = nil +) async throws -> FetchResponse { + let request = FetchRequest( + url: url, + method: method, + body: body, + headers: headers, + searchParams: searchParams, + timeoutInterval: timeoutInterval + ) return try await fetch(request) } -public func fetch(_ urlPath: String) async throws -> FetchResponse { +public func fetch( + _ urlPath: String, + method: HTTPRequest.Method = .get, + body: FetchRequest.Body? = nil, + headers: [String: String] = [:], + searchParams: [String: String] = [:], + timeoutInterval: TimeInterval? = nil +) async throws -> FetchResponse { guard let url = URL(string: urlPath) else { throw FetchError.invalidRequest } - return try await fetch(url) + return try await fetch( + url, + method: method, + body: body, + headers: headers, + searchParams: searchParams, + timeoutInterval: timeoutInterval + ) } diff --git a/Sources/Vercel/FetchRequest.swift b/Sources/Vercel/FetchRequest.swift new file mode 100644 index 0000000..6dd3421 --- /dev/null +++ b/Sources/Vercel/FetchRequest.swift @@ -0,0 +1,74 @@ +// +// FetchRequest.swift +// +// +// Created by Andrew Barba on 1/22/23. +// + +import Foundation +import HTTPTypes + +public struct FetchRequest: Sendable { + + public var url: URL + + public var method: HTTPRequest.Method + + public var headers: [String: String] + + public var searchParams: [String: String] + + public var body: Body? + + public var timeoutInterval: TimeInterval? = nil + + public init( + url: URL, + method: HTTPRequest.Method = .get, + body: Body? = nil, + headers: [String: String] = [:], + searchParams: [String: String] = [:], + timeoutInterval: TimeInterval? = nil + ) { + self.url = url + self.method = method + self.headers = headers + self.searchParams = searchParams + self.body = body + self.timeoutInterval = timeoutInterval + } +} + +extension FetchRequest { + + public enum Body: Sendable { + case bytes(_ bytes: [UInt8]) + case data(_ data: Data) + case text(_ text: String) + case json(_ json: Data) + + public static func json(_ value: T, encoder: JSONEncoder = .init()) throws -> Body where T: Encodable { + let data = try encoder.encode(value) + return Body.json(data) + } + + public static func json(_ jsonObject: [String: Any]) throws -> Body { + let data = try JSONSerialization.data(withJSONObject: jsonObject, options: []) + return Body.json(data) + } + + public static func json(_ jsonArray: [Any]) throws -> Body { + let data = try JSONSerialization.data(withJSONObject: jsonArray, options: []) + return Body.json(data) + } + + public var defaultContentType: String? { + switch self { + case .json: + return "application/json" + default: + return nil + } + } + } +} diff --git a/Sources/Vercel/IncomingRequest.swift b/Sources/Vercel/IncomingRequest.swift index f6ea37c..8de4270 100644 --- a/Sources/Vercel/IncomingRequest.swift +++ b/Sources/Vercel/IncomingRequest.swift @@ -9,10 +9,32 @@ import AWSLambdaRuntime import HTTPTypes public struct IncomingRequest: Sendable { + public struct Body: Sendable { + public enum Error: Swift.Error { + case invalidEncoding + } + + internal let data: Data + + public func bytes() async throws -> [UInt8] { + return .init(data) + } + + public func data() async throws -> Data { + return data + } + + public func text(encoding: String.Encoding = .utf8) async throws -> String { + guard let value = String(data: data, encoding: encoding) else { + throw Error.invalidEncoding + } + return value + } + } public let request: HTTPRequest - public let body: String? + public let body: Body? public let context: LambdaContext @@ -29,10 +51,6 @@ extension IncomingRequest { request.url!.path } - public var rawPath: String { - request.path! - } - public var method: HTTPRequest.Method { request.method } @@ -40,6 +58,14 @@ extension IncomingRequest { public var headerFields: HTTPFields { request.headerFields } + + public var searchParams: [String: String] { + let components = URLComponents(string: request.path ?? "/") + let searchParams = components?.queryItems?.reduce(into: [:]) { + $0[$1.name] = $1.value + } + return searchParams ?? [:] + } } extension IncomingRequest { @@ -60,3 +86,29 @@ extension IncomingRequest { @TaskLocal public static var current: Self? } + +extension IncomingRequest { + + internal init(_ payload: InvokeEvent.Payload, in context: LambdaContext) { + let headerFields: HTTPFields = payload.headers.reduce(into: .init()) { + $0.append(.init(name: .init($1.key)!, value: $1.value)) + } + let bodyData: Data? + if let encoding = payload.encoding, let body = payload.body, encoding == "base64" { + bodyData = Data(base64Encoded: body) + } else { + bodyData = payload.body?.data(using: .utf8) + } + self.init( + request: .init( + method: payload.method, + scheme: "https", + authority: payload.headers["host"], + path: payload.path, + headerFields: headerFields + ), + body: bodyData.map { .init(data: $0) }, + context: context + ) + } +} diff --git a/Sources/Vercel/RequestHandler.swift b/Sources/Vercel/RequestHandler.swift index 31c2156..4a0f812 100644 --- a/Sources/Vercel/RequestHandler.swift +++ b/Sources/Vercel/RequestHandler.swift @@ -1,6 +1,6 @@ // // RequestHandler.swift -// +// // // Created by Andrew Barba on 1/21/23. // @@ -9,7 +9,8 @@ import AWSLambdaRuntime import HTTPTypes import NIOCore -public protocol RequestHandler: Sendable & EventLoopLambdaHandler where Event == InvokeEvent, Output == InvokeResponse { +public protocol RequestHandler: Sendable & EventLoopLambdaHandler +where Event == InvokeEvent, Output == InvokeResponse { func onRequest(_ req: IncomingRequest) async throws -> OutgoingResponse @@ -20,24 +21,14 @@ public protocol RequestHandler: Sendable & EventLoopLambdaHandler where Event == extension RequestHandler { - public func handle(_ event: InvokeEvent, context: LambdaContext) -> EventLoopFuture { + public func handle( + _ event: InvokeEvent, + context: LambdaContext + ) -> EventLoopFuture { return context.eventLoop.makeFutureWithTask { let data = Data(event.body.utf8) let payload = try JSONDecoder().decode(InvokeEvent.Payload.self, from: data) - let headerFields: HTTPFields = payload.headers.reduce(into: .init()) { - $0.append(.init(name: .init($1.key)!, value: $1.value)) - } - let request = IncomingRequest( - request: .init( - method: payload.method, - scheme: "https", - authority: payload.headers["host"], - path: payload.path, - headerFields: headerFields - ), - body: payload.body, - context: context - ) + let request = IncomingRequest(payload, in: context) return try await IncomingRequest.$current.withValue(request) { let response = try await onRequest(request) let headers: [String: String] = response.response.headerFields.reduce(into: [:]) { diff --git a/Sources/VercelVapor/VaporHandler.swift b/Sources/VercelVapor/VaporHandler.swift index 021fb52..e993b72 100644 --- a/Sources/VercelVapor/VaporHandler.swift +++ b/Sources/VercelVapor/VaporHandler.swift @@ -37,7 +37,7 @@ extension VaporHandler { guard let app = await Shared.default.app else { return .status(.serviceUnavailable).send("Vapor application not configured") } - let vaporRequest = try Vapor.Request.from(request: req, for: app) + let vaporRequest = try await Vapor.Request.from(request: req, for: app) let vaporResponse = try await app.responder.respond(to: vaporRequest).get() return try await .from(response: vaporResponse, on: app.eventLoopGroup.next()) } @@ -56,10 +56,12 @@ private actor Shared { extension Vapor.Request { - static func from(request: IncomingRequest, for app: Application) throws -> Self { - let buffer = request.body.map { data in + static func from(request: IncomingRequest, for app: Application) async throws -> Self { + let bytes = try await request.body?.bytes() + + let buffer = bytes.map { data in var _buffer = request.context.allocator.buffer(capacity: data.count) - _buffer.writeBytes(data.utf8) + _buffer.writeBytes(data) return _buffer } @@ -70,7 +72,7 @@ extension Vapor.Request { return try .init( application: app, method: .init(rawValue: request.method.rawValue), - url: .init(string: request.rawPath), + url: .init(string: request.url.absoluteString), version: HTTPVersion(major: 1, minor: 1), headers: nioHeaders, collectedBody: buffer, diff --git a/Tests/VercelTests/EdgeConfigTests.swift b/Tests/VercelTests/EdgeConfigTests.swift index 2b93570..6e3d860 100644 --- a/Tests/VercelTests/EdgeConfigTests.swift +++ b/Tests/VercelTests/EdgeConfigTests.swift @@ -1,4 +1,5 @@ import XCTest + @testable import Vercel private let _id = "ecfg_12345678" diff --git a/Tests/VercelTests/FetchTests.swift b/Tests/VercelTests/FetchTests.swift index a33acc7..8476300 100644 --- a/Tests/VercelTests/FetchTests.swift +++ b/Tests/VercelTests/FetchTests.swift @@ -1,4 +1,5 @@ import XCTest + @testable import Vercel final class FetchTests: XCTestCase { diff --git a/Tests/VercelTests/RequestTests.swift b/Tests/VercelTests/RequestTests.swift index e526945..b3f92b5 100644 --- a/Tests/VercelTests/RequestTests.swift +++ b/Tests/VercelTests/RequestTests.swift @@ -1,90 +1,96 @@ import AWSLambdaRuntime import XCTest + @testable import Vercel let context = LambdaContext.__forTestsOnly( - requestID: "", - traceID: "", - invokedFunctionARN: "", - timeout: .seconds(10), - logger: .init(label: ""), + requestID: "", + traceID: "", + invokedFunctionARN: "", + timeout: .seconds(10), + logger: .init(label: ""), eventLoop: .singletonMultiThreadedEventLoopGroup.next() ) final class RequestTests: XCTestCase { func testSimpleDecode() throws { let json = """ - { - "method": "GET", - "path": "/", - "headers": {} - } - """ - let payload = try JSONDecoder().decode(InvokeEvent.Payload.self, from: json.data(using: .utf8)!) - let req = Request(payload, in: context) - XCTAssertEqual(req.method, .GET) + { + "method": "GET", + "path": "/", + "headers": {} + } + """ + let payload = try JSONDecoder().decode( + InvokeEvent.Payload.self, from: json.data(using: .utf8)!) + let req = IncomingRequest(payload, in: context) + XCTAssertEqual(req.method, .get) } func testMultiValueHeaderDecode() throws { let json = """ - { - "method": "GET", - "path": "/", - "headers": { - "a": "1", - "b": ["2", "3"] - } - } - """ - let payload = try JSONDecoder().decode(InvokeEvent.Payload.self, from: json.data(using: .utf8)!) - let req = Request(payload, in: context) - XCTAssertEqual(req.method, .GET) - XCTAssertEqual(req.headers["a"]!.value, "1") - XCTAssertEqual(req.headers["b"]!.value, "2") + { + "method": "GET", + "path": "/", + "headers": { + "accept": "1", + "content-type": ["2", "3"] + } + } + """ + let payload = try JSONDecoder().decode( + InvokeEvent.Payload.self, from: json.data(using: .utf8)!) + let req = IncomingRequest(payload, in: context) + XCTAssertEqual(req.method, .get) + XCTAssertEqual(req.headerFields[.accept], "1") + XCTAssertEqual(req.headerFields[.contentType], "2") } func testSearchParams() throws { let json = """ - { - "method": "GET", - "path": "/foo?token=12345", - "headers": {} - } - """ - let payload = try JSONDecoder().decode(InvokeEvent.Payload.self, from: json.data(using: .utf8)!) - let req = Request(payload, in: context) - XCTAssertEqual(req.method, .GET) + { + "method": "GET", + "path": "/foo?token=12345", + "headers": {} + } + """ + let payload = try JSONDecoder().decode( + InvokeEvent.Payload.self, from: json.data(using: .utf8)!) + let req = IncomingRequest(payload, in: context) + XCTAssertEqual(req.method, .get) XCTAssertEqual(req.searchParams["token"], "12345") } - func testPlainBody() throws { + func testPlainBody() async throws { let json = """ - { - "method": "PUT", - "path": "/", - "headers": {}, - "body": "hello" - } - """ - let payload = try JSONDecoder().decode(InvokeEvent.Payload.self, from: json.data(using: .utf8)!) - let req = Request(payload, in: context) - XCTAssertEqual(req.body, "hello") + { + "method": "PUT", + "path": "/", + "headers": {}, + "body": "hello" + } + """ + let payload = try JSONDecoder().decode( + InvokeEvent.Payload.self, from: json.data(using: .utf8)!) + let req = IncomingRequest(payload, in: context) + let text = try await req.body?.text() + XCTAssertEqual(text, "hello") } - func testBase64Body() throws { + func testBase64Body() async throws { let json = """ - { - "method": "PUT", - "path": "/", - "headers": {}, - "body": "/////w==", - "encoding": "base64" - } - """ - let payload = try JSONDecoder().decode(InvokeEvent.Payload.self, from: json.data(using: .utf8)!) - let req = Request(payload, in: context) - let expectData: [UInt8] = [0xff, 0xff, 0xff, 0xff] - XCTAssertEqual(req.rawBody, Data(expectData)) - XCTAssertNil(req.body) + { + "method": "PUT", + "path": "/", + "headers": {}, + "body": "/////w==", + "encoding": "base64" + } + """ + let payload = try JSONDecoder().decode( + InvokeEvent.Payload.self, from: json.data(using: .utf8)!) + let req = IncomingRequest(payload, in: context) + let data = try await req.body?.bytes() + XCTAssertEqual(data, [0xff, 0xff, 0xff, 0xff]) } } From c141447c63e4efe1b4820c9dda1f551114bd01b9 Mon Sep 17 00:00:00 2001 From: Andrew Barba Date: Tue, 30 Jul 2024 11:33:40 -0400 Subject: [PATCH 7/9] Use swift nightly --- .swift-format | 1 + Plugins/VercelPackager/VercelOutput.swift | 30 ++++++++++++++--------- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/.swift-format b/.swift-format index 306e8dd..30b6e03 100644 --- a/.swift-format +++ b/.swift-format @@ -2,6 +2,7 @@ "indentation" : { "spaces": 4 }, + "lineLength": 120, "tabWidth": 4, "version": 1 } \ No newline at end of file diff --git a/Plugins/VercelPackager/VercelOutput.swift b/Plugins/VercelPackager/VercelOutput.swift index 20ac984..eea8f80 100644 --- a/Plugins/VercelPackager/VercelOutput.swift +++ b/Plugins/VercelPackager/VercelOutput.swift @@ -1,6 +1,6 @@ // // VercelOutput.swift -// +// // // Created by Andrew Barba on 1/21/23. // @@ -26,7 +26,8 @@ public struct VercelOutput { return encoder } - public init(packageManager: PackagePlugin.PackageManager, context: PackagePlugin.PluginContext, arguments: [String]) { + public init(packageManager: PackagePlugin.PackageManager, context: PackagePlugin.PluginContext, arguments: [String]) + { self.packageManager = packageManager self.context = context self.arguments = arguments @@ -55,7 +56,7 @@ public struct VercelOutput { var deployArguments = [ "deploy", - "--prebuilt" + "--prebuilt", ] if arguments.contains("--prod") { @@ -190,7 +191,8 @@ extension VercelOutput { } // Split file into lines - let lines = text + let lines = + text .split(separator: "\n") .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } @@ -199,7 +201,8 @@ extension VercelOutput { guard !line.starts(with: "#") else { return } // Split the line into key value parts - let keyValue = line + let keyValue = + line .split(separator: "=", maxSplits: 1) .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } @@ -383,7 +386,7 @@ extension VercelOutput { // Handle filesystem .init(handle: "filesystem"), // Proxy all other routes - .init(src: "^.*$", dest: product.name, check: true) + .init(src: "^.*$", dest: product.name, check: true), ] let config = OutputConfiguration( routes: routes, @@ -442,7 +445,9 @@ extension VercelOutput { architecture: architecture, memory: .init(functionMemory), maxDuration: .init(functionDuration), - regions: functionRegions?.components(separatedBy: ",").map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } + regions: functionRegions?.components(separatedBy: ",").map { + $0.trimmingCharacters(in: .whitespacesAndNewlines) + } ) let data = try encoder.encode(config) fs.createFile(atPath: vercelFunctionConfigurationPath(product).string, contents: data) @@ -491,8 +496,10 @@ extension VercelOutput { try Shell.execute( executable: context.tool(named: "node").path, arguments: [ - projectDirectory.appending([".build", "checkouts", "Vercel", "Plugins", "VercelPackager", "Server", "server.cjs"]).string, - port + projectDirectory.appending([ + ".build", "checkouts", "Vercel", "Plugins", "VercelPackager", "Server", "server.cjs", + ]).string, + port, ], environment: ["SWIFT_PROJECT_DIRECTORY": projectDirectory.string], printCommand: false @@ -551,7 +558,8 @@ extension VercelOutput { private func buildDockerProduct(_ product: Product) async throws -> Path { let dockerToolPath = try context.tool(named: "docker").path - let baseImage = "swift:\(context.package.toolsVersion.major).\(context.package.toolsVersion.minor)-amazonlinux2" + let baseImage = + "swiftlang/swift:nightly-\(context.package.toolsVersion.major).\(context.package.toolsVersion.minor)-amazonlinux2" // build the product try Shell.execute( @@ -563,7 +571,7 @@ extension VercelOutput { "-v", "\(context.package.directory.string):/workspace", "-w", "/workspace", baseImage, - "bash", "-cl", "swift build -c release -Xswiftc -Osize --static-swift-stdlib" + "bash", "-cl", "swift build -c release -Xswiftc -Osize --static-swift-stdlib", ] ) From 602c3c7d08f46827226440c255bcdee14d75479b Mon Sep 17 00:00:00 2001 From: Andrew Barba Date: Tue, 30 Jul 2024 12:07:14 -0400 Subject: [PATCH 8/9] Revert --- .gitignore | 1 + Plugins/VercelPackager/VercelOutput.swift | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 3596c28..bc04b1c 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ DerivedData/ .swiftpm/config/registries.json .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata .netrc +.vscode/ diff --git a/Plugins/VercelPackager/VercelOutput.swift b/Plugins/VercelPackager/VercelOutput.swift index eea8f80..17da345 100644 --- a/Plugins/VercelPackager/VercelOutput.swift +++ b/Plugins/VercelPackager/VercelOutput.swift @@ -559,7 +559,7 @@ extension VercelOutput { private func buildDockerProduct(_ product: Product) async throws -> Path { let dockerToolPath = try context.tool(named: "docker").path let baseImage = - "swiftlang/swift:nightly-\(context.package.toolsVersion.major).\(context.package.toolsVersion.minor)-amazonlinux2" + "swift:\(context.package.toolsVersion.major).\(context.package.toolsVersion.minor)-amazonlinux2" // build the product try Shell.execute( From 17b796ad0d9250f73f60cc8cd8e29f2fd806e42a Mon Sep 17 00:00:00 2001 From: Andrew Barba Date: Tue, 30 Jul 2024 14:16:35 -0400 Subject: [PATCH 9/9] Nightly arg --- Plugins/VercelPackager/VercelOutput.swift | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Plugins/VercelPackager/VercelOutput.swift b/Plugins/VercelPackager/VercelOutput.swift index 17da345..4b6d749 100644 --- a/Plugins/VercelPackager/VercelOutput.swift +++ b/Plugins/VercelPackager/VercelOutput.swift @@ -149,6 +149,10 @@ extension VercelOutput { arguments.contains("--prod") } + public var nightly: Bool { + arguments.contains("--nightly") + } + public var functionMemory: String { argument("memory") ?? "512" } @@ -558,8 +562,9 @@ extension VercelOutput { private func buildDockerProduct(_ product: Product) async throws -> Path { let dockerToolPath = try context.tool(named: "docker").path - let baseImage = - "swift:\(context.package.toolsVersion.major).\(context.package.toolsVersion.minor)-amazonlinux2" + let baseImage = nightly + ? "swiftlang/swift:nightly-\(context.package.toolsVersion.major).\(context.package.toolsVersion.minor)-amazonlinux2" + : "swift:\(context.package.toolsVersion.major).\(context.package.toolsVersion.minor)-amazonlinux2" // build the product try Shell.execute(