├── .github
└── workflows
│ └── swift.yml
├── .gitignore
├── CONTRIBUTING.md
├── LICENSE
├── Makefile
├── Package.swift
├── README.md
└── Sources
├── MacroLambda
├── LambdaExpress.swift
└── ReExports.swift
└── MacroLambdaCore
├── LambdaNIOConversions.swift
├── LambdaRequest.swift
├── LambdaResponse.swift
├── LambdaServer.swift
├── Process.swift
├── README.md
└── lambda.swift
/.github/workflows/swift.yml:
--------------------------------------------------------------------------------
1 | name: Build and Test
2 |
3 | on:
4 | push:
5 | pull_request:
6 | schedule:
7 | - cron: "25 9 * * 1"
8 |
9 | jobs:
10 | linux:
11 | runs-on: ubuntu-latest
12 | strategy:
13 | fail-fast: false
14 | matrix:
15 | image:
16 | - swift:5.9.2-focal
17 | - swift:5.10-jammy
18 | - swift:6.0-noble
19 | container: ${{ matrix.image }}
20 | steps:
21 | - name: Checkout Repository
22 | uses: actions/checkout@v4
23 | - name: Build Swift Debug Package
24 | run: swift build -c debug
25 | - name: Build Swift Release Package
26 | run: swift build -c release
27 | nextstep:
28 | runs-on: macos-latest
29 | steps:
30 | - name: Select latest available Xcode
31 | uses: maxim-lobanov/setup-xcode@v1.5.1
32 | with:
33 | xcode-version: latest
34 | - name: Checkout Repository
35 | uses: actions/checkout@v4
36 | - name: Build Swift Debug Package
37 | run: swift build -c debug
38 | - name: Build Swift Release Package
39 | run: swift build -c release
40 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Xcode
2 | #
3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
4 |
5 | ## User settings
6 | xcuserdata/
7 |
8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
9 | *.xcscmblueprint
10 | *.xccheckout
11 |
12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
13 | build/
14 | DerivedData/
15 | *.moved-aside
16 | *.pbxuser
17 | !default.pbxuser
18 | *.mode1v3
19 | !default.mode1v3
20 | *.mode2v3
21 | !default.mode2v3
22 | *.perspectivev3
23 | !default.perspectivev3
24 |
25 | ## Obj-C/Swift specific
26 | *.hmap
27 |
28 | ## App packaging
29 | *.ipa
30 | *.dSYM.zip
31 | *.dSYM
32 |
33 | ## Playgrounds
34 | timeline.xctimeline
35 | playground.xcworkspace
36 |
37 | # Swift Package Manager
38 | #
39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
40 | # Packages/
41 | # Package.pins
42 | # Package.resolved
43 | # *.xcodeproj
44 | #
45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
46 | # hence it is not needed unless you have added a package configuration file to your project
47 | # .swiftpm
48 |
49 | .build/
50 |
51 | # CocoaPods
52 | #
53 | # We recommend against adding the Pods directory to your .gitignore. However
54 | # you should judge for yourself, the pros and cons are mentioned at:
55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
56 | #
57 | # Pods/
58 | #
59 | # Add this line if you want to avoid checking in source code from the Xcode workspace
60 | # *.xcworkspace
61 |
62 | # Carthage
63 | #
64 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
65 | # Carthage/Checkouts
66 |
67 | Carthage/Build/
68 |
69 | # Accio dependency management
70 | Dependencies/
71 | .accio/
72 |
73 | # fastlane
74 | #
75 | # It is recommended to not store the screenshots in the git repo.
76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed.
77 | # For more information about the recommended setup visit:
78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control
79 |
80 | fastlane/report.xml
81 | fastlane/Preview.html
82 | fastlane/screenshots/**/*.png
83 | fastlane/test_output
84 |
85 | # Code Injection
86 | #
87 | # After new code Injection tools there's a generated folder /iOSInjectionProject
88 | # https://github.com/johnno1962/injectionforxcode
89 |
90 | iOSInjectionProject/
91 |
92 | # hh
93 | Package.resolved
94 | xcuserdata
95 | .docker.build
96 | .swiftpm
97 |
98 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | ## Legal
2 |
3 | By submitting a pull request, you represent that you have the right to license
4 | your contribution to ZeeZide and the community, and agree by submitting the patch
5 | that your contributions are licensed under the Apache 2.0 license.
6 |
7 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright 2020 ZeeZide GmbH
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | # Makefile
2 |
3 | # local config
4 | SWIFT_BUILD=swift build
5 | SWIFT_CLEAN=swift package clean
6 | SWIFT_BUILD_DIR=.build
7 | SWIFT_TEST=swift test
8 | CONFIGURATION=release
9 |
10 | # docker config
11 | SWIFT_BUILD_IMAGE="swift:5.2.4"
12 | DOCKER_BUILD_DIR=".docker.build"
13 | SWIFT_DOCKER_BUILD_DIR="$(DOCKER_BUILD_DIR)/x86_64-unknown-linux/$(CONFIGURATION)"
14 | DOCKER_BUILD_PRODUCT="$(DOCKER_BUILD_DIR)/$(TOOL_NAME)"
15 |
16 |
17 | SWIFT_SOURCES=\
18 | Sources/*/*/*.swift \
19 | Sources/*/*/*/*.swift
20 |
21 | all:
22 | $(SWIFT_BUILD) -c $(CONFIGURATION)
23 |
24 | # Cannot test in `release` configuration?!
25 | test:
26 | $(SWIFT_TEST)
27 |
28 | clean :
29 | $(SWIFT_CLEAN)
30 | # We have a different definition of "clean", might be just German
31 | # pickyness.
32 | rm -rf $(SWIFT_BUILD_DIR)
33 |
34 | $(DOCKER_BUILD_PRODUCT): $(SWIFT_SOURCES)
35 | docker run --rm \
36 | -v "$(PWD):/src" \
37 | -v "$(PWD)/$(DOCKER_BUILD_DIR):/src/.build" \
38 | "$(SWIFT_BUILD_IMAGE)" \
39 | bash -c 'cd /src && swift build -c $(CONFIGURATION)'
40 |
41 | docker-all: $(DOCKER_BUILD_PRODUCT)
42 |
43 | docker-clean:
44 | rm $(DOCKER_BUILD_PRODUCT)
45 |
46 | docker-distclean:
47 | rm -rf $(DOCKER_BUILD_DIR)
48 |
49 | distclean: clean docker-distclean
50 |
51 | docker-emacs:
52 | docker run --rm -it \
53 | -v "$(PWD):/src" \
54 | -v "$(PWD)/$(DOCKER_BUILD_DIR):/src/.build" \
55 | "$(SWIFT_BUILD_IMAGE)" \
56 | emacs /src
57 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.5
2 |
3 | import PackageDescription
4 |
5 | let package = Package(
6 |
7 | name: "MacroLambda",
8 |
9 | products: [
10 | .library(name: "MacroLambdaCore", targets: [ "MacroLambdaCore" ]),
11 | .library(name: "MacroLambda", targets: [ "MacroLambda" ])
12 | ],
13 |
14 | dependencies: [
15 | .package(url: "https://github.com/Macro-swift/Macro.git",
16 | from: "1.0.0"),
17 | .package(url: "https://github.com/Macro-swift/MacroExpress.git",
18 | from: "1.0.0"),
19 | .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git",
20 | from: "0.5.2")
21 | ],
22 |
23 | targets: [
24 | .target(name: "MacroLambdaCore", dependencies: [
25 | .product(name: "MacroCore" , package: "Macro"),
26 | .product(name: "http" , package: "Macro"),
27 | .product(name: "express" , package: "MacroExpress"),
28 | .product(name: "AWSLambdaRuntime" , package: "swift-aws-lambda-runtime"),
29 | .product(name: "AWSLambdaEvents" , package: "swift-aws-lambda-runtime")
30 | ],
31 | exclude: [
32 | "README.md"
33 | ]),
34 | .target(name: "MacroLambda", dependencies: [
35 | "MacroLambdaCore",
36 | .product(name: "AWSLambdaRuntime" , package: "swift-aws-lambda-runtime"),
37 | "MacroExpress"
38 | ])
39 | ]
40 | )
41 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
MacroLambda
2 |
4 |
5 |
6 | AWS Lambda [API Gateway](https://aws.amazon.com/api-gateway/)
7 | Support for Macro and MacroExpress
8 | (and all things built on-top).
9 |
10 | It allows deployment of arbitrary Macro applications as AWS Lambda functions,
11 | including MacroApp endpoints.
12 |
13 | Blog article: [Deploying Swift on AWS Lambda](http://www.alwaysrightinstitute.com/macrolambda/).
14 |
15 | The module is split into the `MacroLambda` module,
16 | which provides the Express runner (`Lambda.run(express-app)`)
17 | and `MacroLambdaCore` which only links against `http` and provides the
18 | `lambda.createServer` (the peer to `http.createServer`).
19 |
20 | There is a tutorial on getting started with those things:
21 | [Create your first HTTP endpoint with Swift on AWS Lambda](https://fabianfett.de/swift-on-aws-lambda-creating-your-first-http-endpoint).
22 |
23 | Note: The Swift Lambda Runtime requires Swift 5.2.
24 |
25 | ## Example
26 |
27 | ```swift
28 | import MacroLambda
29 |
30 | let app = Express()
31 |
32 | app.use(bodyParser.text())
33 |
34 | app.post("/hello") { req, res, next in
35 | console.log("Client posted:", req.body.text ?? "-")
36 | res.send("Client body sent: \(req.body.text ?? "~nothing~")")
37 | }
38 |
39 | app.get { req, res, next in
40 | res.send("Welcome to Macro!")
41 | }
42 |
43 | Lambda.run(app)
44 | ```
45 |
46 | ## Deployment
47 |
48 | Using `swift lambda` (`brew install SPMDestinations/tap/swift-lambda`):
49 | ```
50 | $ swift lambda deploy -d 5.2
51 | ```
52 | Tutorial available: [Deploying Swift on AWS Lambda](http://www.alwaysrightinstitute.com/macrolambda/).
53 |
54 | ## Environment Variables
55 |
56 | - `macro.core.numthreads`
57 | - `macro.core.iothreads`
58 | - `macro.core.retain.debug`
59 | - `macro.concat.maxsize`
60 | - `macro.streams.debug.rc`
61 |
62 | ### Links
63 |
64 | - [Deploying Swift on AWS Lambda](http://www.alwaysrightinstitute.com/macrolambda/)
65 | - WWDC 2020: [Use Swift on AWS Lambda with Xcode](https://developer.apple.com/videos/play/wwdc2020/10644/)
66 | - Tutorial: [Create your first HTTP endpoint with Swift on AWS Lambda](https://fabianfett.de/swift-on-aws-lambda-creating-your-first-http-endpoint)
67 | - [Swift AWS Lambda Runtime](https://github.com/swift-server/swift-aws-lambda-runtime)
68 | - Amazon Web Services [API Gateway](https://aws.amazon.com/api-gateway/)
69 | - [µExpress](http://www.alwaysrightinstitute.com/microexpress-nio2/)
70 | - [SwiftNIO](https://github.com/apple/swift-nio)
71 |
72 | ### Who
73 |
74 | **Macro** is brought to you by
75 | [Helge Heß](https://github.com/helje5/) / [ZeeZide](https://zeezide.de).
76 | We like feedback, GitHub stars, cool contract work,
77 | presumably any form of praise you can think of.
78 |
79 | There is a `#microexpress` channel on the
80 | [Noze.io Slack](http://slack.noze.io/). Feel free to join!
81 |
--------------------------------------------------------------------------------
/Sources/MacroLambda/LambdaExpress.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ReExports.swift
3 | // MacroLambda
4 | //
5 | // Created by Helge Heß
6 | // Copyright © 2020 ZeeZide GmbH. All rights reserved.
7 | //
8 |
9 | import enum AWSLambdaRuntime.Lambda
10 |
11 | public extension Lambda {
12 |
13 | static func run(_ middleware: MiddlewareObject) -> Never {
14 | let server = lambda.createServer(handler: middleware.requestHandler)
15 | _ = server.run()
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/Sources/MacroLambda/ReExports.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ReExports.swift
3 | // MacroLambda
4 | //
5 | // Created by Helge Heß
6 | // Copyright © 2020 ZeeZide GmbH. All rights reserved.
7 | //
8 |
9 | @_exported import MacroCore
10 | @_exported import MacroExpress
11 |
12 | import enum MacroLambdaCore.lambda
13 | public typealias lambda = MacroLambdaCore.lambda
14 |
15 | import enum AWSLambdaRuntime.Lambda
16 | public typealias Lambda = AWSLambdaRuntime.Lambda
17 |
--------------------------------------------------------------------------------
/Sources/MacroLambdaCore/LambdaNIOConversions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LambdaNIOConversions.swift
3 | // MacroLambda
4 | //
5 | // Created by Helge Heß
6 | // Copyright © 2020-2021 ZeeZide GmbH. All rights reserved.
7 | //
8 |
9 | #if canImport(AWSLambdaEvents)
10 | import struct NIOHTTP1.HTTPRequestHead
11 | import struct NIOHTTP1.HTTPVersion
12 | import enum NIOHTTP1.HTTPMethod
13 | import struct NIOHTTP1.HTTPHeaders
14 | import enum NIOHTTP1.HTTPResponseStatus
15 |
16 | import struct AWSLambdaEvents.HTTPMethod
17 | import struct AWSLambdaEvents.HTTPHeaders
18 | import struct AWSLambdaEvents.HTTPMultiValueHeaders
19 | import struct AWSLambdaEvents.HTTPResponseStatus
20 |
21 | internal extension AWSLambdaEvents.HTTPMethod {
22 |
23 | @inlinable
24 | var asNIO: NIOHTTP1.HTTPMethod {
25 | switch self {
26 | case .GET : return .GET
27 | case .POST : return .POST
28 | case .PUT : return .PUT
29 | case .PATCH : return .PATCH
30 | case .DELETE : return .DELETE
31 | case .OPTIONS : return .OPTIONS
32 | case .HEAD : return .HEAD
33 | default : return .RAW(value: rawValue)
34 | }
35 | }
36 | }
37 |
38 | internal extension AWSLambdaEvents.HTTPHeaders {
39 |
40 | @inlinable
41 | var asNIO: NIOHTTP1.HTTPHeaders {
42 | get {
43 | var headers = NIOHTTP1.HTTPHeaders()
44 | for ( name, value ) in self {
45 | headers.add(name: name, value: value)
46 | }
47 | return headers
48 | }
49 | }
50 | }
51 |
52 | internal extension AWSLambdaEvents.HTTPMultiValueHeaders {
53 |
54 | @inlinable
55 | var asNIO: NIOHTTP1.HTTPHeaders {
56 | set {
57 | for ( name, value ) in newValue {
58 | self[name, default: []].append(value)
59 | }
60 | }
61 | get {
62 | var headers = NIOHTTP1.HTTPHeaders()
63 | for ( name, values ) in self {
64 | for value in values {
65 | headers.add(name: name, value: value)
66 | }
67 | }
68 | return headers
69 | }
70 | }
71 | }
72 |
73 | internal extension NIOHTTP1.HTTPHeaders {
74 |
75 | @inlinable
76 | func asLambda() -> ( headers : AWSLambdaEvents.HTTPHeaders?,
77 | cookies : [ String ]? )
78 | {
79 | guard !isEmpty else { return ( nil, nil ) }
80 |
81 | // Those do no proper CI, lets hope they are consistent
82 | var headers = AWSLambdaEvents.HTTPHeaders()
83 | var cookies = [ String ]()
84 | headers.reserveCapacity(headers.count)
85 |
86 | // Schnüff, we don't get NIO's `compareCaseInsensitiveASCIIBytes`
87 | for ( name, value ) in self {
88 | // This is all not good. But neither is the JSON gateway :-)
89 | if name.caseInsensitiveCompare("Set-Cookie") == .orderedSame ||
90 | name.caseInsensitiveCompare("Cookie") == .orderedSame
91 | {
92 | cookies.append(value)
93 | }
94 | else {
95 | if let existing = headers.removeValue(forKey: name) {
96 | // Don't know, SwiftLambda 0.4 dropped the multiheaders? What should
97 | // we do for dupes?
98 | if value .isEmpty {}
99 | else if existing.isEmpty { headers[name] = value }
100 | else { headers[name] = existing + ", " + value }
101 | }
102 | else {
103 | headers[name] = value
104 | }
105 | }
106 | }
107 |
108 | return ( headers : headers.isEmpty ? nil : headers,
109 | cookies : cookies.isEmpty ? nil : cookies )
110 | }
111 | }
112 |
113 | internal extension NIOHTTP1.HTTPResponseStatus {
114 |
115 | @inlinable
116 | var asLambda : AWSLambdaEvents.HTTPResponseStatus { // why, o why
117 | return .init(code: UInt(code), reasonPhrase: reasonPhrase)
118 | }
119 | }
120 |
121 | internal extension AWSLambdaEvents.HTTPResponseStatus {
122 |
123 | @inlinable
124 | var asNIO : NIOHTTP1.HTTPResponseStatus { // why, o why
125 | return .init(statusCode: Int(code),
126 | reasonPhrase: reasonPhrase ?? "HTTP Status \(code)")
127 | }
128 | }
129 | #endif // canImport(AWSLambdaEvents)
130 |
--------------------------------------------------------------------------------
/Sources/MacroLambdaCore/LambdaRequest.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LambdaRequest.swift
3 | // MacroLambda
4 | //
5 | // Created by Helge Heß
6 | // Copyright © 2020 ZeeZide GmbH. All rights reserved.
7 | //
8 |
9 | #if canImport(AWSLambdaEvents)
10 |
11 | import struct Logging.Logger
12 | import struct NIOHTTP1.HTTPRequestHead
13 | import enum AWSLambdaEvents.APIGateway
14 | import struct MacroCore.Buffer
15 | import protocol MacroCore.EnvironmentKey
16 | import class http.IncomingMessage
17 |
18 | public extension IncomingMessage {
19 |
20 | convenience init(lambdaRequest : APIGateway.V2.Request,
21 | log : Logger = .init(label: "μ.http"))
22 | {
23 | // version doesn't matter, we don't really do HTTP
24 | var head = HTTPRequestHead(
25 | version : .init(major: 1, minor: 1),
26 | method : lambdaRequest.context.http.method.asNIO,
27 | uri : lambdaRequest.context.http.path
28 | )
29 | head.headers = lambdaRequest.headers.asNIO
30 |
31 | if let cookies = lambdaRequest.cookies, !cookies.isEmpty {
32 | // So our "connect" module expects them in the headers, so we'd need
33 | // to serialize them again ...
34 | // The `IncomingMessage` also has a `cookies` getter, but I think that
35 | // isn't cached.
36 | for cookie in cookies { // that is weird too, is it right?
37 | head.headers.add(name: "Cookie", value: cookie)
38 | }
39 | }
40 |
41 | // TBD: there is also "pathParameters", what is that, URL fragments (#)?
42 | if let pathParams = lambdaRequest.pathParameters, !pathParams.isEmpty {
43 | log.warning("ignoring lambda path parameters: \(pathParams)")
44 | }
45 |
46 | if let qsParameters = lambdaRequest.queryStringParameters,
47 | !qsParameters.isEmpty
48 | {
49 | // TBD: is that included in the path?
50 | var isFirst = false
51 | if !head.uri.contains("?") { head.uri.append("?"); isFirst = true }
52 | for ( key, value ) in qsParameters {
53 | if isFirst { isFirst = false }
54 | else { head.uri += "&" }
55 |
56 | head.uri +=
57 | key.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)
58 | ?? key
59 | head.uri += "="
60 | head.uri +=
61 | value.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)
62 | ?? value
63 | }
64 | }
65 |
66 | self.init(head, socket: nil, log: log)
67 |
68 | // and keep the whole thing
69 | lambdaGatewayRequest = lambdaRequest
70 | }
71 |
72 | internal func sendLambdaBody(_ lambdaRequest: APIGateway.V2.Request) {
73 | defer { push(nil) }
74 |
75 | guard let body = lambdaRequest.body else { return }
76 | do {
77 | if lambdaRequest.isBase64Encoded {
78 | push(try Buffer.from(body, "base64"))
79 | }
80 | else {
81 | push(try Buffer.from(body))
82 | }
83 | }
84 | catch {
85 | emit(error: error)
86 | }
87 | }
88 | }
89 |
90 |
91 | enum LambdaRequestKey: EnvironmentKey {
92 | static let defaultValue : APIGateway.V2.Request? = nil
93 | static let loggingKey = "lambda-request"
94 | }
95 |
96 | public extension IncomingMessage {
97 |
98 | var lambdaGatewayRequest: APIGateway.V2.Request? {
99 | set { environment[LambdaRequestKey.self] = newValue }
100 | get { return environment[LambdaRequestKey.self] }
101 | }
102 | }
103 | #endif // canImport(AWSLambdaEvents)
104 |
--------------------------------------------------------------------------------
/Sources/MacroLambdaCore/LambdaResponse.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LambdaResponse.swift
3 | // MacroLambda
4 | //
5 | // Created by Helge Heß
6 | // Copyright © 2020-2021 ZeeZide GmbH. All rights reserved.
7 | //
8 |
9 | #if canImport(AWSLambdaEvents)
10 | import enum AWSLambdaEvents.APIGateway
11 | import class http.ServerResponse
12 |
13 | extension ServerResponse {
14 |
15 | var asLambdaGatewayResponse: APIGateway.V2.Response {
16 | assert(writableEnded, "sending ServerResponse which didn't end?!")
17 |
18 | let ( headers, cookies ) = self.headers.asLambda()
19 |
20 | let body : String? = {
21 | guard let writtenContent = writableBuffer, !writtenContent.isEmpty else {
22 | return nil
23 | }
24 |
25 | // TBD: We could make this more tolerant and use a String if the content
26 | // is textual and can be converted to UTF-8? Would make it faster as
27 | // well.
28 | do {
29 | return try writtenContent.toString("base64")
30 | }
31 | catch { // FIXME: make throwing
32 | log.error("could not convert body to base64: \(error)")
33 | return nil
34 | }
35 | }()
36 |
37 | return .init(statusCode : status.asLambda,
38 | headers : headers,
39 | body : body,
40 | isBase64Encoded : body != nil ? true : false,
41 | cookies : cookies)
42 | }
43 | }
44 |
45 | #endif // canImport(AWSLambdaEvents)
46 |
--------------------------------------------------------------------------------
/Sources/MacroLambdaCore/LambdaServer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LambdaServer.swift
3 | // MacroLambda
4 | //
5 | // Created by Helge Heß
6 | // Copyright © 2020 ZeeZide GmbH. All rights reserved.
7 | //
8 |
9 | #if canImport(AWSLambdaEvents) && canImport(AWSLambdaRuntime)
10 |
11 | import func Foundation.setenv
12 | import func Foundation.exit
13 | import struct Logging.Logger
14 | import class NIO.EventLoopFuture
15 | import class MacroCore.ErrorEmitter
16 | import enum MacroCore.EventListenerSet
17 | import struct MacroCore.Buffer
18 | import class http.IncomingMessage
19 | import class http.ServerResponse
20 | import protocol AWSLambdaRuntime.EventLoopLambdaHandler
21 | import enum AWSLambdaRuntime.Lambda
22 | import enum AWSLambdaEvents.APIGateway
23 | import express
24 |
25 | extension lambda {
26 |
27 | /**
28 | * An `http.Server` lookalike, but for AWS Lambda functions addressed using
29 | * the AWS API Gateway V2.
30 | *
31 | * Create those server objects using `lambda.createServer`, example:
32 | *
33 | * let server = lambda.createServer { req, res in
34 | * req.log.info("request arrived in Macro land: \(req.url)")
35 | * res.writeHead(200, [ "Content-Type": "text/html" ])
36 | * res.end("Hello World
")
37 | * }
38 | * server.run()
39 | *
40 | * Note that the `run` function never returns.
41 | */
42 | open class Server: ErrorEmitter {
43 |
44 | public let log : Logger
45 | private var didRetain = false
46 |
47 | @usableFromInline
48 | init(log: Logger = .init(label: "μ.http")) {
49 | self.log = log
50 | super.init()
51 | }
52 | deinit {
53 | if didRetain {
54 | core.release()
55 | didRetain = false
56 | }
57 | }
58 |
59 | override open func emit(error: Error) {
60 | log.error("server error: \(error)")
61 | super.emit(error: error)
62 | }
63 |
64 |
65 | // MARK: - Events
66 |
67 | // Note: This does NOT support once!
68 | private var _requestListeners =
69 | EventListenerSet<( IncomingMessage, ServerResponse )>()
70 | private var _expectListeners =
71 | EventListenerSet<( IncomingMessage, ServerResponse )>()
72 | private var _listeningListeners =
73 | EventListenerSet()
74 |
75 | private var hasRequestListeners : Bool {
76 | return !_requestListeners.isEmpty
77 | }
78 |
79 | @discardableResult
80 | public func onRequest(execute:
81 | @escaping ( IncomingMessage, ServerResponse ) -> Void) -> Self
82 | {
83 | _requestListeners.add(execute)
84 | return self
85 | }
86 | @discardableResult
87 | public func onCheckExpectation(execute:
88 | @escaping ( IncomingMessage, ServerResponse ) -> Void) -> Self
89 | {
90 | _expectListeners.add(execute)
91 | return self
92 | }
93 |
94 | @discardableResult
95 | public func onListening(execute: @escaping ( Server ) -> Void) -> Self {
96 | _listeningListeners.add(execute)
97 | if listening { execute(self) }
98 | return self
99 | }
100 |
101 | private func emitExpect(request: IncomingMessage, response: ServerResponse)
102 | -> Bool
103 | {
104 | var listeners = _expectListeners // Note: No `once` support!
105 | if listeners.isEmpty {
106 | response.status = .expectationFailed
107 | response.end()
108 | return false
109 | }
110 | else {
111 | listeners.emit(( request, response ))
112 | return true
113 | }
114 | }
115 |
116 |
117 | // MARK: - Handle Requests
118 |
119 | private func handle(request: IncomingMessage, response: ServerResponse)
120 | -> Bool
121 | {
122 | // aka onRequest
123 | var listeners = _requestListeners // Note: No `once` support!
124 | guard !listeners.isEmpty else { return false }
125 |
126 | listeners.emit(( request, response ))
127 | return true
128 | }
129 |
130 | private func feed(request: IncomingMessage, data: Buffer) {
131 | request.push(data)
132 | }
133 |
134 | private func end(request: IncomingMessage) {
135 | assert(!request.complete)
136 | #if false // we don't have that
137 | request.complete = true
138 | #endif
139 | }
140 |
141 | private func cancel(request: IncomingMessage, response: ServerResponse) {
142 | // TODO / TBD
143 | }
144 |
145 | private func emitError(_ error: Swift.Error,
146 | transaction: ( IncomingMessage, ServerResponse )?)
147 | {
148 | if let ( request, _ ) = transaction {
149 | // TBD: we also need to tell the response if the channel was closed?
150 | request.emit(error: error)
151 | }
152 | else {
153 | emit(error: error)
154 | }
155 | }
156 |
157 | private var didRun = false
158 |
159 | open var listening : Bool {
160 | return didRun
161 | }
162 |
163 | open func run(onRun : ( ( Server ) -> Void)? = nil) -> Never {
164 | assert(!didRun)
165 | guard !didRun else {
166 | fatalError("run called twice, which is impossible :-)")
167 | }
168 | didRun = true
169 |
170 | // Let "onListener" listeners know that we started running.
171 | if let onRun = onRun { onRun(self) }
172 | var listeners = _listeningListeners
173 | _listeningListeners.removeAll()
174 | listeners.emit(self)
175 |
176 | // Note: I think we can't set the core.eventLoopGroup here, because the
177 | // eventLoop is only available in the Lambda context? Not sure who
178 | // would even touch the MacroCore loop (vs using current).
179 |
180 | struct APIGatewayProxyLambda: EventLoopLambdaHandler {
181 | typealias In = APIGateway.V2.Request
182 | typealias Out = APIGateway.V2.Response
183 |
184 | let server : Server
185 |
186 | func handle(context: Lambda.Context, event: In) -> EventLoopFuture
187 | {
188 | let promise = context.eventLoop.makePromise(of: Out.self)
189 | server.handle(context: context, request: event) { result in
190 | promise.completeWith(result)
191 | }
192 | return promise.futureResult
193 | }
194 | }
195 | let proxy = APIGatewayProxyLambda(server: self)
196 | Lambda.run(proxy)
197 | Foundation.exit(0) // Because `run` is not marked as Never (Issue #151)
198 | }
199 |
200 | private func handle(context : Lambda.Context,
201 | request : APIGateway.V2.Request,
202 | callback : @escaping
203 | ( Result ) -> Void)
204 | {
205 | guard !self._requestListeners.isEmpty else {
206 | assertionFailure("no request listeners?!")
207 | return callback(.failure(ServerError.noRequestListeners))
208 | }
209 |
210 | let req = IncomingMessage(lambdaRequest: request, log: context.logger)
211 | let res = ServerResponse(unsafeChannel: nil, log: context.logger)
212 | res.cork()
213 | res.request = req
214 |
215 | // The transaction ends when the response is done, not when the
216 | // request was read completely!
217 | var didFinish = false
218 |
219 | res.onceFinish {
220 | // convert res to gateway Response and call callback
221 | guard !didFinish else {
222 | return context.logger.error("TX already finished!")
223 | }
224 | didFinish = true
225 |
226 | callback(.success(res.asLambdaGatewayResponse))
227 | }
228 |
229 | res.onError { error in
230 | guard !didFinish else {
231 | return context.logger.error("Follow up error: \(error)")
232 | }
233 | didFinish = true
234 | callback(.failure(error))
235 | }
236 |
237 | // TODO: Process Expect. It's not really "ahead of sending the body",
238 | // but we still need to validate the preconditions. http.Server
239 | // has code for this. Do the same.
240 |
241 | do { // onRequest
242 | var listeners = self._requestListeners // Note: No `once` support!
243 | guard !listeners.isEmpty else {
244 | didFinish = true
245 | return callback(.failure(ServerError.noRequestListeners))
246 | }
247 |
248 | listeners.emit(( req, res ))
249 | }
250 |
251 | // For a streaming push, we do the lambda-send here, after announcing the
252 | // head.
253 | if !res.writableEnded { // response is already closed
254 | req.sendLambdaBody(request)
255 | }
256 | else {
257 | assert(didFinish)
258 | }
259 | }
260 | }
261 |
262 | enum ServerError: Swift.Error {
263 | case noRequestListeners
264 | }
265 | }
266 |
267 | #endif // canImport(AWSLambdaEvents) && canImport(AWSLambdaRuntime)
268 |
--------------------------------------------------------------------------------
/Sources/MacroLambdaCore/Process.swift:
--------------------------------------------------------------------------------
1 | //
2 | // lambda.swift
3 | // MacroLambda
4 | //
5 | // Created by Helge Heß
6 | // Copyright © 2020 ZeeZide GmbH. All rights reserved.
7 | //
8 |
9 | import let xsys.getenv
10 | import enum MacroCore.process
11 |
12 | public extension process {
13 |
14 | #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) || os(Windows)
15 | /**
16 | * Returns true if the server is running in an actual AWS Lambda
17 | * environment. (I.e. never true on macOS/iOS/etc)
18 | */
19 | static let isRunningInLambda = false
20 | #else
21 | /**
22 | * Returns true if the server is running in an actual AWS Lambda
23 | * environment. (I.e. never true on macOS/iOS/etc)
24 | */
25 | static let isRunningInLambda : Bool = {
26 | guard let s = xsys.getenv("AWS_LAMBDA_FUNCTION_NAME") else {
27 | return false
28 | }
29 | return s[0] != 0
30 | }()
31 | #endif
32 | }
33 |
--------------------------------------------------------------------------------
/Sources/MacroLambdaCore/README.md:
--------------------------------------------------------------------------------
1 | # Macro http.lambda
2 |
3 | The files within simulate `http.createServer` but for AWS Lambda functions
4 | addressed using the
5 | AWS [API Gateway](https://aws.amazon.com/api-gateway/) V2.
6 |
7 | Tutorial:
8 | [Create your first HTTP endpoint with Swift on AWS Lambda](https://fabianfett.de/swift-on-aws-lambda-creating-your-first-http-endpoint))
9 |
10 | Requests are regular `IncomingMessage` objects, responses are regular
11 | `ServerResponse` objects.
12 |
13 | Requests carry the additional `lambdaGatewayRequest` property to provide
14 | access to the full Lambda JSON structure.
15 |
16 | ## Example
17 |
18 | ```swift
19 | let server = lambda.createServer { req, res in
20 | req.log.info("request arrived in Macro land: \(req.url)")
21 | res.send("Hello You!")
22 | }
23 | server.run()
24 | ```
25 |
26 | Note that the `run` function never returns.
27 |
--------------------------------------------------------------------------------
/Sources/MacroLambdaCore/lambda.swift:
--------------------------------------------------------------------------------
1 | //
2 | // lambda.swift
3 | // MacroLambda
4 | //
5 | // Created by Helge Heß
6 | // Copyright © 2020 ZeeZide GmbH. All rights reserved.
7 | //
8 |
9 | #if canImport(AWSLambdaRuntime)
10 |
11 | public enum lambda {}
12 |
13 | #if canImport(AWSLambdaEvents)
14 |
15 | import func Foundation.setenv
16 | import class http.IncomingMessage
17 | import class http.ServerResponse
18 |
19 | public extension lambda {
20 |
21 | /**
22 | * An `http.createServer` lookalike, but for AWS Lambda functions addressed
23 | * using the AWS API Gateway V2.
24 | *
25 | * Requests are regular `IncomingMessage` objects, responses are regular
26 | * `ServerResponse` objects.
27 | *
28 | * Requests carry the additional `lambdaGatewayRequest` property to provide
29 | * access to the full Lambda JSON structure.
30 | *
31 | * Example:
32 | *
33 | * let server = createServer { req, res in
34 | * req.log.info("request arrived in Macro land: \(req.url)")
35 | * res.writeHead(200, [ "Content-Type": "text/html" ])
36 | * res.end("Hello World
")
37 | * }
38 | * server.run()
39 | *
40 | * Note that the `run` function never returns.
41 | */
42 | @inlinable
43 | @discardableResult
44 | static func createServer(handler: ((IncomingMessage, ServerResponse) -> Void)?
45 | = nil)
46 | -> Server
47 | {
48 | // This is used the first time MacroCore.shared eventloop is accessed,
49 | // it shouldn't ever, but if it is, we just fork one thread :-)
50 | let magicKey = "macro.core.numthreads"
51 | setenv(magicKey, "1", 0 /* do not overwrite */)
52 |
53 | let server = Server()
54 | if let handler = handler { _ = server.onRequest(execute: handler) }
55 | return server
56 | }
57 | }
58 |
59 | #endif // canImport(AWSLambdaEvents)
60 | #endif // canImport(AWSLambdaRuntime)
61 |
--------------------------------------------------------------------------------