Project updates from Template #1

Merged
javier merged 123 commits from project/update-from-template into main 2026-09-04 13:40:36 +00:00
16 changed files with 31 additions and 26 deletions
Showing only changes of commit a0f5c79434 - Show all commits
@@ -26,8 +26,13 @@
buildConfiguration = "Debug" buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES" shouldUseLaunchSchemeArgsEnv = "YES">
shouldAutocreateTestPlan = "YES"> <TestPlans>
<TestPlanReference
reference = "container:Tests/Website.xctestplan"
default = "YES">
</TestPlanReference>
</TestPlans>
<Testables> <Testables>
<TestableReference <TestableReference
skipped = "NO"> skipped = "NO">
@@ -18,8 +18,8 @@
skipped = "NO"> skipped = "NO">
<BuildableReference <BuildableReference
BuildableIdentifier = "primary" BuildableIdentifier = "primary"
BlueprintIdentifier = "WebsiteCoreTests" BlueprintIdentifier = "WebsiteLibraryTests"
BuildableName = "WebsiteCoreTests" BuildableName = "WebsiteLibraryTests"
ReferencedContainer = "container:"> ReferencedContainer = "container:">
</BuildableReference> </BuildableReference>
</TestableReference> </TestableReference>
+5 -5
View File
@@ -15,7 +15,7 @@ let package = Package(
name: "Website", name: "Website",
targets: [ targets: [
"Website", "Website",
"WebsiteCore", "WebsiteLibrary",
] ]
) )
], ],
@@ -56,7 +56,7 @@ let package = Package(
name: "Website", name: "Website",
dependencies: [ dependencies: [
.byName(name: "Persistence"), .byName(name: "Persistence"),
.byName(name: "WebsiteCore"), .byName(name: "WebsiteLibrary"),
.product( .product(
name: "Configuration", name: "Configuration",
package: "swift-configuration" package: "swift-configuration"
@@ -73,7 +73,7 @@ let package = Package(
path: "Sources/App" path: "Sources/App"
), ),
.target( .target(
name: "WebsiteCore", name: "WebsiteLibrary",
dependencies: [ dependencies: [
.byName(name: "Localization"), .byName(name: "Localization"),
.byName(name: "Persistence"), .byName(name: "Persistence"),
@@ -113,9 +113,9 @@ let package = Package(
path: "Tests/App" path: "Tests/App"
), ),
.testTarget( .testTarget(
name: "WebsiteCoreTests", name: "WebsiteLibraryTests",
dependencies: [ dependencies: [
.byName(name: "WebsiteCore"), .byName(name: "WebsiteLibrary"),
.product( .product(
name: "Elementary", name: "Elementary",
package: "elementary" package: "elementary"
+4 -4
View File
@@ -4,7 +4,7 @@ The **Loud** public website service — a [Hummingbird](https://github.com/hummi
## Overview ## Overview
The service: The service:
- Serves the landing page at `GET /` (rendered once per supported language with [Elementary](https://github.com/elementary-swift/elementary) and cached). - Serves the landing page at `GET /` (rendered once per supported language with [Elementary](https://github.com/elementary-swift/elementary) and cached).
- Negotiates each request's language from its `Accept-Language` header against the languages in the `WebsiteCore` String Catalog, falling back to the default (`en`); pages are served from the per-language cache with `Content-Language` and `Vary: Accept-Language` headers. - Negotiates each request's language from its `Accept-Language` header against the languages in the `WebsiteLibrary` String Catalog, falling back to the default (`en`); pages are served from the per-language cache with `Content-Language` and `Vary: Accept-Language` headers.
- Answers a liveness check at `GET /health` with a static JSON payload, and a readiness check at `GET /health/ready` that reports whether the database is reachable (`200` ready / `503` unavailable). - Answers a liveness check at `GET /health` with a static JSON payload, and a readiness check at `GET /health/ready` that reports whether the database is reachable (`200` ready / `503` unavailable).
- Serves static files (CSS, JS, icons, manifest, `robots.txt`) from `Resources/Static` via Hummingbird's `FileMiddleware`, tagged with media-type-specific `Cache-Control`. - Serves static files (CSS, JS, icons, manifest, `robots.txt`) from `Resources/Static` via Hummingbird's `FileMiddleware`, tagged with media-type-specific `Cache-Control`.
- Returns a custom HTML 404 page, localized like the landing page, for any request that matches neither a route nor a static file. - Returns a custom HTML 404 page, localized like the landing page, for any request that matches neither a route nor a static file.
@@ -22,10 +22,10 @@ Two SwiftPM targets:
| Target | Kind | Path | Role | | Target | Kind | Path | Role |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| `Website` | executable | `Sources/App` | Entry point: reads configuration, builds the persistence service, and either serves the website or runs the migrate-and-exit mode. | | `Website` | executable | `Sources/App` | Entry point: reads configuration, builds the persistence service, and either serves the website or runs the migrate-and-exit mode. |
| `WebsiteCore` | library | `Sources/Library` | Controllers, middlewares, pages, cached responses, and configuration helpers. | | `WebsiteLibrary` | library | `Sources/Library` | Controllers, middlewares, pages, cached responses, and configuration helpers. |
The `Website` executable depends on two local packages: The `Website` executable depends on two local packages:
- `Localization` (`Packages/Localization`) — the `Localize` and `Negotiate` helpers and the `LanguageList` of catalog languages (used by `WebsiteCore`). - `Localization` (`Packages/Localization`) — the `Localize` and `Negotiate` helpers and the `LanguageList` of catalog languages (used by `WebsiteLibrary`).
- `Persistence` (`Packages/Persistence`) — the Fluent-based data layer: the `Driver` selector, the `Service` factory that builds the `Fluent` service, the `PrepareDB` registrar that declares the migrations, and the `Probe` consulted by the readiness check; the models, migrations, and repositories stay internal to the package. It has no dependency on `swift-configuration`; the executable maps the `database.*` keys onto the driver. - `Persistence` (`Packages/Persistence`) — the Fluent-based data layer: the `Driver` selector, the `Service` factory that builds the `Fluent` service, the `PrepareDB` registrar that declares the migrations, and the `Probe` consulted by the readiness check; the models, migrations, and repositories stay internal to the package. It has no dependency on `swift-configuration`; the executable maps the `database.*` keys onto the driver.
The persistence backend runs as a `Fluent` service inside the application's ServiceLifecycle group, so it starts and stops alongside the HTTP server (which owns its connection-pool shutdown on graceful termination). The persistence backend runs as a `Fluent` service inside the application's ServiceLifecycle group, so it starts and stops alongside the HTTP server (which owns its connection-pool shutdown on graceful termination).
@@ -164,7 +164,7 @@ make pkg-test
# = swift test --disable-xctest --enable-code-coverage --enable-swift-testing --parallel # = swift test --disable-xctest --enable-code-coverage --enable-swift-testing --parallel
``` ```
Tests use the [Swift Testing](https://developer.apple.com/documentation/testing/) framework. The `Website.xctestplan` covers two targets: `WebsiteTests` (the executable/integration tests) and `WebsiteCoreTests` (the library unit tests). Tests use the [Swift Testing](https://developer.apple.com/documentation/testing/) framework. The `Website.xctestplan` covers two targets: `WebsiteTests` (the executable/integration tests) and `WebsiteLibraryTests` (the library unit tests).
The `Persistence` package has its own suite (run it from `Packages/Persistence`). Its tests run against the in-memory backend by default; the MySQL integration test is skipped unless a database is pointed at via `MYSQL_TEST_HOST` (with optional `MYSQL_TEST_PORT`/`NAME`/`USERNAME`/`PASSWORD`), so `swift test` stays runnable with no database: The `Persistence` package has its own suite (run it from `Packages/Persistence`). Its tests run against the in-memory backend by default; the MySQL integration test is skipped unless a database is pointed at via `MYSQL_TEST_HOST` (with optional `MYSQL_TEST_PORT`/`NAME`/`USERNAME`/`PASSWORD`), so `swift test` stays runnable with no database:
```sh ```sh
@@ -3,7 +3,7 @@ import Hummingbird
import HummingbirdCompression import HummingbirdCompression
import Logging import Logging
import Persistence import Persistence
import WebsiteCore import WebsiteLibrary
/// Builds the website application. /// Builds the website application.
/// ///
@@ -2,7 +2,7 @@ import Configuration
import Hummingbird import Hummingbird
import Logging import Logging
import Persistence import Persistence
import WebsiteCore import WebsiteLibrary
package extension ConfigReader { package extension ConfigReader {
+1 -1
View File
@@ -7,7 +7,7 @@ import NIOCore
import Testing import Testing
@testable import Website @testable import Website
@testable import WebsiteCore @testable import WebsiteLibrary
@Suite("App executable") @Suite("App executable")
struct AppTests { struct AppTests {
@@ -1,7 +1,7 @@
import Foundation import Foundation
import Testing import Testing
@testable import WebsiteCore @testable import WebsiteLibrary
@Suite("StaticFile enumeration") @Suite("StaticFile enumeration")
struct StaticFileTests { struct StaticFileTests {
@@ -2,7 +2,7 @@ import Elementary
import Foundation import Foundation
import Testing import Testing
@testable import WebsiteCore @testable import WebsiteLibrary
@Suite("ErrorPage page") @Suite("ErrorPage page")
struct ErrorPageTests { struct ErrorPageTests {
@@ -2,7 +2,7 @@ import Elementary
import Foundation import Foundation
import Testing import Testing
@testable import WebsiteCore @testable import WebsiteLibrary
@Suite("IndexPage page") @Suite("IndexPage page")
struct IndexPageTests { struct IndexPageTests {
@@ -5,7 +5,7 @@ import NIOCore
import Persistence import Persistence
import Testing import Testing
@testable import WebsiteCore @testable import WebsiteLibrary
@Suite("HealthController controller") @Suite("HealthController controller")
struct HealthControllerTests { struct HealthControllerTests {
@@ -3,7 +3,7 @@ import HummingbirdTesting
import NIOCore import NIOCore
import Testing import Testing
@testable import WebsiteCore @testable import WebsiteLibrary
@Suite("RootController controller") @Suite("RootController controller")
struct RootControllerTests { struct RootControllerTests {
@@ -4,7 +4,7 @@ import HummingbirdTesting
import NIOCore import NIOCore
import Testing import Testing
@testable import WebsiteCore @testable import WebsiteLibrary
@Suite("LocalizationMiddleware middleware") @Suite("LocalizationMiddleware middleware")
struct LocalizationMiddlewareTests { struct LocalizationMiddlewareTests {
@@ -3,7 +3,7 @@ import HummingbirdTesting
import NIOCore import NIOCore
import Testing import Testing
@testable import WebsiteCore @testable import WebsiteLibrary
@Suite("NotFoundMiddleware middleware") @Suite("NotFoundMiddleware middleware")
struct NotFoundMiddlewareTests { struct NotFoundMiddlewareTests {
@@ -2,7 +2,7 @@ import Hummingbird
import HummingbirdTesting import HummingbirdTesting
import Testing import Testing
@testable import WebsiteCore @testable import WebsiteLibrary
@Suite("SecurityHeadersMiddleware middleware") @Suite("SecurityHeadersMiddleware middleware")
struct SecurityHeadersMiddlewareTests { struct SecurityHeadersMiddlewareTests {
@@ -28,8 +28,8 @@
{ {
"target" : { "target" : {
"containerPath" : "container:", "containerPath" : "container:",
"identifier" : "WebsiteCoreTests", "identifier" : "WebsiteLibraryTests",
"name" : "WebsiteCoreTests" "name" : "WebsiteLibraryTests"
} }
} }
], ],