From be3245ae3913dc5b184a5899f6d560fc3b82e54a Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sat, 22 Feb 2025 10:21:07 +0100 Subject: [PATCH] Initial commit --- .dockerignore | 2 + .gitignore | 10 + App/Sources/App.swift | 20 ++ App/Sources/AppOptions.swift | 20 ++ Dockerfile | 87 ++++++++ LICENSE | 201 ++++++++++++++++++ .../Internal/Environment+Properties.swift | 11 + .../Internal/LoggerLevel+Conformances.swift | 9 + Library/Sources/Public/AppArguments.swift | 11 + Library/Sources/Public/AppBuilder.swift | 69 ++++++ Package.swift | 45 ++++ README.md | 10 + Test/Sources/Cases/Public/AppTests.swift | 33 +++ Test/Sources/Helpers/TestArguments.swift | 12 ++ docker-compose.yml | 7 + 15 files changed, 547 insertions(+) create mode 100644 .dockerignore create mode 100644 .gitignore create mode 100644 App/Sources/App.swift create mode 100644 App/Sources/AppOptions.swift create mode 100644 Dockerfile create mode 100644 LICENSE create mode 100644 Library/Sources/Internal/Environment+Properties.swift create mode 100644 Library/Sources/Internal/LoggerLevel+Conformances.swift create mode 100644 Library/Sources/Public/AppArguments.swift create mode 100644 Library/Sources/Public/AppBuilder.swift create mode 100644 Package.swift create mode 100644 README.md create mode 100644 Test/Sources/Cases/Public/AppTests.swift create mode 100644 Test/Sources/Helpers/TestArguments.swift create mode 100644 docker-compose.yml diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..4e05543 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +.build +.git diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..edcd2d6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +.DS_Store +/.build +/.devContainer +/.swiftpm +/.vscode +/Packages +/*.xcodeproj +xcuserdata/ +.env.* +.env diff --git a/App/Sources/App.swift b/App/Sources/App.swift new file mode 100644 index 0000000..77ab0fe --- /dev/null +++ b/App/Sources/App.swift @@ -0,0 +1,20 @@ +import AppLibrary +import ArgumentParser + +@main +struct App: AsyncParsableCommand { + + // MARK: Properties + + @OptionGroup var options: Options + + // MARK: Functions + + mutating func run() async throws { + let builder = AppBuilder(name: "DocCRepo") + let app = try await builder(options) + + try await app.runService() + } + +} diff --git a/App/Sources/AppOptions.swift b/App/Sources/AppOptions.swift new file mode 100644 index 0000000..a6d835f --- /dev/null +++ b/App/Sources/AppOptions.swift @@ -0,0 +1,20 @@ +import AppLibrary +import ArgumentParser +import Logging + +extension App { + struct Options: AppArguments, ParsableArguments { + + // MARK: Properties + + @Option(name: .shortAndLong) + var hostname: String = "127.0.0.1" + + @Option(name: .shortAndLong) + var port: Int = 8080 + + @Option(name: .shortAndLong) + var logLevel: Logger.Level? + + } +} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e6cf30a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,87 @@ +# ================================ +# Build image +# ================================ +FROM swift:6.0.3-noble AS build + +# Install OS updates +RUN export DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true \ + && apt-get -q update \ + && apt-get -q dist-upgrade -y \ + && apt-get install -y libjemalloc-dev \ + && rm -rf /var/lib/apt/lists/* + +# Set up a build area +WORKDIR /build + +# First just resolve dependencies. +# This creates a cached layer that can be reused +# as long as your Package.swift/Package.resolved +# files do not change. +COPY ./Package.* ./ +RUN swift package resolve + +# Copy entire repo into container +COPY . . + +# Build the application, with optimizations, with static linking, and using jemalloc +RUN swift build -c release \ + --product "App" \ + --static-swift-stdlib \ + -Xlinker -ljemalloc + +# Switch to the staging area +WORKDIR /staging + +# Copy main executable to staging area +RUN cp "$(swift build --package-path /build -c release --show-bin-path)/App" ./ + +# Copy static swift backtracer binary to staging area +RUN cp "/usr/libexec/swift/linux/swift-backtrace-static" ./ + +# Copy resources bundled by SPM to staging area +RUN find -L "$(swift build --package-path /build -c release --show-bin-path)/" -regex '.*\.resources$' -exec cp -Ra {} ./ \; + +# Copy any resources from the public directory and views directory if the directories exist +# Ensure that by default, neither the directory nor any of its contents are writable. +RUN [ -d /build/public ] && { mv /build/public ./public && chmod -R a-w ./public; } || true + +# ================================ +# Run image +# ================================ +FROM ubuntu:noble + +# Make sure all system packages are up to date, and install only essential packages. +RUN export DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true \ + && apt-get -q update \ + && apt-get -q dist-upgrade -y \ + && apt-get -q install -y \ + libjemalloc2 \ + ca-certificates \ + tzdata \ +# If your app or its dependencies import FoundationNetworking, also install `libcurl4`. + # libcurl4 \ +# If your app or its dependencies import FoundationXML, also install `libxml2`. + # libxml2 \ + && rm -r /var/lib/apt/lists/* + +# Create a hummingbird user and group with /app as its home directory +RUN useradd --user-group --create-home --system --skel /dev/null --home-dir /app hummingbird + +# Switch to the new home directory +WORKDIR /app + +# Copy built executable and any staged resources from builder +COPY --from=build --chown=hummingbird:hummingbird /staging /app + +# Provide configuration needed by the built-in crash reporter and some sensible default behaviors. +ENV SWIFT_BACKTRACE=enable=yes,sanitize=yes,threads=all,images=all,interactive=no,swift-backtrace=./swift-backtrace-static + +# Ensure all further commands run as the hummingbird user +USER hummingbird:hummingbird + +# Let Docker bind to port 8080 +EXPOSE 8080 + +# Start the Hummingbird service when the image is run, default to listening on 8080 in production environment +ENTRYPOINT ["./App"] +CMD ["--hostname", "0.0.0.0", "--port", "8080"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..05b900f --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2025 Röck+Cöde + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/Library/Sources/Internal/Environment+Properties.swift b/Library/Sources/Internal/Environment+Properties.swift new file mode 100644 index 0000000..99d4d41 --- /dev/null +++ b/Library/Sources/Internal/Environment+Properties.swift @@ -0,0 +1,11 @@ +import Hummingbird + +extension Environment { + + // MARK: Computed + + public var logLevel: String? { + self.get("LOG_LEVEL") + } + +} diff --git a/Library/Sources/Internal/LoggerLevel+Conformances.swift b/Library/Sources/Internal/LoggerLevel+Conformances.swift new file mode 100644 index 0000000..0d1abb6 --- /dev/null +++ b/Library/Sources/Internal/LoggerLevel+Conformances.swift @@ -0,0 +1,9 @@ +import ArgumentParser +import Logging + +/// Extend `Logger.Level` so it can be used as an argument +#if hasFeature(RetroactiveAttribute) +extension Logger.Level: @retroactive ExpressibleByArgument {} +#else +extension Logger.Level: ExpressibleByArgument {} +#endif diff --git a/Library/Sources/Public/AppArguments.swift b/Library/Sources/Public/AppArguments.swift new file mode 100644 index 0000000..40039e1 --- /dev/null +++ b/Library/Sources/Public/AppArguments.swift @@ -0,0 +1,11 @@ +import Logging + +public protocol AppArguments { + + // MARK: Properties + + var hostname: String { get } + var logLevel: Logger.Level? { get } + var port: Int { get } + +} diff --git a/Library/Sources/Public/AppBuilder.swift b/Library/Sources/Public/AppBuilder.swift new file mode 100644 index 0000000..1998089 --- /dev/null +++ b/Library/Sources/Public/AppBuilder.swift @@ -0,0 +1,69 @@ +import Hummingbird +import Logging + +public struct AppBuilder { + + // MARK: Properties + + private let environment: Environment + private let name: String + + // MARK: Initialisers + + public init(name: String) { + self.environment = Environment() + self.name = name + } + + // MARK: Functions + + public func callAsFunction( + _ arguments: some AppArguments + ) async throws -> some ApplicationProtocol { + let logger = { + var logger = Logger(label: name) + + logger.logLevel = arguments.logLevel + ?? environment.logLevel.flatMap { Logger.Level(rawValue: $0) ?? .info } + ?? .info + + return logger + }() + + let router = router(logger: logger) + + return Application( + router: router, + configuration: .init( + address: .hostname(arguments.hostname, port: arguments.port), + serverName: name + ), + logger: logger + ) + } + +} + +// MARK: - Helpers + +private extension AppBuilder { + + // MARK: Type aliases + + typealias AppRequestContext = BasicRequestContext + + // MARK: Functions + + func router(logger: Logger) -> Router { + let router = Router() + + router.add(middleware: LogRequestsMiddleware(logger.logLevel)) + + router.get("/") { _,_ in + "" + } + + return router + } + +} diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..71f9e66 --- /dev/null +++ b/Package.swift @@ -0,0 +1,45 @@ +// swift-tools-version:6.0 + +import PackageDescription + +let package = Package( + name: "DocCRepo", + platforms: [ + .macOS(.v14) + ], + products: [ + .executable(name: "App", targets: ["App"]), + .library(name: "AppLibrary", targets: ["AppLibrary"]) + ], + dependencies: [ + .package(url: "https://github.com/hummingbird-project/hummingbird.git", from: "2.0.0"), + .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.3.0") + ], + targets: [ + .executableTarget( + name: "App", + dependencies: [ + .product(name: "ArgumentParser", package: "swift-argument-parser"), + .product(name: "Hummingbird", package: "hummingbird"), + .target(name: "AppLibrary") + ], + path: "App" + ), + .target( + name: "AppLibrary", + dependencies: [ + .product(name: "ArgumentParser", package: "swift-argument-parser"), + .product(name: "Hummingbird", package: "hummingbird") + ], + path: "Library" + ), + .testTarget( + name: "AppTests", + dependencies: [ + .product(name: "HummingbirdTesting", package: "hummingbird"), + .target(name: "AppLibrary") + ], + path: "Test" + ) + ] +) diff --git a/README.md b/README.md new file mode 100644 index 0000000..e13e70d --- /dev/null +++ b/README.md @@ -0,0 +1,10 @@ +

+ + + + +

+ +# Hummingbird project template + +This is a template for your new [Hummingbird](https://wwww.hummingbird.codes) project. diff --git a/Test/Sources/Cases/Public/AppTests.swift b/Test/Sources/Cases/Public/AppTests.swift new file mode 100644 index 0000000..7622998 --- /dev/null +++ b/Test/Sources/Cases/Public/AppTests.swift @@ -0,0 +1,33 @@ +import AppLibrary +import Hummingbird +import HummingbirdTesting +import Testing + +struct AppTests { + + // MARK: Properties + + private let arguments = TestArguments() + private let builder = AppBuilder(name: "DocCRepo") + + // MARK: Route tests + + @Test(arguments: ["/"]) + func routes(_ uri: String) async throws { + let app = try await builder(arguments) + + try await app.test(.router) { client in + try await client.execute(uri: uri, method: .get) { response in + #expect(response.status == .ok) + #expect(response.body == .empty) + } + } + } + +} + +// MARK: ByteBuffer+Constants + +private extension ByteBuffer { + static let empty = ByteBuffer(string: "") +} diff --git a/Test/Sources/Helpers/TestArguments.swift b/Test/Sources/Helpers/TestArguments.swift new file mode 100644 index 0000000..c2f0b21 --- /dev/null +++ b/Test/Sources/Helpers/TestArguments.swift @@ -0,0 +1,12 @@ +import AppLibrary +import Logging + +struct TestArguments: AppArguments { + + // MARK: Properties + + let hostname = "127.0.0.1" + let port = 0 + let logLevel: Logger.Level? = .trace + +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..cbf9270 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,7 @@ +services: + app: + build: + context: . + ports: + - 3000:8080 + command: ["--hostname", "0.0.0.0", "--port", "8080"]