elm-matrix-sdk-alpha/src/Internal/Api/Chain.elm

248 lines
7.5 KiB
Elm
Raw Normal View History

2023-03-10 20:34:25 +00:00
module Internal.Api.Chain exposing (..)
2023-03-10 20:34:25 +00:00
{-| This module aims to simplify chaining several API tasks together.
Chaining tasks together is usually done through the `Task` submodule of `elm/core`,
but this isn't always sufficient for getting complex chained tasks.
For example, suppose you need to run 3 consecutive tasks that each need an access
token, and only the 1st and the 3rd require another token. You will need to pass
on all necessary information, and preferably in a way that the compiler can
assure that the information is present when it arrives there. Using the `Task`
submodule, this can lead to indentation hell.
This module aims to allow for simple task chaining without adding too much complexity
if you wish to pass on values.
2023-03-14 14:51:40 +00:00
The model is like a snake: _____
/ o \
/-|------------ | ------- | ------------- | -------- | |\/\/
< | accessToken | baseUrl | transactionId | API call | |------< Final API call
\-|------------ | ------- | ------------- | -------- | |/\/\
\-----/
(You're not allowed to judge my ASCII art skills unless you submit a PR with a
2023-03-10 20:34:25 +00:00
superior ASCII snake model.)
Every task will add another value to an extensible record, which can be used
2023-03-14 14:50:23 +00:00
by later tasks in the chain. Additionally, every subtask can leave a `VaultUpdate`
type as a message to the Vault to update certain information.
2023-03-10 20:34:25 +00:00
-}
import Http
2023-03-14 21:31:55 +00:00
import Internal.Api.Helpers as Helpers
import Internal.Tools.Context as Context exposing (Context)
import Internal.Tools.Exceptions as X
2023-03-10 20:34:25 +00:00
import Task exposing (Task)
type alias TaskChain err u a b =
Context a -> Task (FailedChainPiece err u) (TaskChainPiece u a b)
type alias IdemChain err u a =
TaskChain err u a a
2023-03-10 20:34:25 +00:00
type alias CompleteChain u =
TaskChain () u {} {}
type alias TaskChainPiece u a b =
{ contextChange : Context a -> Context b
, messages : List u
}
type alias FailedChainPiece err u =
{ error : err, messages : List u }
2023-03-10 20:34:25 +00:00
2023-03-10 20:34:25 +00:00
{-| Chain two tasks together. The second task will only run if the first one succeeds.
-}
andThen : TaskChain err u b c -> TaskChain err u a b -> TaskChain err u a c
2023-03-10 20:34:25 +00:00
andThen f2 f1 =
\context ->
2023-03-10 20:34:25 +00:00
f1 context
|> Task.andThen
(\old ->
context
|> old.contextChange
|> f2
|> Task.map
(\new ->
{ contextChange = old.contextChange >> new.contextChange
, messages = List.append old.messages new.messages
}
)
|> Task.mapError
(\{ error, messages } ->
{ error = error, messages = List.append old.messages messages }
)
)
{-| Same as `andThen`, but the results are placed at the front of the list, rather than at the end.
-}
andBeforeThat : TaskChain err u b c -> TaskChain err u a b -> TaskChain err u a c
andBeforeThat f2 f1 =
\context ->
f1 context
|> Task.andThen
(\old ->
context
|> old.contextChange
|> f2
|> Task.map
(\new ->
{ contextChange = old.contextChange >> new.contextChange
, messages = List.append new.messages old.messages
}
)
|> Task.mapError
(\{ error, messages } ->
{ error = error, messages = List.append messages old.messages }
)
)
{-| When an error has occurred, "fix" it with the following function.
-}
catchWith : (err -> TaskChainPiece u a b) -> TaskChain err u a b -> TaskChain err u a b
catchWith onErr f =
onError (\e -> succeed <| onErr e) f
{-| Create a task chain that always fails.
-}
fail : err -> TaskChain err u a b
fail e _ =
Task.fail { error = e, messages = [] }
{-| Optionally run a task that may provide additional information.
2023-03-10 20:34:25 +00:00
2023-03-12 13:53:56 +00:00
If the provided chain fails, it will be ignored. This way, the chain can be tasked
without needlessly breaking the whole chain if anything breaks in here.
2023-03-10 20:34:25 +00:00
2023-03-12 13:53:56 +00:00
You cannot use this function to execute a task chain that adds or removes context.
2023-03-10 20:34:25 +00:00
-}
maybe : IdemChain err u a -> IdemChain err u a
2023-03-10 20:34:25 +00:00
maybe f =
{ contextChange = identity
, messages = []
}
|> succeed
|> always
|> onError
|> (|>) f
{-| Map a value to a different one.
-}
map : (u1 -> u2) -> TaskChain err u1 a b -> TaskChain err u2 a b
map m f =
\context ->
f context
|> Task.map
(\{ contextChange, messages } ->
{ contextChange = contextChange, messages = List.map m messages }
)
|> Task.mapError
(\{ error, messages } ->
{ error = error, messages = List.map m messages }
)
2023-03-10 20:34:25 +00:00
{-| If the TaskChain errfails, run this task otherwise.
2023-03-10 20:34:25 +00:00
-}
otherwise : TaskChain err u a b -> TaskChain e u a b -> TaskChain err u a b
2023-03-10 20:34:25 +00:00
otherwise f2 f1 context =
Task.onError (always <| f2 context) (f1 context)
{-| If all else fails, you can also just add the failing part to the succeeding part.
-}
otherwiseFail : IdemChain err u a -> IdemChain err (Result err u) a
otherwiseFail =
map Ok
>> catchWith
(\err ->
{ contextChange = identity
, messages = [ Err err ]
}
)
{-| If an error is raised, deal with it accordingly.
-}
onError : (err -> TaskChain err2 u a b) -> TaskChain err u a b -> TaskChain err2 u a b
onError onErr f =
\context ->
f context
|> Task.onError
(\{ error, messages } ->
succeed { contextChange = identity, messages = messages }
|> andThen (onErr error)
|> (|>) context
)
{-| Create a task chain that always succeeds.
-}
succeed : { contextChange : Context a -> Context b, messages : List u } -> TaskChain err u a b
succeed d _ =
Task.succeed d
2023-03-10 20:34:25 +00:00
{-| Once all the pieces of the chain have been assembled, you can turn it into a task.
The compiler will fail if the chain is missing a vital piece of information.
2023-03-10 20:34:25 +00:00
-}
toTask : TaskChain err u {} b -> Task (FailedChainPiece err u) (List u)
2023-03-10 20:34:25 +00:00
toTask f1 =
Context.init
|> f1
|> Task.map .messages
2023-03-10 20:34:25 +00:00
{-| If the TaskChain errfails, this function will get it to retry.
2023-03-10 20:34:25 +00:00
When set to 1 or lower, the task will only try once.
2023-03-10 20:34:25 +00:00
-}
tryNTimes : Int -> TaskChain X.Error u a b -> TaskChain X.Error u a b
tryNTimes n f =
if n <= 0 then
f
else
onError
(\e ->
case e of
X.InternetException (Http.BadUrl _) ->
fail e
X.InternetException _ ->
tryNTimes (n - 1) f
X.SDKException (X.ServerReturnsBadJSON _) ->
tryNTimes (n - 1) f
X.SDKException _ ->
fail e
X.ServerException _ ->
fail e
X.ContextFailed _ ->
fail e
X.UnsupportedSpecVersion ->
fail e
)
f