|
| 1 | +// A dependency-injection container — the explicit, MoonBit-idiomatic equivalent |
| 2 | +// of FastAPI's `Depends`. FastAPI reads a dependency's callable off the handler |
| 3 | +// signature and resolves it per request, caching the result and running any |
| 4 | +// `yield` teardown afterwards. MoonBit has no runtime reflection and no `Any`, |
| 5 | +// so the container is a first-class value keyed by name, and its dependency |
| 6 | +// value type `V` is an explicit parameter: for a single dependency type `V` is |
| 7 | +// that type; for several, `V` is a user-defined sum type wrapping them — the |
| 8 | +// exhaustive, type-safe stand-in for Python's dynamic `Any` (cf. axum's typemap |
| 9 | +// + downcast, Go's `interface{}` + type assertion). Everything else — the |
| 10 | +// registry, request-scoped one-shot resolution, `yield`-style teardown, and |
| 11 | +// `dependency_overrides` — is modelled faithfully. |
| 12 | + |
| 13 | +///| |
| 14 | +/// A provider: a keyed factory that builds a request-scoped dependency value, |
| 15 | +/// with an optional teardown run after the handler (FastAPI's `yield` |
| 16 | +/// dependencies, whose post-`yield` body is cleanup). The `factory` runs at most |
| 17 | +/// once per request scope; the `teardown` receives the produced value. |
| 18 | +pub(all) struct Provider[V] { |
| 19 | + factory : () -> V |
| 20 | + teardown : (V) -> Unit |
| 21 | +} |
| 22 | + |
| 23 | +///| |
| 24 | +/// Build a provider. `teardown` defaults to a no-op — the common "plain value, |
| 25 | +/// nothing to release" case. |
| 26 | +pub fn[V] Provider::new( |
| 27 | + factory : () -> V, |
| 28 | + teardown? : (V) -> Unit = _v => (), |
| 29 | +) -> Provider[V] { |
| 30 | + { factory, teardown } |
| 31 | +} |
| 32 | + |
| 33 | +///| |
| 34 | +/// The provider registry: `key -> Provider`, plus a separate `overrides` map |
| 35 | +/// that shadows it. Overrides are FastAPI's `app.dependency_overrides` — a test |
| 36 | +/// swaps a real dependency (a live DB session) for a fake without touching the |
| 37 | +/// routes. A registered override always wins over the base provider. |
| 38 | +pub(all) struct Container[V] { |
| 39 | + providers : Map[String, Provider[V]] |
| 40 | + overrides : Map[String, Provider[V]] |
| 41 | +} |
| 42 | + |
| 43 | +///| |
| 44 | +/// An empty container. |
| 45 | +pub fn[V] Container::new() -> Container[V] { |
| 46 | + { providers: Map([]), overrides: Map([]) } |
| 47 | +} |
| 48 | + |
| 49 | +///| |
| 50 | +/// Register a base provider under `key` (last registration wins), returning the |
| 51 | +/// container so registrations can chain. |
| 52 | +pub fn[V] Container::provide( |
| 53 | + self : Container[V], |
| 54 | + key : String, |
| 55 | + factory : () -> V, |
| 56 | + teardown? : (V) -> Unit = _v => (), |
| 57 | +) -> Container[V] { |
| 58 | + self.providers[key] = Provider::new(factory, teardown~) |
| 59 | + self |
| 60 | +} |
| 61 | + |
| 62 | +///| |
| 63 | +/// Register a dependency override for `key` — FastAPI's |
| 64 | +/// `app.dependency_overrides[dep] = fake`. Takes precedence over the base |
| 65 | +/// provider until cleared. |
| 66 | +pub fn[V] Container::override_( |
| 67 | + self : Container[V], |
| 68 | + key : String, |
| 69 | + factory : () -> V, |
| 70 | + teardown? : (V) -> Unit = _v => (), |
| 71 | +) -> Container[V] { |
| 72 | + self.overrides[key] = Provider::new(factory, teardown~) |
| 73 | + self |
| 74 | +} |
| 75 | + |
| 76 | +///| |
| 77 | +/// Drop the override for `key` (no-op if none), restoring the base provider. |
| 78 | +pub fn[V] Container::clear_override(self : Container[V], key : String) -> Unit { |
| 79 | + self.overrides.remove(key) |
| 80 | +} |
| 81 | + |
| 82 | +///| |
| 83 | +/// Drop every override — the usual test teardown that returns the container to |
| 84 | +/// its production wiring. |
| 85 | +pub fn[V] Container::clear_overrides(self : Container[V]) -> Unit { |
| 86 | + self.overrides.clear() |
| 87 | +} |
| 88 | + |
| 89 | +///| |
| 90 | +/// The effective provider for `key`: an override if one is registered, else the |
| 91 | +/// base provider, else `None`. |
| 92 | +fn[V] Container::resolve(self : Container[V], key : String) -> Provider[V]? { |
| 93 | + match self.overrides.get(key) { |
| 94 | + Some(p) => Some(p) |
| 95 | + None => self.providers.get(key) |
| 96 | + } |
| 97 | +} |
| 98 | + |
| 99 | +///| |
| 100 | +/// A request-scoped resolution scope. Each dependency is built at most once and |
| 101 | +/// its value cached for the life of the scope (FastAPI's per-request dependency |
| 102 | +/// cache), and each built value's teardown is recorded to run — in reverse |
| 103 | +/// registration order (LIFO) — when the scope closes. Open one per request, |
| 104 | +/// resolve dependencies through it, then `close` it (or use `Container::run`). |
| 105 | +pub struct Scope[V] { |
| 106 | + container : Container[V] |
| 107 | + cache : Map[String, V] |
| 108 | + teardowns : Array[() -> Unit] |
| 109 | +} |
| 110 | + |
| 111 | +///| |
| 112 | +/// Open a fresh request scope over this container. |
| 113 | +pub fn[V] Container::open_scope(self : Container[V]) -> Scope[V] { |
| 114 | + { container: self, cache: Map([]), teardowns: [] } |
| 115 | +} |
| 116 | + |
| 117 | +///| |
| 118 | +/// Resolve `key` within this scope: return the already-built instance if the |
| 119 | +/// dependency was resolved earlier in the same request; otherwise run its |
| 120 | +/// factory once, cache the value, register its teardown, and return it. `None` |
| 121 | +/// when no provider (or override) is registered for `key`. |
| 122 | +pub fn[V] Scope::get(self : Scope[V], key : String) -> V? { |
| 123 | + match self.cache.get(key) { |
| 124 | + Some(v) => Some(v) |
| 125 | + None => |
| 126 | + match self.container.resolve(key) { |
| 127 | + None => None |
| 128 | + Some(prov) => { |
| 129 | + let v = (prov.factory)() |
| 130 | + self.cache[key] = v |
| 131 | + let td = prov.teardown |
| 132 | + self.teardowns.push(() => td(v)) |
| 133 | + Some(v) |
| 134 | + } |
| 135 | + } |
| 136 | + } |
| 137 | +} |
| 138 | + |
| 139 | +///| |
| 140 | +/// Run every recorded teardown in LIFO order and clear them, so a closed scope |
| 141 | +/// is inert. Mirrors FastAPI unwinding `yield` dependencies in reverse — the |
| 142 | +/// last opened is torn down first. |
| 143 | +pub fn[V] Scope::close(self : Scope[V]) -> Unit { |
| 144 | + for i = self.teardowns.length() - 1; i >= 0; i = i - 1 { |
| 145 | + self.teardowns[i]() |
| 146 | + } |
| 147 | + self.teardowns.clear() |
| 148 | +} |
| 149 | + |
| 150 | +///| |
| 151 | +/// Run `handler` inside a fresh request scope, then tear the scope down — the |
| 152 | +/// setup/teardown pair wrapped around a handler, exactly as a FastAPI `yield` |
| 153 | +/// dependency brackets the request. The handler resolves whatever it needs |
| 154 | +/// through the scope; every dependency built during the call is released |
| 155 | +/// (LIFO) once it returns, then the response is handed back. |
| 156 | +pub fn[V] Container::run( |
| 157 | + self : Container[V], |
| 158 | + handler : (Scope[V]) -> @moonasgi.Response, |
| 159 | +) -> @moonasgi.Response { |
| 160 | + let scope = self.open_scope() |
| 161 | + let resp = handler(scope) |
| 162 | + scope.close() |
| 163 | + resp |
| 164 | +} |
0 commit comments