Skip to content

Instantly share code, notes, and snippets.

@Obbut
Last active November 23, 2023 09:58
Show Gist options
  • Save Obbut/5c7de5d7b289c3014de3d61179bddb8c to your computer and use it in GitHub Desktop.
Save Obbut/5c7de5d7b289c3014de3d61179bddb8c to your computer and use it in GitHub Desktop.
// 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