Last active
November 23, 2023 09:58
-
-
Save Obbut/5c7de5d7b289c3014de3d61179bddb8c to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// MARK: Usage example | |
let applicationMiddleware = buildMiddleware { | |
CatMiddleware() | |
DogMiddleware() | |
} | |
let routeSpecificMiddleware = applicationMiddleware.chain { | |
TokenMiddleware() // Removing this will cause compiler errors, because UserMiddleware depends on TokenMiddleware.Context | |
UserMiddleware() | |
// TODO: it's not yet possible to chain another middleware with InputContext == TokenMiddleware.Context | |
} | |
// MARK: - Middleware definitions | |
struct TokenMiddleware: Middleware { | |
struct Context { | |
var token: String | |
} | |
func tranform(context: EmptyContext) -> Context { | |
Context(token: "abc") | |
} | |
} | |
struct UserMiddleware: Middleware { | |
struct Context { | |
var userId: String | |
} | |
func tranform(context: TokenMiddleware.Context) -> Context { | |
Context(userId: "\(context.token)userid") | |
} | |
} | |
struct CatMiddleware: Middleware { | |
struct Context { | |
var catName: String | |
} | |
func tranform(context: EmptyContext) -> Context { | |
Context(catName: "Cat") | |
} | |
} | |
struct DogMiddleware: Middleware { | |
struct Context { | |
var dogName: String | |
} | |
func tranform(context: EmptyContext) -> Context { | |
Context(dogName: "Doggo") | |
} | |
} | |
// MARK: Framework | |
struct ChainedMiddleware<M1, M2>: Middleware | |
where M1: Middleware, M2: Middleware, M1.OutputContext == M2.InputContext { | |
var m1: M1 | |
var m2: M2 | |
func tranform(context: M1.InputContext) -> M2.OutputContext { | |
m2.tranform(context: m1.tranform(context: context)) | |
} | |
} | |
struct ContextEraser<InputContext, M>: Middleware | |
where M: Middleware, M.InputContext == EmptyContext { | |
var m: M | |
func tranform(context: InputContext) -> M.OutputContext { | |
m.tranform(context: EmptyContext()) | |
} | |
} | |
@resultBuilder | |
struct MiddlewareBuilder<InputContext> { | |
static func buildPartialBlock<M>(first: M) -> M | |
where M: Middleware, M.InputContext == InputContext { | |
first | |
} | |
@_disfavoredOverload | |
static func buildPartialBlock<M>(first: M) -> ContextEraser<InputContext, M> | |
where M: Middleware, M.InputContext == EmptyContext { | |
ContextEraser(m: first) | |
} | |
static func buildPartialBlock<Accumulated, Next>( | |
accumulated: Accumulated, | |
next: Next | |
) -> ChainedMiddleware<Accumulated, Next> | |
where Accumulated: Middleware, Next: Middleware, Next.InputContext == Accumulated.OutputContext | |
{ | |
ChainedMiddleware(m1: accumulated, m2: next) | |
} | |
static func buildPartialBlock<Accumulated, Next>( | |
accumulated: Accumulated, | |
next: Next | |
) -> ChainedMiddleware<Accumulated, ContextEraser<Accumulated.OutputContext, Next>> | |
where Accumulated: Middleware, Next: Middleware, Next.InputContext == EmptyContext { | |
ChainedMiddleware(m1: accumulated, m2: ContextEraser(m: next)) | |
} | |
// build where Next.InputContext == EmptyContext | |
} | |
func buildMiddleware<M>(@MiddlewareBuilder<EmptyContext> _ build: () -> M) -> M | |
where M: Middleware { | |
build() | |
} | |
extension Middleware { | |
func chain<M>(@MiddlewareBuilder<OutputContext> _ build: () -> M) -> M where M: Middleware { | |
build() | |
} | |
} | |
protocol Middleware<InputContext, OutputContext> { | |
// note: for ergonomics, there should be a way to write middleware without worrying about InputContext / comboning contexts | |
associatedtype InputContext = EmptyContext | |
associatedtype OutputContext // rename to Context | |
// obviously this would need something like a request parameter | |
func tranform(context: InputContext) -> OutputContext | |
} | |
@dynamicMemberLookup | |
struct CombinedContext<A, B> { | |
var a: A | |
var b: B | |
subscript<T>(dynamicMember keyPath: WritableKeyPath<A, T>) -> T { | |
get { a[keyPath: keyPath] } | |
set { a[keyPath: keyPath] = newValue } | |
} | |
subscript<T>(dynamicMember keyPath: WritableKeyPath<B, T>) -> T { | |
get { b[keyPath: keyPath] } | |
set { b[keyPath: keyPath] = newValue } | |
} | |
} | |
struct EmptyContext {} | |
// MARK: - Router | |
// TODO: still figuring out how to make all of this work without having to deal with CombinedContext | |
struct Request {} | |
struct Router<Context> { | |
init() { | |
} | |
mutating func get<each RouteContext, Output>( | |
path: String, | |
handler: (Request, repeat each RouteContext) async throws -> Output | |
) { | |
} | |
} | |
func myRouteHandler( | |
request: Request, | |
userContext: UserMiddleware.Context, | |
catContext: CatMiddleware.Context | |
) async throws -> String { | |
return "\(userContext.userId) \(catContext.catName)" | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment