Skip to content

Commit 7487b2c

Browse files
authored
Add support for bare Lambda function URLs (#21)
Add request and response events for Lambda Function URL invocations [Lambdas can be called with an auto-generated URL](https://docs.aws.amazon.com/lambda/latest/dg/urls-invocation.html), which has a specific format similar but not identical to API Gateway.
1 parent b415da2 commit 7487b2c

File tree

2 files changed

+264
-0
lines changed

2 files changed

+264
-0
lines changed
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the SwiftAWSLambdaRuntime open source project
4+
//
5+
// Copyright (c) 2017-2022 Apple Inc. and the SwiftAWSLambdaRuntime project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import class Foundation.JSONEncoder
16+
17+
// https://docs.aws.amazon.com/lambda/latest/dg/urls-invocation.html
18+
19+
/// FunctionURLRequest contains data coming from a bare Lambda Function URL
20+
public struct FunctionURLRequest: Codable {
21+
public struct Context: Codable {
22+
public struct Authorizer: Codable {
23+
public struct IAMAuthorizer: Codable {
24+
public let accessKey: String
25+
26+
public let accountId: String
27+
public let callerId: String
28+
public let cognitoIdentity: String?
29+
30+
public let principalOrgId: String?
31+
32+
public let userArn: String
33+
public let userId: String
34+
}
35+
36+
public let iam: IAMAuthorizer?
37+
}
38+
39+
public struct HTTP: Codable {
40+
public let method: HTTPMethod
41+
public let path: String
42+
public let `protocol`: String
43+
public let sourceIp: String
44+
public let userAgent: String
45+
}
46+
47+
public let accountId: String
48+
public let apiId: String
49+
public let authentication: String?
50+
public let authorizer: Authorizer?
51+
public let domainName: String
52+
public let domainPrefix: String
53+
public let http: HTTP
54+
55+
public let requestId: String
56+
public let routeKey: String
57+
public let stage: String
58+
59+
public let time: String
60+
public let timeEpoch: Int
61+
}
62+
63+
public let version: String
64+
65+
public let routeKey: String
66+
public let rawPath: String
67+
public let rawQueryString: String
68+
public let cookies: [String]?
69+
public let headers: HTTPHeaders
70+
public let queryStringParameters: [String: String]?
71+
72+
public let requestContext: Context
73+
74+
public let body: String?
75+
public let pathParameters: [String: String]?
76+
public let isBase64Encoded: Bool
77+
78+
public let stageVariables: [String: String]?
79+
}
80+
81+
// MARK: - Response -
82+
83+
public struct FunctionURLResponse: Codable {
84+
public var statusCode: HTTPResponseStatus
85+
public var headers: HTTPHeaders?
86+
public var body: String?
87+
public let cookies: [String]?
88+
public var isBase64Encoded: Bool?
89+
90+
public init(
91+
statusCode: HTTPResponseStatus,
92+
headers: HTTPHeaders? = nil,
93+
body: String? = nil,
94+
cookies: [String]? = nil,
95+
isBase64Encoded: Bool? = nil
96+
) {
97+
self.statusCode = statusCode
98+
self.headers = headers
99+
self.body = body
100+
self.cookies = cookies
101+
self.isBase64Encoded = isBase64Encoded
102+
}
103+
}
104+
105+
#if swift(>=5.6)
106+
extension FunctionURLRequest: Sendable {}
107+
extension FunctionURLRequest.Context: Sendable {}
108+
extension FunctionURLRequest.Context.Authorizer: Sendable {}
109+
extension FunctionURLRequest.Context.Authorizer.IAMAuthorizer: Sendable {}
110+
extension FunctionURLRequest.Context.HTTP: Sendable {}
111+
extension FunctionURLResponse: Sendable {}
112+
#endif
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the SwiftAWSLambdaRuntime open source project
4+
//
5+
// Copyright (c) 2017-2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
@testable import AWSLambdaEvents
16+
import XCTest
17+
18+
class FunctionURLTests: XCTestCase {
19+
/// Example event body pulled from [AWS documentation](https://docs.aws.amazon.com/lambda/latest/dg/urls-invocation.html#urls-request-payload).
20+
static let documentationExample = """
21+
{
22+
"version": "2.0",
23+
"routeKey": "$default",
24+
"rawPath": "/my/path",
25+
"rawQueryString": "parameter1=value1&parameter1=value2&parameter2=value",
26+
"cookies": [
27+
"cookie1",
28+
"cookie2"
29+
],
30+
"headers": {
31+
"header1": "value1",
32+
"header2": "value1,value2"
33+
},
34+
"queryStringParameters": {
35+
"parameter1": "value1,value2",
36+
"parameter2": "value"
37+
},
38+
"requestContext": {
39+
"accountId": "123456789012",
40+
"apiId": "<urlid>",
41+
"authentication": null,
42+
"authorizer": {
43+
"iam": {
44+
"accessKey": "AKIA...",
45+
"accountId": "111122223333",
46+
"callerId": "AIDA...",
47+
"cognitoIdentity": null,
48+
"principalOrgId": null,
49+
"userArn": "arn:aws:iam::111122223333:user/example-user",
50+
"userId": "AIDA..."
51+
}
52+
},
53+
"domainName": "<url-id>.lambda-url.us-west-2.on.aws",
54+
"domainPrefix": "<url-id>",
55+
"http": {
56+
"method": "POST",
57+
"path": "/my/path",
58+
"protocol": "HTTP/1.1",
59+
"sourceIp": "123.123.123.123",
60+
"userAgent": "agent"
61+
},
62+
"requestId": "id",
63+
"routeKey": "$default",
64+
"stage": "$default",
65+
"time": "12/Mar/2020:19:03:58 +0000",
66+
"timeEpoch": 1583348638390
67+
},
68+
"body": "Hello from client!",
69+
"pathParameters": null,
70+
"isBase64Encoded": false,
71+
"stageVariables": null
72+
}
73+
"""
74+
75+
/// Example event body pulled from an an actual Lambda invocation.
76+
static let realWorldExample = """
77+
{
78+
"headers": {
79+
"x-amzn-tls-cipher-suite": "ECDHE-RSA-AES128-GCM-SHA256",
80+
"x-amzn-tls-version": "TLSv1.2",
81+
"x-amzn-trace-id": "Root=0-12345678-9abcdef0123456789abcdef0",
82+
"cookie": "test",
83+
"x-forwarded-proto": "https",
84+
"host": "0123456789abcdefghijklmnopqrstuv.lambda-url.us-west-2.on.aws",
85+
"x-forwarded-port": "443",
86+
"x-forwarded-for": "1.2.3.4",
87+
"accept": "*/*",
88+
"user-agent": "curl"
89+
},
90+
"isBase64Encoded": false,
91+
"rawPath": "/",
92+
"routeKey": "$default",
93+
"requestContext": {
94+
"accountId": "anonymous",
95+
"timeEpoch": 1667192002044,
96+
"routeKey": "$default",
97+
"stage": "$default",
98+
"domainPrefix": "0123456789abcdefghijklmnopqrstuv",
99+
"requestId": "01234567-89ab-cdef-0123-456789abcdef",
100+
"domainName": "0123456789abcdefghijklmnopqrstuv.lambda-url.us-west-2.on.aws",
101+
"http": {
102+
"path": "/",
103+
"protocol": "HTTP/1.1",
104+
"method": "GET",
105+
"sourceIp": "1.2.3.4",
106+
"userAgent": "curl"
107+
},
108+
"time": "31/Oct/2022:04:53:22 +0000",
109+
"apiId": "0123456789abcdefghijklmnopqrstuv"
110+
},
111+
"queryStringParameters": {
112+
"test": "2"
113+
},
114+
"version": "2.0",
115+
"rawQueryString": "test=2",
116+
"cookies": [
117+
"test"
118+
]
119+
}
120+
"""
121+
122+
// MARK: - Request -
123+
124+
// MARK: Decoding
125+
126+
func testRequestDecodingDocumentationExampleRequest() {
127+
let data = Self.documentationExample.data(using: .utf8)!
128+
var req: FunctionURLRequest?
129+
XCTAssertNoThrow(req = try JSONDecoder().decode(FunctionURLRequest.self, from: data))
130+
131+
XCTAssertEqual(req?.rawPath, "/my/path")
132+
XCTAssertEqual(req?.requestContext.http.method, .POST)
133+
XCTAssertEqual(req?.queryStringParameters?.count, 2)
134+
XCTAssertEqual(req?.rawQueryString, "parameter1=value1&parameter1=value2&parameter2=value")
135+
XCTAssertEqual(req?.headers.count, 2)
136+
XCTAssertEqual(req?.body, "Hello from client!")
137+
}
138+
139+
func testRequestDecodingRealWorldExampleRequest() {
140+
let data = Self.realWorldExample.data(using: .utf8)!
141+
var req: FunctionURLRequest?
142+
XCTAssertNoThrow(req = try JSONDecoder().decode(FunctionURLRequest.self, from: data))
143+
144+
XCTAssertEqual(req?.rawPath, "/")
145+
XCTAssertEqual(req?.requestContext.http.method, .GET)
146+
XCTAssertEqual(req?.queryStringParameters?.count, 1)
147+
XCTAssertEqual(req?.rawQueryString, "test=2")
148+
XCTAssertEqual(req?.headers.count, 10)
149+
XCTAssertEqual(req?.cookies, ["test"])
150+
XCTAssertNil(req?.body)
151+
}
152+
}

0 commit comments

Comments
 (0)