Skip to content

Add server example #19

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions Examples/server/.devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
{
"name": "Server",
"workspaceFolder": "/workspace/server",
"dockerComposeFile": [
"../docker-compose.yaml",
"docker-compose.yaml"
],
"service": "server",
"runServices": [
"flipt",
"jaeger",
"otel-collector",
"server"
],
"shutdownAction": "stopCompose",
"features": {
"ghcr.io/devcontainers/features/common-utils:2": {
"installZsh": "false",
"username": "server",
"upgradePackages": "false"
},
"ghcr.io/devcontainers/features/git:1": {
"version": "os-provided",
"ppa": "false"
}
},
"customizations": {
"vscode": {
"settings": {
"lldb.library": "/usr/lib/liblldb.so",
"swift.autoGenerateLaunchConfigurations": false
},
"extensions": [
"swiftlang.swift-vscode"
]
}
},
"forwardPorts": [8080, 8081, 16686],
"portsAttributes": {
"8080": {
"label": "API",
"onAutoForward": "notify"
},
"8081": {
"label": "Flipt"
},
"16686": {
"label": "Jaeger"
}
},
"remoteUser": "server"
}
6 changes: 6 additions & 0 deletions Examples/server/.devcontainer/docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
services:
server:
image: swift:6.1
command: sleep infinity
volumes:
- ..:/workspace:cached
50 changes: 50 additions & 0 deletions Examples/server/Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// swift-tools-version:6.0
import PackageDescription

let package = Package(
name: "server",
platforms: [.macOS(.v15)],
products: [
.executable(name: "server", targets: ["CTL"])
],
dependencies: [
.package(url: "https://github.com/swift-server/swift-service-lifecycle.git", from: "2.0.0"),
.package(url: "https://github.com/apple/swift-log.git", from: "1.5.0"),
.package(url: "https://github.com/apple/swift-distributed-tracing.git", from: "1.0.0"),
.package(url: "https://github.com/swift-otel/swift-otel.git", .upToNextMinor(from: "0.11.0")),
.package(url: "https://github.com/hummingbird-project/hummingbird.git", from: "2.0.0"),
.package(url: "https://github.com/hummingbird-project/hummingbird-auth.git", from: "2.0.0"),
.package(url: "https://github.com/swift-open-feature/swift-open-feature.git", branch: "main"),
.package(url: "https://github.com/swift-open-feature/swift-ofrep.git", branch: "main"),

// override HTTP Client until Tracing PR is merged
.package(url: "https://github.com/slashmo/async-http-client.git", branch: "feature/tracing"),
],
targets: [
.executableTarget(
name: "CTL",
dependencies: [
.target(name: "API"),
.product(name: "ServiceLifecycle", package: "swift-service-lifecycle"),
.product(name: "Logging", package: "swift-log"),
.product(name: "Tracing", package: "swift-distributed-tracing"),
.product(name: "OTel", package: "swift-otel"),
.product(name: "OTLPGRPC", package: "swift-otel"),
.product(name: "Hummingbird", package: "hummingbird"),
.product(name: "OpenFeature", package: "swift-open-feature"),
.product(name: "OpenFeatureTracing", package: "swift-open-feature"),
.product(name: "OFREP", package: "swift-ofrep"),
]
),
.target(
name: "API",
dependencies: [
.product(name: "ServiceLifecycle", package: "swift-service-lifecycle"),
.product(name: "Logging", package: "swift-log"),
.product(name: "Hummingbird", package: "hummingbird"),
.product(name: "HummingbirdAuth", package: "hummingbird-auth"),
.product(name: "OpenFeature", package: "swift-open-feature"),
]
),
]
)
3 changes: 3 additions & 0 deletions Examples/server/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Swift OFREP server example

An HTTP server that can be altered at runtime using feature flags powered by Swift OFREP.
38 changes: 38 additions & 0 deletions Examples/server/Sources/API/APIService.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift OpenFeature open source project
//
// Copyright (c) 2025 the Swift OpenFeature project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

