Skip to content

Commit

Permalink
HBResponder conforming to Sendable (#252)
Browse files Browse the repository at this point in the history
* Sendable HBResponder

* Comments

* Use macOS 13 runner

* Re-enable test coverage
  • Loading branch information
adam-fowler authored Oct 29, 2023
1 parent f0f5862 commit e5729d5
Show file tree
Hide file tree
Showing 13 changed files with 148 additions and 89 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ on:

jobs:
macOS:
runs-on: macOS-latest
runs-on: macOS-13
steps:
- name: Checkout
uses: actions/checkout@v3
Expand Down
2 changes: 1 addition & 1 deletion Sources/Hummingbird/ApplicationBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ public final class HBApplicationBuilder<RequestContext: HBRequestContext> {

/// Construct the RequestResponder from the middleware group and router
func constructResponder() -> any HBResponder<RequestContext> {
return self.router.buildRouter()
return self.router.buildResponder()
}

public func addChannelHandler(_ handler: @autoclosure @escaping @Sendable () -> any RemovableChannelHandler) {
Expand Down
2 changes: 1 addition & 1 deletion Sources/Hummingbird/Middleware/CORSMiddleware.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import NIOCore
/// "access-control-allow-origin" header
public struct HBCORSMiddleware<Context: HBRequestContext>: HBMiddleware {
/// Defines what origins are allowed
public enum AllowOrigin {
public enum AllowOrigin: Sendable {
case none
case all
case originBased
Expand Down
2 changes: 1 addition & 1 deletion Sources/Hummingbird/Middleware/Middleware.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ import NIOCore
/// }
/// }
/// ```
public protocol HBMiddleware<Context> {
public protocol HBMiddleware<Context>: Sendable {
associatedtype Context: HBRequestContext
func apply(to request: HBRequest, context: Context, next: any HBResponder<Context>) -> EventLoopFuture<HBResponse>
}
Expand Down
4 changes: 2 additions & 2 deletions Sources/Hummingbird/Router/EndpointResponder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import NIOCore
import NIOHTTP1

/// Stores endpoint responders for each HTTP method
final class HBEndpointResponders<Context: HBRequestContext> {
struct HBEndpointResponders<Context: HBRequestContext>: Sendable {
init(path: String) {
self.path = path
self.methods = [:]
Expand All @@ -26,7 +26,7 @@ final class HBEndpointResponders<Context: HBRequestContext> {
return self.methods[method.rawValue]
}

func addResponder(for method: HTTPMethod, responder: any HBResponder<Context>) {
mutating func addResponder(for method: HTTPMethod, responder: any HBResponder<Context>) {
guard self.methods[method.rawValue] == nil else {
preconditionFailure("\(method.rawValue) already has a handler")
}
Expand Down
24 changes: 10 additions & 14 deletions Sources/Hummingbird/Router/RouterBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,13 @@ import NIOHTTP1
/// `head`, `post` and `patch`. The route handler closures all return objects conforming to
/// `HBResponseGenerator`. This allows us to support routes which return a multitude of types eg
/// ```
/// app.router.get("string") { _ -> String in
/// router.get("string") { _ -> String in
/// return "string"
/// }
/// app.router.post("status") { _ -> HTTPResponseStatus in
/// router.post("status") { _ -> HTTPResponseStatus in
/// return .ok
/// }
/// app.router.data("data") { request -> ByteBuffer in
/// router.data("data") { request -> ByteBuffer in
/// return context.allocator.buffer(string: "buffer")
/// }
/// ```
Expand All @@ -39,18 +39,18 @@ import NIOHTTP1
/// The default `Router` setup in `HBApplication` is the `TrieRouter` . This uses a
/// trie to partition all the routes for faster access. It also supports wildcards and parameter extraction
/// ```
/// app.router.get("user/*", use: anyUser)
/// app.router.get("user/:id", use: userWithId)
/// router.get("user/*", use: anyUser)
/// router.get("user/:id", use: userWithId)
/// ```
/// Both of these match routes which start with "/user" and the next path segment being anything.
/// The second version extracts the path segment out and adds it to `HBRequest.parameters` with the
/// key "id".
public final class HBRouterBuilder<Context: HBRequestContext>: HBRouterMethods {
var trie: RouterPathTrie<HBEndpointResponders<Context>>
var trie: RouterPathTrieBuilder<HBEndpointResponders<Context>>
public let middlewares: HBMiddlewareGroup<Context>

public init(context: Context.Type) {
self.trie = RouterPathTrie()
public init(context: Context.Type = HBBasicRequestContext.self) {
self.trie = RouterPathTrieBuilder()
self.middlewares = .init()
}

Expand All @@ -67,13 +67,9 @@ public final class HBRouterBuilder<Context: HBRequestContext>: HBRouterMethods {
}
}

func endpoint(_ path: String) -> HBEndpointResponders<Context>? {
self.trie.getValueAndParameters(path)?.value
}

/// build router
public func buildRouter() -> any HBResponder<Context> {
HBRouter(context: Context.self, trie: self.trie, notFoundResponder: self.middlewares.constructResponder(finalResponder: NotFoundResponder<Context>()))
public func buildResponder() -> some HBResponder<Context> {
HBRouter(context: Context.self, trie: self.trie.build(), notFoundResponder: self.middlewares.constructResponder(finalResponder: NotFoundResponder<Context>()))
}

/// Add path for closure returning type conforming to ResponseFutureEncodable
Expand Down
6 changes: 3 additions & 3 deletions Sources/Hummingbird/Router/RouterMethods.swift
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ extension HBRouterMethods {
use closure: @escaping (HBRequest, Context) throws -> Output
) -> HBCallbackResponder<Context> {
// generate response from request. Moved repeated code into internal function
func _respond(request: HBRequest, context: Context) throws -> HBResponse {
@Sendable func _respond(request: HBRequest, context: Context) throws -> HBResponse {
return try closure(request, context).response(from: request, context: context)
}

Expand Down Expand Up @@ -216,7 +216,7 @@ extension HBRouterMethods {
use closure: @escaping (HBRequest, Context) -> EventLoopFuture<Output>
) -> HBCallbackResponder<Context> {
// generate response from request. Moved repeated code into internal function
func _respond(request: HBRequest, context: Context) -> EventLoopFuture<HBResponse> {
@Sendable func _respond(request: HBRequest, context: Context) -> EventLoopFuture<HBResponse> {
let responseFuture = closure(request, context).flatMapThrowing { try $0.response(from: request, context: context) }
return responseFuture.hop(to: context.eventLoop)
}
Expand All @@ -227,14 +227,14 @@ extension HBRouterMethods {
}
} else {
return HBCallbackResponder { request, context in
var request = request
if case .byteBuffer = request.body {
return _respond(request: request, context: context)
} else {
return request.body.consumeBody(
maxSize: context.applicationContext.configuration.maxUploadSize,
on: context.eventLoop
).flatMap { buffer in
var request = request
request.body = .byteBuffer(buffer)
return _respond(request: request, context: context)
}
Expand Down
91 changes: 71 additions & 20 deletions Sources/Hummingbird/Router/TrieRouter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,19 @@
//
//===----------------------------------------------------------------------===//

/// URI Path Trie
struct RouterPathTrie<Value> {
/// URI Path Trie Builder
struct RouterPathTrieBuilder<Value: Sendable> {
var root: Node

init() {
self.root = Node(key: .null, output: nil)
}

/// Add Entry to Trie
/// - Parameters:
/// - entry: Path for entry
/// - value: Value to add to this path if one does not exist already
/// - onAdd: How to edit the value at this path
func addEntry(_ entry: RouterPath, value: @autoclosure () -> Value, onAdd: (Node) -> Void = { _ in }) {
var node = self.root
for key in entry {
Expand All @@ -33,6 +38,61 @@ struct RouterPathTrie<Value> {
}
}

func build() -> RouterPathTrie<Value> {
.init(root: self.root.build())
}

/// Trie Node. Each node represents one component of a URI path
final class Node {
let key: RouterPath.Element
var children: [Node]
var value: Value?

init(key: RouterPath.Element, output: Value?) {
self.key = key
self.value = output
self.children = []
}

func addChild(key: RouterPath.Element, output: Value?) -> Node {
if let child = getChild(key) {
return child
}
let node = Node(key: key, output: output)
self.children.append(node)
return node
}

func getChild(_ key: RouterPath.Element) -> Node? {
return self.children.first { $0.key == key }
}

func getChild(_ key: Substring) -> Node? {
if let child = self.children.first(where: { $0.key == key }) {
return child
}
return self.children.first { $0.key ~= key }
}

func build() -> RouterPathTrie<Value>.Node {
return .init(key: self.key, value: self.value, children: self.children.map { $0.build() })
}
}
}

/// Trie used by HBRouter responder
struct RouterPathTrie<Value: Sendable>: Sendable {
let root: Node

/// Initialise RouterPathTrie
/// - Parameter root: Root node of trie
init(root: Node) {
self.root = root
}

/// Get value from trie and any parameters from capture nodes
/// - Parameter path: Path to process
/// - Returns: value and parameters
func getValueAndParameters(_ path: String) -> (value: Value, parameters: HBParameters?)? {
let pathComponents = path.split(separator: "/", omittingEmptySubsequences: true)
var parameters: HBParameters?
Expand Down Expand Up @@ -63,25 +123,16 @@ struct RouterPathTrie<Value> {
return nil
}

/// Trie Node. Each node represents one component of a URI path
final class Node {
/// Internally used Node to describe static trie
struct Node: Sendable {
let key: RouterPath.Element
var children: [Node]
var value: Value?
let children: [Node]
let value: Value?

init(key: RouterPath.Element, output: Value?) {
init(key: RouterPath.Element, value: Value?, children: [Node]) {
self.key = key
self.value = output
self.children = []
}

func addChild(key: RouterPath.Element, output: Value?) -> Node {
if let child = getChild(key) {
return child
}
let node = Node(key: key, output: output)
self.children.append(node)
return node
self.value = value
self.children = children
}

func getChild(_ key: RouterPath.Element) -> Node? {
Expand All @@ -98,7 +149,7 @@ struct RouterPathTrie<Value> {
}

extension Optional where Wrapped == HBParameters {
mutating func set(_ s: Substring, value: Substring) {
fileprivate mutating func set(_ s: Substring, value: Substring) {
switch self {
case .some(var parameters):
parameters.set(s, value: value)
Expand All @@ -108,7 +159,7 @@ extension Optional where Wrapped == HBParameters {
}
}

mutating func setCatchAll(_ value: Substring) {
fileprivate mutating func setCatchAll(_ value: Substring) {
switch self {
case .some(var parameters):
parameters.setCatchAll(value)
Expand Down
6 changes: 3 additions & 3 deletions Sources/Hummingbird/Server/Responder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,17 @@ import NIOCore
/// Protocol for object that produces a response given a request
///
/// This is the core protocol for Hummingbird. It defines an object that can respond to a request.
public protocol HBResponder<Context> {
public protocol HBResponder<Context>: Sendable {
associatedtype Context: HBRequestContext
/// Return EventLoopFuture that will be fulfilled with response to the request supplied
func respond(to request: HBRequest, context: Context) -> EventLoopFuture<HBResponse>
}

/// Responder that calls supplied closure
public struct HBCallbackResponder<Context: HBRequestContext>: HBResponder {
let callback: (HBRequest, Context) -> EventLoopFuture<HBResponse>
let callback: @Sendable (HBRequest, Context) -> EventLoopFuture<HBResponse>

public init(callback: @escaping (HBRequest, Context) -> EventLoopFuture<HBResponse>) {
public init(callback: @escaping @Sendable (HBRequest, Context) -> EventLoopFuture<HBResponse>) {
self.callback = callback
}

Expand Down
6 changes: 3 additions & 3 deletions Sources/HummingbirdFoundation/Files/CacheControl.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@
import Hummingbird

/// Associates cache control values with filename
public struct HBCacheControl {
public enum Value: CustomStringConvertible {
public struct HBCacheControl: Sendable {
public enum Value: CustomStringConvertible, Sendable {
case noStore
case noCache
case `private`
Expand Down Expand Up @@ -62,7 +62,7 @@ public struct HBCacheControl {
.joined(separator: ", ")
}

private struct Entry {
private struct Entry: Sendable {
let mediaType: HBMediaType
let cacheControl: [Value]
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/HummingbirdFoundation/Files/FileIO.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import NIOCore
import NIOPosix

/// Manages File reading and writing.
public struct HBFileIO {
public struct HBFileIO: Sendable {
let fileIO: NonBlockingFileIO
let chunkSize: Int

Expand Down
2 changes: 1 addition & 1 deletion Sources/HummingbirdXCT/HBXCTRouter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ struct HBXCTRouter<RequestContext: HBTestRouterContextProtocol>: HBXCTApplicatio
encoder: builder.encoder,
decoder: builder.decoder
)
self.responder = builder.router.buildRouter()
self.responder = builder.router.buildResponder()
}

func shutdown() async throws {
Expand Down
Loading

0 comments on commit e5729d5

Please sign in to comment.