@@ -0,0 +1,206 @@
1+-- @inline export makeLayer arity=1
2+-- @inline export runLayer arity=2
10+ , class CheckAllProvided
11+ , class CheckAllLabelsExist
12+ , class CheckLabelExists
17+import Data.Newtype (class Newtype)
18+import Effect.Aff.Class (liftAff)
19+import Prim.Row (class Nub, class Union)
20+import Prim.RowList (class RowToList, Cons, Nil, RowList)
21+import Prim.TypeError (class Fail, Above, Quote, Text)
22+import Record as Record
23+import Record.Studio (class Keys)
24+import Type.Equality (class TypeEquals)
25+import Unsafe.Coerce (unsafeCoerce)
29+-- | A layer that requires dependencies (req) and provides services (prov)
30+newtype OmLayer req prov err = OmLayer (Om (Record req) err (Record prov))
32+derive instance Newtype (OmLayer req prov err) _
34+-- | Helper to create a layer from an Om computation
35+makeLayer :: forall req prov err. Om (Record req) err (Record prov) -> OmLayer req prov err
38+-- =============================================================================
39+-- Custom Type Errors for Missing Dependencies
40+-- =============================================================================
42+-- | Check if all required dependencies are provided
43+-- If not, show a helpful diff of required vs available
44+class CheckAllProvided (required :: Row Type) (available :: Row Type)
46+instance checkAllProvidedImpl ::
47+ ( RowToList required reqList
48+ , RowToList available availList
49+ , CheckAllLabelsExist reqList availList required available
51+ CheckAllProvided required available
53+-- | Check that all labels in required RowList exist in available RowList
54+-- Pass along the original rows for better error messages
55+class CheckAllLabelsExist (required :: RowList Type) (available :: RowList Type) (requiredRow :: Row Type) (availableRow :: Row Type)
57+-- Base case: no more requirements to check
58+instance checkAllLabelsNil :: CheckAllLabelsExist Nil available requiredRow availableRow
60+-- Recursive case: check if this label exists, then check the rest
61+instance checkAllLabelsCons ::
62+ ( CheckLabelExists label ty available requiredRow availableRow
63+ , CheckAllLabelsExist tail available requiredRow availableRow
65+ CheckAllLabelsExist (Cons label ty tail) available requiredRow availableRow
67+-- | Check if a single label exists in the available RowList
68+-- This is where the magic happens - we dispatch on RowList structure in the instance head!
69+class CheckLabelExists (label :: Symbol) (ty :: Type) (available :: RowList Type) (requiredRow :: Row Type) (availableRow :: Row Type)
71+-- Found it! The label matches in the instance head
72+-- TypeEquals ensures the types at this label actually match!
73+instance checkLabelExistsFound ::
75+ CheckLabelExists label ty (Cons label ty' tail) requiredRow availableRow
77+-- Keep looking: different label, recurse on tail
78+else instance checkLabelExistsKeepLooking ::
79+ CheckLabelExists label ty tail requiredRow availableRow =>
80+ CheckLabelExists label ty (Cons otherLabel otherTy tail) requiredRow availableRow
82+-- Not found: reached end of list, emit custom error WITH FULL CONTEXT!
83+else instance checkLabelExistsNotFound ::
86+ (Text "Missing required dependency!")
90+ (Text "The first missing field is: ")
96+ (Text "The layer requires: ")
102+ (Text "But you provided: ")
103+ (Quote availableRow)
113+ CheckLabelExists label ty Nil requiredRow availableRow
115+-- | Run a layer with a given context, with custom error if requirements aren't met
117+ :: forall req prov err available
118+ . CheckAllProvided req available -- โ Custom error if dependencies missing!
120+ -> OmLayer req prov err
121+ -> Om (Record available) err (Record prov)
122+runLayer _ctx layer = unsafeCastLayer layer
124+ -- After CheckAllProvided succeeds, we know req โ available at compile time
125+ -- So this cast is safe - we're just telling the type system what it already proved
126+ unsafeCastLayer :: OmLayer req prov err -> Om (Record available) err (Record prov)
127+ unsafeCastLayer = unsafeCoerce
129+-- | The key function: combine two layers with automatic deduplication
130+-- Given two layers with potentially overlapping requirements,
131+-- produce a layer with deduplicated requirements
133+-- Uses expand to properly widen contexts and errors, then runs both layers!
135+ :: forall req1 req2 prov1 prov2 err1 err2 provMerged provMergedRaw reqMerged reqDeduped _req1 _req2 _err1 _err2
136+ . Union req1 req2 reqMerged
137+ => Nub reqMerged reqDeduped -- โ Automatic deduplication of requirements!
138+ => Union req1 _req1 reqDeduped -- req1 is subset of reqDeduped
139+ => Union req2 _req2 reqDeduped -- req2 is subset of reqDeduped
140+ => Union err1 _err1 () -- Widen errors to empty
141+ => Union err2 _err2 ()
142+ => Union prov1 prov2 provMergedRaw -- Merge provisions
143+ => Nub provMergedRaw provMerged -- Deduplicate provisions too!
144+ => Keys req1 -- Needed for expand
145+ => Keys req2 -- Needed for expand
146+ => OmLayer req1 prov1 err1
147+ -> OmLayer req2 prov2 err2
148+ -> OmLayer reqDeduped provMerged ()
149+combineRequirements (OmLayer build1) (OmLayer build2) = makeLayer do
150+ -- Widen both contexts and errors to allow them to run in the deduplicated context
151+ -- expand allows an Om computation to run in a larger context with larger error row
152+ rec1 <- Om.expand build1 -- Om req1 err1 -> Om reqDeduped ()
153+ rec2 <- Om.expand build2 -- Om req2 err2 -> Om reqDeduped ()
154+ -- Merge the results (Record.merge handles the Nub)
155+ pure (Record.merge rec1 rec2)
157+-- Example proof that this works:
159+-- layer1 :: OmLayer (config :: Config) (logger :: Logger) ()
160+-- layer2 :: OmLayer (config :: Config) (database :: Database) ()
162+-- Then: combineRequirements layer1 layer2
163+-- Has type: OmLayer (config :: Config) prov3 err3
165+-- NOT: OmLayer (config :: Config, config :: Config) prov3 err3
167+-- The duplicated requirement (config, config) is automatically
168+-- deduplicated to just (config) by the Nub constraint!
170+-- =============================================================================
171+-- Vertical Composition (ZLayer-style provide)
172+-- =============================================================================
174+-- | Vertical composition: feed output of one layer into input of another
175+-- | This is the >>> operator from ZIO ZLayer
178+-- | postgresLive :: OmLayer (config :: Config) (postgres :: SQL) ()
179+-- | userRepoLive :: OmLayer (postgres :: SQL) (userRepo :: UserRepo) ()
181+-- | composed = userRepoLive `provide` postgresLive
182+-- | -- Result: OmLayer (config :: Config) (userRepo :: UserRepo) ()
184+ :: forall req prov1 prov2 err1 err2 _req _prov1 _err1 _err2
185+ . Union req _req req -- expand constraints
186+ => Union prov1 _prov1 prov1
187+ => Union err1 _err1 ()
188+ => Union err2 _err2 ()
191+ => OmLayer prov1 prov2 err2 -- layer that needs prov1, provides prov2
192+ -> OmLayer req prov1 err1 -- layer that needs req, provides prov1
193+ -> OmLayer req prov2 () -- composed: needs req, provides prov2
194+provide (OmLayer layer2) (OmLayer layer1) = makeLayer do
195+ -- Run first layer to get prov1
196+ prov1 <- Om.expand layer1
198+ -- Run second layer with prov1 as context
199+ liftAff $ Om.runOm prov1
200+ { exception: \_ -> pure (unsafeCoerce {}) }
203+infixl 9 provide as >->
205+-- TODO: More sophisticated composition operators
206+-- For now, use >>> for simple chains and combineRequirements for parallel deps