Add Task Chain + API setup
parent
2baf012345
commit
7935e112ed
2
elm.json
2
elm.json
|
@ -14,9 +14,11 @@
|
|||
"elm-version": "0.19.0 <= v < 0.20.0",
|
||||
"dependencies": {
|
||||
"elm/core": "1.0.0 <= v < 2.0.0",
|
||||
"elm/http": "2.0.0 <= v < 3.0.0",
|
||||
"elm/json": "1.0.0 <= v < 2.0.0",
|
||||
"elm/parser": "1.0.0 <= v < 2.0.0",
|
||||
"elm/time": "1.0.0 <= v < 2.0.0",
|
||||
"elm/url": "1.0.0 <= v < 2.0.0",
|
||||
"micahhahn/elm-safe-recursion": "2.0.0 <= v < 3.0.0",
|
||||
"miniBill/elm-fast-dict": "1.0.0 <= v < 2.0.0"
|
||||
},
|
||||
|
|
|
@ -0,0 +1,175 @@
|
|||
module Internal.Api.Chain exposing (TaskChain, IdemChain, CompleteChain)
|
||||
|
||||
{-|
|
||||
|
||||
|
||||
# Task chains
|
||||
|
||||
Elm uses a `Task` type to avoid issues that JavaScript deals with, yet the same
|
||||
**callback hell** issue might appear that JavaScript developers often deal with.
|
||||
For this reason, this module helps chain different `Task` types together such
|
||||
that all information is stored and values are dealt with appropriately.
|
||||
|
||||
Elm's type checking system helps making this system sufficiently rigorous to
|
||||
avoid leaking values passing through the API in unexpected ways.
|
||||
|
||||
@docs TaskChain, IdemChain, CompleteChain
|
||||
|
||||
-}
|
||||
|
||||
import Internal.Config.Log exposing (Log)
|
||||
import Internal.Values.Context as Context exposing (APIContext)
|
||||
import Task
|
||||
|
||||
|
||||
type alias Backpacked u a =
|
||||
{ a | messages : List u, logs : List Log }
|
||||
|
||||
|
||||
{-| The TaskChain is a piece in the long chain of tasks that need to be completed.
|
||||
The type defines four variables:
|
||||
|
||||
- `err` value that may arise on an error
|
||||
- `u` the update msg that should be returned
|
||||
- `a` phantom type before executing the chain's context
|
||||
- `b` phantom type after executing the chain's context
|
||||
|
||||
-}
|
||||
type alias TaskChain err u a b =
|
||||
APIContext a -> Task.Task (FailedChainPiece err u) (TaskChainPiece u a b)
|
||||
|
||||
|
||||
{-| An IdemChain is a TaskChain that does not influence the chain's context
|
||||
|
||||
- `err` value that may arise on an error
|
||||
- `u` the update msg that should be executed
|
||||
- `a` phantom type before, during and after the chain's context
|
||||
|
||||
-}
|
||||
type alias IdemChain err u a =
|
||||
TaskChain err u a a
|
||||
|
||||
|
||||
{-| A CompleteChain is a complete task chain where all necessary information
|
||||
has been defined. In simple terms, whenever a Matrix API call is made, all
|
||||
necessary information for that endpoint:
|
||||
|
||||
1. Was previously known and has been inserted, or
|
||||
2. Was acquired before actually making the API call.
|
||||
|
||||
-}
|
||||
type alias CompleteChain u =
|
||||
TaskChain Never u {} {}
|
||||
|
||||
|
||||
{-| A TaskChainPiece is a piece that updates the chain's context.
|
||||
|
||||
Once a chain is executed, the process will add the `messages` value to its list
|
||||
of updates, and it will update its context according to the `contextChange`
|
||||
function.
|
||||
|
||||
-}
|
||||
type alias TaskChainPiece u a b =
|
||||
Backpacked u { contextChange : APIContext a -> APIContext b }
|
||||
|
||||
|
||||
{-| A FailedChainPiece initiates an early breakdown of a chain. Unless caught,
|
||||
this halts execution of the chain. The process will add the `messages` value to
|
||||
its list of updates, and it will return the given `err` value for a direct
|
||||
explanation of what went wrong.
|
||||
-}
|
||||
type alias FailedChainPiece err u =
|
||||
Backpacked u { error : err }
|
||||
|
||||
|
||||
{-| 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
|
||||
andThen f2 f1 =
|
||||
\context ->
|
||||
f1 context
|
||||
|> Task.andThen
|
||||
(\old ->
|
||||
context
|
||||
|> old.contextChange
|
||||
|> f2
|
||||
|> Task.map
|
||||
(\new ->
|
||||
{ contextChange = old.contextChange >> new.contextChange
|
||||
, logs = List.append old.logs new.logs
|
||||
, messages = List.append old.messages new.messages
|
||||
}
|
||||
)
|
||||
|> Task.mapError
|
||||
(\new ->
|
||||
{ error = new.error
|
||||
, logs = List.append old.logs new.logs
|
||||
, messages = List.append old.messages new.messages
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
{-| When an error has occurred, "fix" it with an artificial task chain result.
|
||||
-}
|
||||
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
|
||||
|
||||
|
||||
{-| Creates a task that always fails.
|
||||
-}
|
||||
fail : err -> TaskChain err u a b
|
||||
fail e _ =
|
||||
Task.fail { error = e, logs = [], messages = [] }
|
||||
|
||||
|
||||
{-| Optionally run a task that doesn't need to succeed.
|
||||
|
||||
If the provided chain fails, it will be ignored. This way, the chain can be
|
||||
executed without breaking the whole chain if it fails. This can be useful for:
|
||||
|
||||
1. Sending information to the Matrix API and not caring if it actually arrives
|
||||
2. Gaining optional information that might be nice to know, but not necessary
|
||||
|
||||
Consequently, the optional chain cannot add any information that the rest of
|
||||
the chain relies on.
|
||||
|
||||
-}
|
||||
maybe : IdemChain err u a -> IdemChain err2 u a
|
||||
maybe f =
|
||||
{ contextChange = identity
|
||||
, logs = []
|
||||
, messages = []
|
||||
}
|
||||
|> succeed
|
||||
|> always
|
||||
|> onError
|
||||
|> (|>) f
|
||||
|
||||
|
||||
{-| When an error occurs, this function allows the task chain to go down a
|
||||
similar but different route.
|
||||
-}
|
||||
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
|
||||
(\old ->
|
||||
{ contextChange = identity
|
||||
, logs = old.logs
|
||||
, messages = old.messages
|
||||
}
|
||||
|> succeed
|
||||
|> andThen (onErr old.error)
|
||||
|> (|>) context
|
||||
)
|
||||
|
||||
|
||||
{-| Creates a task that always succeeds.
|
||||
-}
|
||||
succeed : TaskChainPiece u a b -> TaskChain err u a b
|
||||
succeed piece _ =
|
||||
Task.succeed piece
|
|
@ -0,0 +1,284 @@
|
|||
module Internal.Api.Request exposing
|
||||
( ApiCall, ApiPlan, callAPI, withAttributes
|
||||
, accessToken, withTransactionId
|
||||
, fullBody, bodyBool, bodyInt, bodyString, bodyValue, bodyOpBool, bodyOpInt, bodyOpString, bodyOpValue
|
||||
, queryBool, queryInt, queryString, queryOpBool, queryOpInt, queryOpString
|
||||
)
|
||||
|
||||
{-|
|
||||
|
||||
|
||||
# API module
|
||||
|
||||
This module helps describe API requests.
|
||||
|
||||
|
||||
## Plan
|
||||
|
||||
@docs ApiCall, ApiPlan, callAPI, withAttributes
|
||||
|
||||
|
||||
## API attributes
|
||||
|
||||
|
||||
### General attributes
|
||||
|
||||
@docs accessToken, withTransactionId
|
||||
|
||||
|
||||
### Body
|
||||
|
||||
@docs fullBody, bodyBool, bodyInt, bodyString, bodyValue, bodyOpBool, bodyOpInt, bodyOpString, bodyOpValue
|
||||
|
||||
|
||||
### Query parameters
|
||||
|
||||
@docs queryBool, queryInt, queryString, queryOpBool, queryOpInt, queryOpString
|
||||
|
||||
-}
|
||||
|
||||
import Http
|
||||
import Internal.Tools.Json as Json
|
||||
import Internal.Values.Context as Context exposing (APIContext)
|
||||
import Url
|
||||
import Url.Builder as UrlBuilder
|
||||
|
||||
|
||||
{-| The API call is a plan that describes how an interaction is planned with
|
||||
the Matrix API.
|
||||
-}
|
||||
type alias ApiCall ph =
|
||||
{ attributes : List ContextAttr
|
||||
, baseUrl : String
|
||||
, context : APIContext ph
|
||||
, method : String
|
||||
}
|
||||
|
||||
|
||||
{-| Shortcut definition to define a function that bases an APICall on a given
|
||||
APIContext.
|
||||
-}
|
||||
type alias ApiPlan a =
|
||||
APIContext a -> ApiCall a
|
||||
|
||||
|
||||
{-| An attribute maps a given context to an attribute for an API call.
|
||||
-}
|
||||
type alias Attribute a =
|
||||
APIContext a -> ContextAttr
|
||||
|
||||
|
||||
{-| A context attribute describes one aspect of the API call that is to be made.
|
||||
-}
|
||||
type ContextAttr
|
||||
= BodyParam String Json.Value
|
||||
| FullBody Json.Value
|
||||
| Header Http.Header
|
||||
| NoAttr
|
||||
| QueryParam UrlBuilder.QueryParameter
|
||||
| ReplaceInUrl String String
|
||||
| Timeout Float
|
||||
| UrlPath String
|
||||
|
||||
|
||||
{-| Attribute that requires an access token to be present
|
||||
-}
|
||||
accessToken : Attribute { a | accessToken : () }
|
||||
accessToken =
|
||||
Context.getAccessToken
|
||||
>> (++) "Bearer "
|
||||
>> Http.header "Authorization"
|
||||
>> Header
|
||||
|
||||
|
||||
{-| Attribute that adds a boolean value to the HTTP body.
|
||||
-}
|
||||
bodyBool : String -> Bool -> Attribute a
|
||||
bodyBool key value =
|
||||
bodyValue key <| Json.encode Json.bool value
|
||||
|
||||
|
||||
{-| Attribute that adds an integer value to the HTTP body.
|
||||
-}
|
||||
bodyInt : String -> Int -> Attribute a
|
||||
bodyInt key value =
|
||||
bodyValue key <| Json.encode Json.int value
|
||||
|
||||
|
||||
{-| Attribute that adds a boolean to the HTTP body if it is given.
|
||||
-}
|
||||
bodyOpBool : String -> Maybe Bool -> Attribute a
|
||||
bodyOpBool key value =
|
||||
case value of
|
||||
Just v ->
|
||||
bodyBool key v
|
||||
|
||||
Nothing ->
|
||||
empty
|
||||
|
||||
|
||||
{-| Attribute that adds an integer value to the HTTP body if it is given.
|
||||
-}
|
||||
bodyOpInt : String -> Maybe Int -> Attribute a
|
||||
bodyOpInt key value =
|
||||
case value of
|
||||
Just v ->
|
||||
bodyInt key v
|
||||
|
||||
Nothing ->
|
||||
empty
|
||||
|
||||
|
||||
{-| Attribute that adds a string value to the HTTP body if it is given.
|
||||
-}
|
||||
bodyOpString : String -> Maybe String -> Attribute a
|
||||
bodyOpString key value =
|
||||
case value of
|
||||
Just v ->
|
||||
bodyString key v
|
||||
|
||||
Nothing ->
|
||||
empty
|
||||
|
||||
|
||||
{-| Attribute that adds a JSON value to the HTTP body if it is given.
|
||||
-}
|
||||
bodyOpValue : String -> Maybe Json.Value -> Attribute a
|
||||
bodyOpValue key value =
|
||||
case value of
|
||||
Just v ->
|
||||
bodyValue key v
|
||||
|
||||
Nothing ->
|
||||
empty
|
||||
|
||||
|
||||
{-| Attribute that adds a string value to the HTTP body.
|
||||
-}
|
||||
bodyString : String -> String -> Attribute a
|
||||
bodyString key value =
|
||||
bodyValue key <| Json.encode Json.string value
|
||||
|
||||
|
||||
{-| Attribute that adds a JSON value to the HTTP body.
|
||||
-}
|
||||
bodyValue : String -> Json.Value -> Attribute a
|
||||
bodyValue key value _ =
|
||||
BodyParam key value
|
||||
|
||||
|
||||
{-| Create a plan to create an API call.
|
||||
-}
|
||||
callAPI : { method : String, path : List String } -> ApiPlan { a | baseUrl : () }
|
||||
callAPI { method, path } context =
|
||||
{ attributes =
|
||||
path
|
||||
|> List.map Url.percentEncode
|
||||
|> String.join "/"
|
||||
|> (++) "/"
|
||||
|> UrlPath
|
||||
|> List.singleton
|
||||
, baseUrl = Context.getBaseUrl context
|
||||
, context = context
|
||||
, method = method
|
||||
}
|
||||
|
||||
|
||||
{-| Add an empty attribute that does nothing.
|
||||
-}
|
||||
empty : Attribute a
|
||||
empty =
|
||||
always NoAttr
|
||||
|
||||
|
||||
{-| Adds a JSON value as the HTTP body.
|
||||
-}
|
||||
fullBody : Json.Value -> Attribute a
|
||||
fullBody value _ =
|
||||
FullBody value
|
||||
|
||||
|
||||
{-| Add a boolean value as a query parameter to the URL.
|
||||
-}
|
||||
queryBool : String -> Bool -> Attribute a
|
||||
queryBool key value _ =
|
||||
(if value then
|
||||
"true"
|
||||
|
||||
else
|
||||
"false"
|
||||
)
|
||||
|> UrlBuilder.string key
|
||||
|> QueryParam
|
||||
|
||||
|
||||
{-| Add an integer value as a query parameter to the URL.
|
||||
-}
|
||||
queryInt : String -> Int -> Attribute a
|
||||
queryInt key value _ =
|
||||
QueryParam <| UrlBuilder.int key value
|
||||
|
||||
|
||||
{-| Add a boolean value as a query parameter to the URL if it exists.
|
||||
-}
|
||||
queryOpBool : String -> Maybe Bool -> Attribute a
|
||||
queryOpBool key value =
|
||||
case value of
|
||||
Just v ->
|
||||
queryBool key v
|
||||
|
||||
Nothing ->
|
||||
empty
|
||||
|
||||
|
||||
{-| Add an integer value as a query parameter to the URL if it exists.
|
||||
-}
|
||||
queryOpInt : String -> Maybe Int -> Attribute a
|
||||
queryOpInt key value =
|
||||
case value of
|
||||
Just v ->
|
||||
queryInt key v
|
||||
|
||||
Nothing ->
|
||||
empty
|
||||
|
||||
|
||||
{-| Add a string value as a query parameter to the URL if it exists.
|
||||
-}
|
||||
queryOpString : String -> Maybe String -> Attribute a
|
||||
queryOpString key value =
|
||||
case value of
|
||||
Just v ->
|
||||
queryString key v
|
||||
|
||||
Nothing ->
|
||||
empty
|
||||
|
||||
|
||||
{-| Add a string value as a query parameter to the URL.
|
||||
-}
|
||||
queryString : String -> String -> Attribute a
|
||||
queryString key value _ =
|
||||
QueryParam <| UrlBuilder.string key value
|
||||
|
||||
|
||||
{-| Add more attributes to the API plan.
|
||||
-}
|
||||
withAttributes : List (Attribute a) -> ApiPlan a -> ApiPlan a
|
||||
withAttributes attrs f context =
|
||||
f context
|
||||
|> (\data ->
|
||||
{ data
|
||||
| attributes =
|
||||
attrs
|
||||
|> List.map (\attr -> attr data.context)
|
||||
|> List.append data.attributes
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
{-| Attribute that requires a transaction id to be present.
|
||||
-}
|
||||
withTransactionId : Attribute { a | transaction : () }
|
||||
withTransactionId =
|
||||
Context.getTransaction >> ReplaceInUrl "txnId"
|
Loading…
Reference in New Issue