git.iko.soy/rowtype-yoga/purescript-yoga-om
on main help sign in
Initial commit
def0575sigma-andex
.github/workflows/ci.ymlnew file+37
@@ -0,0 +1,37 @@
1+name: CI
2+
3+on:
4+ push:
5+ branches: [main]
6+ pull_request:
7+ branches: [main]
8+
9+jobs:
10+ build:
11+ runs-on: ubuntu-latest
12+ steps:
13+ - uses: actions/checkout@v2
14+
15+ - uses: purescript-contrib/setup-purescript@main
16+
17+ - uses: actions/setup-node@v2
18+ with:
19+ node-version: "14"
20+
21+ - name: Cache PureScript dependencies
22+ uses: actions/cache@v2
23+ # This cache uses the .dhall files to know when it should reinstall
24+ # and rebuild packages. It caches both the installed packages from
25+ # the `.spago` directory and compilation artifacts from the `output`
26+ # directory. When restored the compiler will rebuild any files that
27+ # have changed. If you do not want to cache compiled output, remove
28+ # the `output` path.
29+ with:
30+ key: ${{ runner.os }}-spago-${{ hashFiles('**/*.dhall') }}
31+ path: |
32+ .spago
33+ output
34+
35+ - run: spago build
36+
37+ - run: spago -x test.dhall test
.gitignorenew file+10
@@ -0,0 +1,10 @@
1+/bower_components/
2+/node_modules/
3+/.pulp-cache/
4+/output/
5+/generated-docs/
6+/.psc-package/
7+/.psc*
8+/.purs*
9+/.psa*
10+/.spago
LICENSEnew file+16
@@ -0,0 +1,16 @@
1+MIT No Attribution
2+
3+Copyright 2021 Mark Eibes, Jan Schulte
4+
5+Permission is hereby granted, free of charge, to any person obtaining a copy of this
6+software and associated documentation files (the "Software"), to deal in the Software
7+without restriction, including without limitation the rights to use, copy, modify,
8+merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
9+permit persons to whom the Software is furnished to do so.
10+
11+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
12+INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
13+PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
14+HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
15+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
16+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
README.mdnew file+1
@@ -0,0 +1 @@
1+# purescript-yoga-appm
package.jsonnew file+20
@@ -0,0 +1,20 @@
1+{
2+ "name": "purescript-yoga-appm",
3+ "version": "1.0.0",
4+ "description": "",
5+ "main": "index.js",
6+ "scripts": {
7+ "test": "echo \"Error: no test specified\" && exit 1"
8+ },
9+ "repository": {
10+ "type": "git",
11+ "url": "git+https://github.com/rowtype-yoga/purescript-yoga-appm.git"
12+ },
13+ "keywords": [],
14+ "author": "",
15+ "license": "ISC",
16+ "bugs": {
17+ "url": "https://github.com/rowtype-yoga/purescript-yoga-appm/issues"
18+ },
19+ "homepage": "https://github.com/rowtype-yoga/purescript-yoga-appm#readme"
20+}
packages.dhallnew file+6
@@ -0,0 +1,6 @@
1+
2+let upstream =
3+ https://github.com/purescript/package-sets/releases/download/psc-0.15.2-20220531/packages.dhall
4+ sha256:278d3608439187e51136251ebf12fabda62d41ceb4bec9769312a08b56f853e3
5+
6+in upstream
spago.dhallnew file+20
@@ -0,0 +1,20 @@
1+{ name = "yoga-appm"
2+, dependencies =
3+ [ "aff"
4+ , "checked-exceptions"
5+ , "console"
6+ , "effect"
7+ , "either"
8+ , "exceptions"
9+ , "maybe"
10+ , "newtype"
11+ , "prelude"
12+ , "record"
13+ , "transformers"
14+ , "typelevel-prelude"
15+ , "unsafe-coerce"
16+ , "variant"
17+ ]
18+, packages = ./packages.dhall
19+, sources = [ "src/**/*.purs", "test/**/*.purs" ]
20+}
src/Yoga/AppM/AppM.pursnew file+262
@@ -0,0 +1,262 @@
1+module Yoga.App.AppM where
2+
3+import Prelude
4+
5+import Control.Monad.Error.Class (class MonadError, class MonadThrow, throwError, try)
6+import Control.Monad.Except (runExceptT)
7+import Control.Monad.Except.Checked (ExceptV, safe)
8+import Control.Monad.Except.Checked as ExceptV
9+import Control.Monad.Except.Trans (lift)
10+import Control.Monad.Reader (class MonadAsk, class MonadReader, ReaderT(..), ask, withReaderT)
11+import Control.Monad.Reader.Trans (runReaderT)
12+import Data.Either (Either(..), either)
13+import Data.Maybe (Maybe, maybe)
14+import Data.Newtype (class Newtype, unwrap)
15+import Data.Symbol (class IsSymbol)
16+import Data.Variant (class VariantMatchCases, Variant, case_, inj, onMatch)
17+import Effect (Effect)
18+import Effect.Aff (Aff, runAff_)
19+import Effect.Aff.Class (class MonadAff, liftAff)
20+import Effect.Class (class MonadEffect)
21+import Effect.Class.Console as Console
22+import Effect.Exception (Error)
23+import Prim.Row (class Nub, class Union)
24+import Prim.Row as R
25+import Prim.RowList (class RowToList)
26+import Prim.RowList as RL
27+import Record (disjointUnion, get)
28+import Type.Prelude (Proxy(..))
29+import Unsafe.Coerce (unsafeCoerce)
30+
31+-- | A generic type for handling computations
32+-- | It combines asynchronous side-effecting computations with `Aff`,
33+-- | "dependency injection" via `ReaderT` which tracks a context, and
34+-- | checked exceptions and early return with `ExceptV`.
35+newtype AppM ctx err a = AppM (ReaderT ctx (ExceptV err Aff) a)
36+
37+throw
38+ ∷ ∀ m r1 r2 a
39+ . VariantInjTagged r1 r2
40+ => MonadThrow (Variant r2) m
41+ => Record r1
42+ -> m a
43+throw = throwError <<< injTagged
44+
45+-- | Turns a `Nothing` into an exception which reduces nesting and encourages
46+-- | tracking what actually went wrong
47+-- | e.g.
48+-- | ```purescript
49+-- | do
50+-- | let maybeCached = Object.lookup key cache :: Maybe a
51+-- | cached :: a <- maybeCached # AppM.note (unexpectedCacheMiss key)
52+-- | ```
53+note ∷ ∀ err m a. MonadThrow err m => err -> Maybe a -> m a
54+note err = maybe (throwError $ err) pure
55+
56+noteM ∷ ∀ err m a. MonadThrow err m => m err -> Maybe a -> m a
57+noteM err = maybe (throwError =<< err) pure
58+
59+-- | Turns a `Left a` into an exception which reduces nesting
60+throwLeftAs
61+ ∷ ∀ err m left right
62+ . MonadThrow err m
63+ => (left -> err)
64+ -> Either left right
65+ -> m right
66+throwLeftAs toError = either (throwError <<< toError) pure
67+
68+-- | Turn any `Aff` into an `AppM`.
69+-- | Because `Aff` can throw exceptions of type `Error`, a possible such
70+-- | exception is captured in the `exception` branch of the error variant
71+fromAff ∷ ∀ ctx a err. Aff a -> AppM ctx (exception ∷ Error | err) a
72+fromAff e =
73+ AppM (lift (lift (try e)))
74+ >>= either (throw <<< { exception: _ }) pure
75+
76+handleErrors
77+ ∷ ∀ ctx err1 err2 a
78+ . (Variant err1 -> AppM ctx err2 a)
79+ -> AppM ctx err1 a
80+ -> AppM ctx err2 a
81+handleErrors handle (AppM appM) = do
82+ ctx <- ask
83+ result <- runExceptT (runReaderT appM ctx) # liftAff
84+ case result of
85+ Left err -> handle err
86+ Right r -> pure r
87+
88+handleError
89+ ∷ ∀ rl a ctx handlers excHandled excIn excOut
90+ . RowToList handlers rl
91+ => VariantMatchCases rl excHandled (AppM ctx excOut a)
92+ => Union excHandled excOut excIn
93+ => Record handlers
94+ -> AppM ctx excIn a
95+ -> AppM ctx excOut a
96+handleError cases (AppM appM) = do
97+ ask
98+ >>= (liftAff <$> unwrap <<< runReaderT appM)
99+ >>= either (onMatch cases throwError) pure
100+
101+toExceptV
102+ ∷ ∀ ctx err1 err2 a
103+ . AppM ctx err1 a
104+ -> AppM ctx err2 (ExceptV err1 Aff a)
105+toExceptV (AppM appM) = do
106+ ctx <- ask
107+ pure $ runReaderT appM ctx
108+
109+fromExceptV ∷ ∀ ctx err a. ExceptV err Aff a -> AppM ctx err a
110+fromExceptV = AppM <<< lift
111+
112+-- | When you have a function that can throw a subset of the errors of the ones
113+-- | you could throw in a do block, `expand` allows to make them compatible
114+expand
115+ ∷ ∀ ctx lt more gt
116+ . R.Union lt more gt
117+ => AppM ctx lt
118+ ~> AppM ctx gt
119+expand = unsafeCoerce
120+
121+expandCtx
122+ :: forall lt more gt err
123+ . Union lt more gt
124+ => AppM { | lt } err ~>
125+ AppM { | gt } err
126+expandCtx = unsafeCoerce
127+
128+launchAppM_ ∷ ∀ ctx. ctx -> AppM ctx () Unit -> Effect Unit
129+launchAppM_ ctx appM =
130+ runAff_
131+ (either logUnhandledError pure)
132+ (runAppM_ ctx appM)
133+
134+logUnhandledError ∷ Error -> Effect Unit
135+logUnhandledError (err ∷ Error) =
136+ Console.error
137+ $ "Unhandled Error!\n"
138+ <> show err
139+
140+launchAppM
141+ ∷ ∀ ctx r rl err_ err
142+ . RowToList r rl
143+ => VariantMatchCases rl err_ (Aff Unit)
144+ => Union err_ () err
145+ => ctx
146+ -> Record r
147+ -> AppM ctx err Unit
148+ -> Effect Unit
149+launchAppM ctx errorHandlers (AppM app) =
150+ runAff_ (either logUnhandledError pure) do
151+ ExceptV.handleErrors errorHandlers $ runReaderT app ctx
152+
153+-- | Invoke this function in the end after handling all possible errors, e.g.
154+-- | ```purescript
155+-- | runAppM_ { token: "123" } someApp
156+-- | ```
157+runAppM_ ∷ ∀ ctx a. ctx -> AppM ctx () a -> Aff a
158+runAppM_ ctx (AppM app) = do
159+ safe $ runReaderT app ctx
160+
161+-- | Removes the ReaderT part and makes the errors explicit
162+-- | ```purescript
163+-- | runReader { token: "123" } someApp
164+-- | ```
165+runReader ∷ ∀ ctx err a. ctx -> AppM ctx err a -> Aff (Either (Variant err) a)
166+runReader ctx appM =
167+ runAppM_ ctx
168+ $ handleErrors (pure <<< Left)
169+ $ (Right <$> appM)
170+
171+-- | Useful when constructing a *parameterless* callback of type `Aff` or
172+-- | `Effect` within the context of an AppM computation
173+-- | ```purescript
174+-- | recordAndUploadDOM ∷ Aff _ <- do
175+-- | AppM.unliftAff $ serialiseDOM >>= uploadDOMSnapshot
176+-- | ```
177+unliftAff
178+ ∷ ∀ ctx err otherErrors a
179+ . AppM ctx err a
180+ -> AppM ctx otherErrors (Aff (Either (Variant err) a))
181+unliftAff appM = do
182+ ctx <- ask
183+ pure $ runReader ctx appM
184+
185+-- | Useful when constructing a callback of type `Aff` or `Effect` within the
186+-- | context of an AppM computation
187+-- | ```purescript
188+-- | uploadDOMDOM ∷ (SerialisedDOM -> Aff _) <- do
189+-- | AppM.unliftAffFn $ uploadDOMSnapshot
190+-- | ```
191+unliftAffFn
192+ ∷ ∀ ctx arg otherErrors a
193+ . (arg -> AppM ctx () a)
194+ -> AppM ctx otherErrors (arg -> Aff a)
195+unliftAffFn appM = do
196+ ctx <- ask
197+ pure \arg -> do
198+ runReader ctx (appM arg) <#> either case_ identity
199+
200+-- | Invoke this function in the end and handle all possible errors, e.g.
201+-- | ```purescript
202+-- | runAppM { token: "123" } { someError: \e -> handleTheError e } someApp
203+-- | ```
204+runAppM
205+ ∷ ∀ ctx r rl err_ err a
206+ . RowToList r rl
207+ => VariantMatchCases rl err_ (Aff a)
208+ => Union err_ () err
209+ => ctx
210+ -> Record r
211+ -> AppM ctx err a
212+ -> Aff a
213+runAppM ctx errorHandlers (AppM app) = do
214+ ExceptV.handleErrors errorHandlers $ runReaderT app ctx
215+
216+-- | Manually add some fields to the context of a computation.
217+-- | This function is useful when already inside a `do` block of an `AppM`
218+-- | and you want to call a function that requires some `ctx` that you obtained
219+-- | within this `do` block. For example:
220+-- | ```purescript
221+-- | do
222+-- | moreCtx <- getSomeData :: AppM {} _
223+-- | getMoreData :: AppM { moreCtx :: _ } _ # widenCtx { moreCtx }
224+-- | ```
225+widenCtx
226+ ∷ ∀ err additionalCtx ctx widerCtx
227+ . Union additionalCtx ctx widerCtx
228+ => Nub widerCtx widerCtx
229+ => { | additionalCtx }
230+ -> AppM { | widerCtx } err
231+ ~> AppM { | ctx } err
232+widenCtx toUnion (AppM (appM)) =
233+ AppM
234+ (withReaderT (disjointUnion toUnion) appM)
235+
236+-- Instances for the AppM type
237+derive instance Newtype (AppM ctx err a) _
238+derive newtype instance Functor (AppM ctx err)
239+derive newtype instance Applicative (AppM ctx err)
240+derive newtype instance Apply (AppM ctx err)
241+derive newtype instance Bind (AppM ctx err)
242+derive newtype instance Monad (AppM ctx err)
243+derive newtype instance MonadEffect (AppM ctx err)
244+derive newtype instance MonadAff (AppM ctx err)
245+derive newtype instance MonadAsk ctx (AppM ctx err)
246+derive newtype instance MonadReader ctx (AppM ctx err)
247+derive newtype instance MonadThrow (Variant err) (AppM ctx err)
248+derive newtype instance MonadError (Variant err) (AppM ctx err)
249+
250+-- liftAff = fromAff
251+-- | Helper class to make throwing errors easier
252+class VariantInjTagged a b | a -> b where
253+ injTagged ∷ Record a -> Variant b
254+
255+instance variantInjTagged ∷
256+ ( RowToList r1 (RL.Cons sym a RL.Nil)
257+ , R.Cons sym a () r1
258+ , R.Cons sym a rx r2
259+ , IsSymbol sym
260+ ) =>
261+ VariantInjTagged r1 r2 where
262+ injTagged = inj (Proxy ∷ Proxy sym) <<< get (Proxy ∷ Proxy sym)
test.dhallnew file+12
@@ -0,0 +1,12 @@
1+let conf = ./spago.dhall
2+
3+in conf
4+ // { sources = conf.sources # [ "test/**/*.purs" ]
5+ , dependencies =
6+ conf.dependencies
7+ # [ "aff"
8+ , "effect"
9+ , "spec"
10+ , "spec-discovery"
11+ ]
12+ }
test/Main.pursnew file+11
@@ -0,0 +1,11 @@
1+module Test.Main where
2+
3+import Prelude
4+
5+import Effect (Effect)
6+import Effect.Class.Console (log)
7+
8+main :: Effect Unit
9+main = do
10+ log "🍝"
11+ log "You should add some tests."