import Hummingbird
import OpenFeature
import ServiceLifecycle

public struct APIService: Service {
private let app: Application<RouterResponder<APIRequestContext>>
private let client: OpenFeatureClient

public init(router: Router<APIRequestContext>) {
client = OpenFeatureSystem.client()

router
.addMiddleware {
AuthMiddleware()
EvaluationContextMiddleware()
}
.addRoutes(FeedController().routes)

app = Application(router: router)
}

public func run() async throws {
try await app.run()
}
}
29 changes: 29 additions & 0 deletions Examples/server/Sources/API/AuthMiddleware.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift OpenFeature open source project
//
// Copyright (c) 2025 the Swift OpenFeature project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

import HTTPTypes
import Hummingbird
import HummingbirdAuth

struct AuthMiddleware: AuthenticatorMiddleware {
typealias Context = APIRequestContext

func authenticate(request: Request, context: APIRequestContext) async throws -> User? {
guard let userID = request.headers[.userID] else { return nil }
return User(id: userID)
}
}

extension HTTPField.Name {
fileprivate static let userID = Self("X-User-Id")!
}
29 changes: 29 additions & 0 deletions Examples/server/Sources/API/EvaluationContextMiddleware.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift OpenFeature open source project
//
// Copyright (c) 2025 the Swift OpenFeature project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

import Hummingbird
import OpenFeature

struct EvaluationContextMiddleware: MiddlewareProtocol {
func handle(
_ request: Request,
context: APIRequestContext,
next: (Request, APIRequestContext) async throws -> Response
) async throws -> Response {
var evaluationContext = OpenFeatureEvaluationContext.current ?? OpenFeatureEvaluationContext()
evaluationContext.targetingKey = context.identity?.id
return try await OpenFeatureEvaluationContext.$current.withValue(evaluationContext) {
try await next(request, context)
}
}
}
59 changes: 59 additions & 0 deletions Examples/server/Sources/API/FeedController.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift OpenFeature open source project
//
// Copyright (c) 2025 the Swift OpenFeature project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

import Hummingbird
import OpenFeature

struct FeedController {
private let featureFlags: OpenFeatureClient

init() {
featureFlags = OpenFeatureSystem.client()
}

var routes: RouteCollection<APIRequestContext> {
let routes = RouteCollection(context: APIRequestContext.self)
routes.get(use: list)
return routes
}

private func list(request: Request, context: APIRequestContext) async throws -> Feed {
let useNewFeedAlgorithm = await featureFlags.value(for: "experimental-feed-algorithm", defaultingTo: false)

if useNewFeedAlgorithm {
// the new algorithm is faster but unfortunately still contains some bugs
if UInt.random(in: 0..<100) == 42 {
throw HTTPError(.internalServerError)
}
try await Task.sleep(for: .seconds(1))
return Feed.stub
} else {
try await Task.sleep(for: .seconds(2))
return Feed.stub
}
}
}

struct Feed: ResponseCodable {
let posts: [Post]

struct Post: ResponseCodable {
let id: String
}

static let stub = Feed(posts: [
Post(id: "1"),
Post(id: "2"),
Post(id: "3"),
])
}
24 changes: 24 additions & 0 deletions Examples/server/Sources/API/RequestContext.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift OpenFeature open source project
//
// Copyright (c) 2025 the Swift OpenFeature project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

import Hummingbird
import HummingbirdAuth

public struct APIRequestContext: AuthRequestContext, Sendable {
public var coreContext: CoreRequestContextStorage
public var identity: User?

public init(source: ApplicationRequestContextSource) {
coreContext = .init(source: source)
}
}
16 changes: 16 additions & 0 deletions Examples/server/Sources/API/User.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift OpenFeature open source project
//
// Copyright (c) 2025 the Swift OpenFeature project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

public struct User: Identifiable, Sendable {
public let id: String
}
Loading