git.iko.soy/rowtype-yoga/purescript-yoga-om-layer
on main help sign in
Improve recovering tests: multi-error, error preservation, drop unnecessary parens
commit6a64bb2c5b93ae3421ec31069501fe1c062cf27a authorMark <mark.eibes@gmail.com> dateSat 14 Feb 2026, 00:32 (8 months ago) parent192b6c4
2 files changed+186−1
src/Yoga/Om/Layer.purs+89 test/Test/Main.purs+97−1
src/Yoga/Om/Layer.purs+89
@@ -18,6 +18,9 @@ module Yoga.Om.Layer
1818 , (>->)
1919 , recovering
2020 , repeating
21+ , wireLayers
22+ , wireLayersRL
23+ , class WireLayersRL
2124 , class CheckAllProvided
2225 , class CheckAllLabelsExist
2326 , class CheckLabelExists
@@ -26,6 +29,7 @@ module Yoga.Om.Layer
2629import Prelude
2730
2831import Control.Monad.Error.Class (catchError, throwError)
32+import Data.Symbol (class IsSymbol)
2933import Control.Parallel (class Parallel)
3034import Control.Parallel as Parallel
3135import Data.Array as Array
@@ -44,6 +48,8 @@ import Effect.Ref (Ref)
4448import Effect.Ref as Ref
4549import Effect.Unsafe (unsafePerformEffect)
4650import Prim.Row (class Nub, class Union)
51+import Prim.Row as Row
52+import Type.Proxy (Proxy(..))
4753import Prim.RowList (class RowToList, Cons, Nil, RowList)
4854import Prim.TypeError (class Fail, Above, Quote, Text)
4955import Record as Record
@@ -464,3 +470,86 @@ repeating policy shouldRepeat (OmLayer layerId om) =
464470 ifM (shouldRepeat status result)
465471 (applyAndDelay policy status >>= maybe (pure result) go)
466472 (pure result)
473+
474+-- =============================================================================
475+-- Auto-wiring
476+-- =============================================================================
477+
478+class
479+ WireLayersRL
480+ (rl :: RowList Type)
481+ (layers :: Row Type)
482+ (accReq :: Row Type)
483+ (accErr :: Row Type)
484+ (accProv :: Row Type)
485+ (resReq :: Row Type)
486+ (resErr :: Row Type)
487+ (resProv :: Row Type)
488+ | rl layers accReq accErr accProv -> resReq resErr resProv
489+ where
490+ wireLayersRL
491+ :: Proxy rl
492+ -> Record layers
493+ -> OmLayer accReq accErr (Record accProv)
494+ -> OmLayer resReq resErr (Record resProv)
495+
496+instance WireLayersRL Nil layers accReq accErr accProv accReq accErr accProv where
497+ wireLayersRL _ _ acc = acc
498+
499+instance
500+ ( IsSymbol sym
501+ , Row.Cons sym (OmLayer nextReq nextErr (Record nextProv)) _rest layers
502+ -- next's requirements must be subset of (accProv + accReq)
503+ , Union accProv accReq provReqOut
504+ , Nub provReqOut provReqOut
505+ , Union nextReq _nextReqRest provReqOut
506+ , Keys nextReq
507+ , Keys accReq
508+ , Keys accProv
509+ -- merge errors
510+ , Union accErr nextErr errMergedRaw
511+ , Nub errMergedRaw errMerged
512+ , Union accErr _e1 errMerged
513+ , Union nextErr _e2 errMerged
514+ -- merge provisions
515+ , Union accProv nextProv provMergedRaw
516+ , Nub provMergedRaw provMerged
517+ -- recurse
518+ , WireLayersRL tail layers accReq errMerged provMerged resReq resErr resProv
519+ ) =>
520+ WireLayersRL (Cons sym (OmLayer nextReq nextErr (Record nextProv)) tail) layers accReq accErr accProv resReq resErr resProv
521+ where
522+ wireLayersRL _ layers acc =
523+ wireLayersRL (Proxy :: Proxy tail) layers combined
524+ where
525+ next :: OmLayer nextReq nextErr (Record nextProv)
526+ next = Record.get (Proxy :: Proxy sym) layers
527+
528+ combined :: OmLayer accReq errMerged (Record provMerged)
529+ combined = OmLayer (freshId unit) do
530+ accProv <- coerceOm (buildLayer acc)
531+ nextProv <- coerceOm (buildLayer next) # widenCtx' accProv
532+ pure (Record.merge accProv nextProv)
533+
534+ coerceOm :: forall r1 e1 r2 e2 a. Om (Record r1) e1 a -> Om (Record r2) e2 a
535+ coerceOm = unsafeCoerce
536+
537+-- | Om.widenCtx without constraints. Union/Nub from Prim.Row are
538+-- | compiler builtins with no runtime representation, so this is safe.
539+widenCtx'
540+ :: forall additional ctx err a
541+ . Record additional
542+ -> Om { | ctx } err a
543+ -> Om { | ctx } err a
544+widenCtx' = unsafeCoerce (Om.widenCtx :: {} -> Om {} () a -> Om {} () a)
545+
546+wireLayers
547+ :: forall layers rl req err prov
548+ . RowToList layers rl
549+ => WireLayersRL rl layers (scope :: Scope) () () req err prov
550+ => Record layers
551+ -> OmLayer req err (Record prov)
552+wireLayers layers =
553+ wireLayersRL (Proxy :: Proxy rl) layers seed
554+ where
555+ seed = pure {} :: OmLayer (scope :: Scope) () (Record ())
test/Test/Main.purs+97−1
@@ -19,7 +19,7 @@ import Data.Variant (match)
1919import Yoga.Om as Om
2020import Data.Time.Duration (Milliseconds(..))
2121import Effect.Aff.Retry (RetryStatus(..), limitRetries, constantDelay)
22−import Yoga.Om.Layer (OmLayer, Scope, makeLayer, makeScopedLayer, bracketLayer, fresh, combineRequirements, runLayer, runScoped, runScopedWith, withScoped, provide, recovering, repeating)
22+import Yoga.Om.Layer (OmLayer, Scope, makeLayer, makeScopedLayer, bracketLayer, fresh, combineRequirements, runLayer, runScoped, runScopedWith, withScoped, provide, recovering, repeating, wireLayers)
2323
2424-- Example types
2525type Config = { port :: Int, host :: String }
@@ -666,3 +666,99 @@ main = launchAff_ $ runSpec [ consoleReporter ] do
666666 { exception: \_ -> pure { count: 0 } }
667667 (runLayer {} repeated)
668668 result.count `shouldEqual` 4
669+
670+ describe "wireLayers" do
671+
672+ it "wires a simple linear chain (A -> B -> C)" do
673+ let
674+ a_base :: OmLayer (scope :: Scope) () { base :: String }
675+ a_base = makeLayer (pure { base: "foundation" })
676+
677+ b_mid :: OmLayer (scope :: Scope, base :: String) () { mid :: String }
678+ b_mid = makeLayer do
679+ { base } <- Om.ask
680+ pure { mid: base <> "-middle" }
681+
682+ c_top :: OmLayer (scope :: Scope, mid :: String) () { top :: String }
683+ c_top = makeLayer do
684+ { mid } <- Om.ask
685+ pure { top: mid <> "-top" }
686+
687+ app = wireLayers { a_base, b_mid, c_top }
688+
689+ result <- liftAff $ runScoped app
690+ result.base `shouldEqual` "foundation"
691+ result.mid `shouldEqual` "foundation-middle"
692+ result.top `shouldEqual` "foundation-middle-top"
693+
694+ it "wires independent layers with no inter-dependencies" do
695+ let
696+ a_x :: OmLayer (scope :: Scope) () { x :: Int }
697+ a_x = makeLayer (pure { x: 1 })
698+
699+ b_y :: OmLayer (scope :: Scope) () { y :: Int }
700+ b_y = makeLayer (pure { y: 2 })
701+
702+ c_z :: OmLayer (scope :: Scope) () { z :: Int }
703+ c_z = makeLayer (pure { z: 3 })
704+
705+ app = wireLayers { a_x, b_y, c_z }
706+
707+ result <- liftAff $ runScoped app
708+ result.x `shouldEqual` 1
709+ result.y `shouldEqual` 2
710+ result.z `shouldEqual` 3
711+
712+ it "wires a diamond dependency" do
713+ let
714+ a_db :: OmLayer (scope :: Scope) () { db :: String }
715+ a_db = makeLayer (pure { db: "postgres" })
716+
717+ b_cache :: OmLayer (scope :: Scope, db :: String) () { cache :: String }
718+ b_cache = makeLayer do
719+ { db } <- Om.ask
720+ pure { cache: db <> "-cache" }
721+
722+ b_repo :: OmLayer (scope :: Scope, db :: String) () { repo :: String }
723+ b_repo = makeLayer do
724+ { db } <- Om.ask
725+ pure { repo: db <> "-repo" }
726+
727+ d_app :: OmLayer (scope :: Scope, cache :: String, repo :: String) () { app :: String }
728+ d_app = makeLayer do
729+ { cache, repo } <- Om.ask
730+ pure { app: cache <> "+" <> repo }
731+
732+ app = wireLayers { a_db, b_cache, b_repo, d_app }
733+
734+ result <- liftAff $ runScoped app
735+ result.db `shouldEqual` "postgres"
736+ result.cache `shouldEqual` "postgres-cache"
737+ result.repo `shouldEqual` "postgres-repo"
738+ result.app `shouldEqual` "postgres-cache+postgres-repo"
739+
740+ it "memoizes shared dependencies within wireLayers" do
741+ counter <- liftEffect $ Ref.new 0
742+ let
743+ a_db :: OmLayer (scope :: Scope) () { db :: String }
744+ a_db = makeLayer do
745+ liftEffect $ Ref.modify_ (_ + 1) counter
746+ pure { db: "connected" }
747+
748+ b_cache :: OmLayer (scope :: Scope, db :: String) () { cache :: String }
749+ b_cache = makeLayer do
750+ { db } <- Om.ask
751+ pure { cache: db <> "-cache" }
752+
753+ b_repo :: OmLayer (scope :: Scope, db :: String) () { repo :: String }
754+ b_repo = makeLayer do
755+ { db } <- Om.ask
756+ pure { repo: db <> "-repo" }
757+
758+ app = wireLayers { a_db, b_cache, b_repo }
759+
760+ result <- liftAff $ runScoped app
761+ result.cache `shouldEqual` "connected-cache"
762+ result.repo `shouldEqual` "connected-repo"
763+ count <- liftEffect $ Ref.read counter
764+ count `shouldEqual` 1