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

130 lines
4.0 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.
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
by later tasks in the chain. Additionally, every subtask can leave a `CredUpdate`
type as a message to the Credentials to update certain information.
2023-03-10 20:34:25 +00:00
-}
import Internal.Api.Context as Context exposing (Context)
import Internal.Tools.Exceptions as X
2023-03-10 20:34:25 +00:00
import Task exposing (Task)
2023-03-10 20:34:25 +00:00
type alias TaskChain u a b =
Context a -> Task X.Error (TaskChainPiece u a b)
type alias IdemChain u a =
TaskChain u a a
2023-03-10 20:34:25 +00:00
type TaskChainPiece u a b
= TaskChainPiece
{ contextChange : Context a -> Context b
, messages : List u
}
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 u b c -> TaskChain u a b -> TaskChain u a c
andThen f2 f1 =
\context ->
2023-03-10 20:34:25 +00:00
f1 context
|> Task.andThen
(\(TaskChainPiece old) ->
context
|> old.contextChange
|> f2
|> Task.map
(\(TaskChainPiece new) ->
TaskChainPiece
{ contextChange = old.contextChange >> new.contextChange
, messages = List.append old.messages new.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
-}
2023-03-12 13:53:56 +00:00
maybe : IdemChain u a -> IdemChain u a
2023-03-10 20:34:25 +00:00
maybe f =
{ contextChange = identity
, messages = []
}
|> TaskChainPiece
|> Task.succeed
|> always
|> Task.onError
|> (>>) f
2023-03-10 20:34:25 +00:00
{-| If the TaskChain fails, run this task otherwise.
-}
otherwise : TaskChain u a b -> TaskChain u a b -> TaskChain u a b
otherwise f2 f1 context =
Task.onError (always <| f2 context) (f1 context)
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 u {} b -> Task X.Error (List u)
toTask f1 =
Context.init
|> f1
|> Task.map
(\(TaskChainPiece data) ->
data.messages
)
2023-03-10 20:34:25 +00:00
{-| If the TaskChain fails, this function will get it to retry.
When set to 1 or lower, the task will only try once.
2023-03-10 20:34:25 +00:00
-}
tryNTimes : Int -> TaskChain u a b -> TaskChain u a b
tryNTimes n f context =
if n <= 1 then
f context
2023-03-10 20:34:25 +00:00
else
(\_ -> tryNTimes (n - 1) f context)
|> Task.onError
|> (|>) (f context)