commit
2e6d07bc42
23
elm.json
23
elm.json
|
@ -4,10 +4,29 @@
|
|||
"summary": "Matrix SDK for instant communication. Unstable beta version for testing only.",
|
||||
"license": "EUPL-1.1",
|
||||
"version": "1.0.0",
|
||||
"exposed-modules": [
|
||||
"Matrix",
|
||||
"Matrix.Settings",
|
||||
"Internal.Config.Default",
|
||||
"Internal.Config.Leaks",
|
||||
"Internal.Config.Text",
|
||||
"Internal.Tools.Decode",
|
||||
"Internal.Tools.Encode",
|
||||
"Internal.Tools.Hashdict",
|
||||
"Internal.Tools.Iddict",
|
||||
"Internal.Tools.Timestamp",
|
||||
"Internal.Tools.VersionControl",
|
||||
"Internal.Values.Context",
|
||||
"Internal.Values.Envelope",
|
||||
"Internal.Values.Vault",
|
||||
"Types"
|
||||
],
|
||||
"elm-version": "0.19.0 <= v < 0.20.0",
|
||||
"exposed-modules": [ "Matrix" ],
|
||||
"dependencies": {
|
||||
"elm/core": "1.0.0 <= v < 2.0.0"
|
||||
"elm/core": "1.0.0 <= v < 2.0.0",
|
||||
"elm/json": "1.0.0 <= v < 2.0.0",
|
||||
"elm/time": "1.0.0 <= v < 2.0.0",
|
||||
"miniBill/elm-fast-dict": "1.0.0 <= v < 2.0.0"
|
||||
},
|
||||
"test-dependencies": {}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
module Internal.Config.Default exposing
|
||||
( currentVersion, deviceName
|
||||
, syncTime
|
||||
)
|
||||
|
||||
{-| This module hosts all default settings and configurations that the Vault
|
||||
will assume until overriden by the user.
|
||||
|
||||
|
||||
## Version management
|
||||
|
||||
@docs currentVersion, deviceName
|
||||
|
||||
|
||||
## Communication config
|
||||
|
||||
@docs syncTime
|
||||
|
||||
-}
|
||||
|
||||
|
||||
{-| The version that is being communicated to the user
|
||||
-}
|
||||
currentVersion : String
|
||||
currentVersion =
|
||||
"beta 1.0.0"
|
||||
|
||||
|
||||
{-| The default device name that is being communicated with the Matrix API.
|
||||
|
||||
This is mostly useful for users who are logged in with multiple sessions.
|
||||
|
||||
-}
|
||||
deviceName : String
|
||||
deviceName =
|
||||
"Elm SDK (" ++ currentVersion ++ ")"
|
||||
|
||||
|
||||
{-| Whenever the Matrix API has nothing new to report, the Elm SDK is kept on
|
||||
hold until something new happens. The `syncTime` indicates a timeout to how long
|
||||
the Elm SDK tolerates being held on hold.
|
||||
|
||||
- ↗️ A high value is good because it significantly reduces traffic between the
|
||||
user and the homeserver.
|
||||
- ↘️ A low value is good because it reduces the risk of
|
||||
the connection ending abruptly or unexpectedly.
|
||||
|
||||
Nowadays, most libraries use 30 seconds as the standard, as does the Elm SDK.
|
||||
The value is in miliseconds, so it is set at 30,000.
|
||||
|
||||
-}
|
||||
syncTime : Int
|
||||
syncTime =
|
||||
30 * 1000
|
|
@ -0,0 +1,60 @@
|
|||
module Internal.Config.Leaks exposing (accessToken, baseUrl, transaction, versions)
|
||||
|
||||
{-|
|
||||
|
||||
|
||||
# Leaks module
|
||||
|
||||
The Elm compiler is quite picky when it comes to handling edge cases, which may
|
||||
occasionally result in requiring us to insert values in impossible states.
|
||||
|
||||
This module offers placeholders for those times. The placeholder values are
|
||||
intentionally called "leaks", because they should be used carefully: a wrongful
|
||||
implementation might cause unexpected behaviour, vulnerabilities or even
|
||||
security risks!
|
||||
|
||||
You should not use this module unless you know what you're doing. That is:
|
||||
|
||||
- By exclusively using leaking values in opaque types so a user cannot
|
||||
accidentally reach an impossible state
|
||||
- By exclusively using leaking values in cases where the compiler is the only
|
||||
reason that the leaking value needs to be used
|
||||
- By exclusively using leaking values if there is no way to circumvent the
|
||||
compiler with a reasonable method.
|
||||
|
||||
One such example would be to turn an `Maybe Int` into an `Int` if you already
|
||||
know 100% sure that the value isn't `Nothing`.
|
||||
|
||||
Just 5 |> Maybe.withDefault Leaks.number
|
||||
|
||||
@docs accessToken, baseUrl, transaction, versions
|
||||
|
||||
-}
|
||||
|
||||
|
||||
{-| Placeholder access token.
|
||||
-}
|
||||
accessToken : String
|
||||
accessToken =
|
||||
"elm-sdk-placeholder-access-token-leaks"
|
||||
|
||||
|
||||
{-| Placeholder base URL.
|
||||
-}
|
||||
baseUrl : String
|
||||
baseUrl =
|
||||
"elm-sdk-placeholder-baseurl-leaks.example.org"
|
||||
|
||||
|
||||
{-| Placeholder transaction id.
|
||||
-}
|
||||
transaction : String
|
||||
transaction =
|
||||
"elm-sdk-placeholder-transaction-leaks"
|
||||
|
||||
|
||||
{-| Placeholder versions list.
|
||||
-}
|
||||
versions : List String
|
||||
versions =
|
||||
[ "elm-sdk-placeholder-versions-leaks" ]
|
|
@ -0,0 +1,116 @@
|
|||
module Internal.Config.Text exposing
|
||||
( versionsFoundLocally, versionsReceived, versionsFailedToDecode
|
||||
, accessTokenFoundLocally, accessTokenExpired, accessTokenInvalid
|
||||
, unsupportedVersionForEndpoint
|
||||
)
|
||||
|
||||
{-| Throughout the Elm SDK, there are lots of pieces of text being used for
|
||||
various purposes. Some of these are:
|
||||
|
||||
- To log what is happening during an API call.
|
||||
- To fail with custom decoder errors.
|
||||
- To describe custom values in a human readable format.
|
||||
|
||||
All magic values of text are gathered in this module, to form a monolithic
|
||||
source of text. This allows people to learn more about the Elm SDK, and it
|
||||
offers room for future translations.
|
||||
|
||||
Optionally, developers can even consider taking the values of some of these
|
||||
variables to interpret them automatically when they appear as logs on the other
|
||||
side. This could be used to automatically detect when the Vault is failing to
|
||||
authenticate, for example, so that a new login screen can be shown. **WARNING:**
|
||||
This is a risky feature, keep in mind that even a patch update might break this!
|
||||
You should only do this if you know what you're doing.
|
||||
|
||||
|
||||
## API Versions
|
||||
|
||||
Messages sent as API logs while the Elm SDK is figuring out how modern the
|
||||
homeserver is and how it can best communicate.
|
||||
|
||||
@docs versionsFoundLocally, versionsReceived, versionsFailedToDecode
|
||||
|
||||
|
||||
## API Authentication
|
||||
|
||||
Messages sent as API logs during the authentication phase of the API
|
||||
interaction.
|
||||
|
||||
@docs accessTokenFoundLocally, accessTokenExpired, accessTokenInvalid
|
||||
|
||||
offers room for translation, re-wording and refactors.
|
||||
|
||||
|
||||
## API miscellaneous messages
|
||||
|
||||
Messages sent as API logs during communication with the API.
|
||||
|
||||
@docs unsupportedVersionForEndpoint
|
||||
|
||||
-}
|
||||
|
||||
|
||||
{-| Logs when the Matrix API returns that an access token is no longer valid.
|
||||
-}
|
||||
accessTokenExpired : String
|
||||
accessTokenExpired =
|
||||
"Matrix API reports access token as no longer valid"
|
||||
|
||||
|
||||
{-| Logs when the Vault has an access token that is still (locally) considered
|
||||
valid.
|
||||
-}
|
||||
accessTokenFoundLocally : String
|
||||
accessTokenFoundLocally =
|
||||
"Found locally cached access token"
|
||||
|
||||
|
||||
{-| Logs when the Matrix API rejects an access token without explicitly
|
||||
mentioning a reason.
|
||||
-}
|
||||
accessTokenInvalid : String
|
||||
accessTokenInvalid =
|
||||
"Matrix API rejected access token as invalid"
|
||||
|
||||
|
||||
{-| The Matrix homeserver can specify how it wishes to communicate, and the Elm
|
||||
SDK aims to communicate accordingly. This may fail in some scenarios, however,
|
||||
in which case it will throw this error.
|
||||
|
||||
Most of the time, the error is caused by one of two options:
|
||||
|
||||
1. The homeserver is very archaic and does not (yet) support API endpoints that
|
||||
are nowadays considered mature.
|
||||
|
||||
2. The homeserver is much more modern than the Elm SDK and either uses
|
||||
exclusively API endpoints that the Elm SDK doesn't (yet) support, or it uses
|
||||
spec versions that aren't considered "official" Matrix spec versions and
|
||||
were designed by a third party.
|
||||
|
||||
-}
|
||||
unsupportedVersionForEndpoint : String
|
||||
unsupportedVersionForEndpoint =
|
||||
"This Matrix homeserver and the Elm SDK do not share a common spec version for this endpoint"
|
||||
|
||||
|
||||
{-| Occasionally, the Matrix homeserver fails to communicate how it is best
|
||||
communicated with. Most of the time, this means that the homeserver is somehow
|
||||
unreachable or some gateway error has occured.
|
||||
-}
|
||||
versionsFailedToDecode : String
|
||||
versionsFailedToDecode =
|
||||
"Matrix API returned an invalid version list"
|
||||
|
||||
|
||||
{-| Logs when the Vault remembers how to communicate with the Matrix homeserver.
|
||||
-}
|
||||
versionsFoundLocally : String
|
||||
versionsFoundLocally =
|
||||
"Found locally cached version list"
|
||||
|
||||
|
||||
{-| Logs when the Matrix API has returned how to best communicate with them.
|
||||
-}
|
||||
versionsReceived : String
|
||||
versionsReceived =
|
||||
"Matrix API returned a version list"
|
|
@ -0,0 +1,155 @@
|
|||
module Internal.Tools.Decode exposing
|
||||
( opField, opFieldWithDefault
|
||||
, map9, map10, map11
|
||||
)
|
||||
|
||||
{-|
|
||||
|
||||
|
||||
# Decode module
|
||||
|
||||
This module contains helper functions that help decode JSON.
|
||||
|
||||
|
||||
## Optional field decoders
|
||||
|
||||
@docs opField, opFieldWithDefault
|
||||
|
||||
|
||||
## Extended map functions
|
||||
|
||||
@docs map9, map10, map11
|
||||
|
||||
-}
|
||||
|
||||
import Json.Decode as D
|
||||
|
||||
|
||||
{-| Add an optional field decoder. If the field exists, the decoder will fail
|
||||
if the field doesn't decode properly.
|
||||
|
||||
This decoder standard out from `D.maybe <| D.field fieldName decoder` because
|
||||
that will decode into a `Nothing` if the `decoder` fails. This function will
|
||||
only decode into a `Nothing` if the field doesn't exist, and will fail if
|
||||
`decoder` fails.
|
||||
|
||||
The function also returns Nothing if the field exists but it is null.
|
||||
|
||||
-}
|
||||
opField : String -> D.Decoder a -> D.Decoder (Maybe a)
|
||||
opField fieldName decoder =
|
||||
D.value
|
||||
|> D.field fieldName
|
||||
|> D.maybe
|
||||
|> D.andThen
|
||||
(\v ->
|
||||
case v of
|
||||
Just _ ->
|
||||
D.oneOf
|
||||
[ D.null Nothing
|
||||
, D.map Just decoder
|
||||
]
|
||||
|> D.field fieldName
|
||||
|
||||
Nothing ->
|
||||
D.succeed Nothing
|
||||
)
|
||||
|
||||
|
||||
{-| Add an optional field decoder. If the field is not given, the decoder will
|
||||
return a default value. If the field exists, the decoder will fail if the field
|
||||
doesn't decode properly.
|
||||
-}
|
||||
opFieldWithDefault : String -> a -> D.Decoder a -> D.Decoder a
|
||||
opFieldWithDefault fieldName default decoder =
|
||||
opField fieldName decoder |> D.map (Maybe.withDefault default)
|
||||
|
||||
|
||||
{-| Try 9 decoders and combine the result.
|
||||
-}
|
||||
map9 :
|
||||
(a -> b -> c -> d -> e -> f -> g -> h -> i -> value)
|
||||
-> D.Decoder a
|
||||
-> D.Decoder b
|
||||
-> D.Decoder c
|
||||
-> D.Decoder d
|
||||
-> D.Decoder e
|
||||
-> D.Decoder f
|
||||
-> D.Decoder g
|
||||
-> D.Decoder h
|
||||
-> D.Decoder i
|
||||
-> D.Decoder value
|
||||
map9 func da db dc dd de df dg dh di =
|
||||
D.map8
|
||||
(\a b c d e f g ( h, i ) ->
|
||||
func a b c d e f g h i
|
||||
)
|
||||
da
|
||||
db
|
||||
dc
|
||||
dd
|
||||
de
|
||||
df
|
||||
dg
|
||||
(D.map2 Tuple.pair dh di)
|
||||
|
||||
|
||||
{-| Try 10 decoders and combine the result.
|
||||
-}
|
||||
map10 :
|
||||
(a -> b -> c -> d -> e -> f -> g -> h -> i -> j -> value)
|
||||
-> D.Decoder a
|
||||
-> D.Decoder b
|
||||
-> D.Decoder c
|
||||
-> D.Decoder d
|
||||
-> D.Decoder e
|
||||
-> D.Decoder f
|
||||
-> D.Decoder g
|
||||
-> D.Decoder h
|
||||
-> D.Decoder i
|
||||
-> D.Decoder j
|
||||
-> D.Decoder value
|
||||
map10 func da db dc dd de df dg dh di dj =
|
||||
D.map8
|
||||
(\a b c d e f ( g, h ) ( i, j ) ->
|
||||
func a b c d e f g h i j
|
||||
)
|
||||
da
|
||||
db
|
||||
dc
|
||||
dd
|
||||
de
|
||||
df
|
||||
(D.map2 Tuple.pair dg dh)
|
||||
(D.map2 Tuple.pair di dj)
|
||||
|
||||
|
||||
{-| Try 11 decoders and combine the result.
|
||||
-}
|
||||
map11 :
|
||||
(a -> b -> c -> d -> e -> f -> g -> h -> i -> j -> k -> value)
|
||||
-> D.Decoder a
|
||||
-> D.Decoder b
|
||||
-> D.Decoder c
|
||||
-> D.Decoder d
|
||||
-> D.Decoder e
|
||||
-> D.Decoder f
|
||||
-> D.Decoder g
|
||||
-> D.Decoder h
|
||||
-> D.Decoder i
|
||||
-> D.Decoder j
|
||||
-> D.Decoder k
|
||||
-> D.Decoder value
|
||||
map11 func da db dc dd de df dg dh di dj dk =
|
||||
D.map8
|
||||
(\a b c d e ( f, g ) ( h, i ) ( j, k ) ->
|
||||
func a b c d e f g h i j k
|
||||
)
|
||||
da
|
||||
db
|
||||
dc
|
||||
dd
|
||||
de
|
||||
(D.map2 Tuple.pair df dg)
|
||||
(D.map2 Tuple.pair dh di)
|
||||
(D.map2 Tuple.pair dj dk)
|
|
@ -0,0 +1,52 @@
|
|||
module Internal.Tools.Encode exposing (maybeObject)
|
||||
|
||||
{-|
|
||||
|
||||
|
||||
# Encode module
|
||||
|
||||
This module contains helper functions that help decode JSON.
|
||||
|
||||
|
||||
# Optional body object
|
||||
|
||||
@docs maybeObject
|
||||
|
||||
-}
|
||||
|
||||
import Json.Encode as E
|
||||
|
||||
|
||||
{-| Create a body object based on optionally provided values.
|
||||
|
||||
In other words, the following two variables create the same JSON value:
|
||||
|
||||
value1 : Json.Encode.Value
|
||||
value1 =
|
||||
maybeObject
|
||||
[ ( "name", Just (Json.Encode.string "Alice") )
|
||||
, ( "age", Nothing )
|
||||
, ( "height", Just (Json.Encode.float 1.61) )
|
||||
, ( "weight", Nothing )
|
||||
]
|
||||
|
||||
value2 : Json.Encode.Value
|
||||
value2 =
|
||||
Json.Encode.object
|
||||
[ ( "name", Json.Encode.string "Alice" )
|
||||
, ( "height", Json.Encode.float 1.61 )
|
||||
]
|
||||
|
||||
-}
|
||||
maybeObject : List ( String, Maybe E.Value ) -> E.Value
|
||||
maybeObject =
|
||||
List.filterMap
|
||||
(\( name, value ) ->
|
||||
case value of
|
||||
Just v ->
|
||||
Just ( name, v )
|
||||
|
||||
_ ->
|
||||
Nothing
|
||||
)
|
||||
>> E.object
|
|
@ -0,0 +1,266 @@
|
|||
module Internal.Tools.Hashdict exposing
|
||||
( Hashdict
|
||||
, empty, singleton, insert, remove, removeKey
|
||||
, isEmpty, member, memberKey, get, size
|
||||
, keys, values, toList, fromList
|
||||
, rehash, union
|
||||
, encode, decoder, softDecoder
|
||||
)
|
||||
|
||||
{-| This module abstracts the `Dict` type with one function that assigns a
|
||||
unique identifier for each value based on a function that assigns each value.
|
||||
|
||||
This allows you to store values based on an externally defined identifier.
|
||||
|
||||
|
||||
## Dictionaries
|
||||
|
||||
@docs Hashdict
|
||||
|
||||
|
||||
## Build
|
||||
|
||||
@docs empty, singleton, insert, remove, removeKey
|
||||
|
||||
|
||||
## Query
|
||||
|
||||
@docs isEmpty, member, memberKey, get, size
|
||||
|
||||
|
||||
## Lists
|
||||
|
||||
@docs keys, values, toList, fromList
|
||||
|
||||
|
||||
## Transform
|
||||
|
||||
@docs rehash, union
|
||||
|
||||
|
||||
## JSON coders
|
||||
|
||||
@docs encode, decoder, softDecoder
|
||||
|
||||
-}
|
||||
|
||||
import FastDict as Dict exposing (Dict)
|
||||
import Json.Decode as D
|
||||
import Json.Encode as E
|
||||
|
||||
|
||||
{-| A dictionary of keys and values where each key is defined by its value. For
|
||||
example, this can be useful when every user is identifiable by their username:
|
||||
|
||||
import Hashdict exposing (Hashdict)
|
||||
|
||||
users : Hashdict User
|
||||
users =
|
||||
Hashdict.fromList .name
|
||||
[ User "Alice" 28 1.65
|
||||
, User "Bob" 19 1.82
|
||||
, User "Chuck" 33 1.75
|
||||
]
|
||||
|
||||
type alias User =
|
||||
{ name : String
|
||||
, age : Int
|
||||
, height : Float
|
||||
}
|
||||
|
||||
In the example listed above, the users are stored by their username, which means
|
||||
that all you need to know is the value "Alice" to retrieve all the information
|
||||
about them. Additionally, you do not need to specify a key to insert the values.
|
||||
|
||||
-}
|
||||
type Hashdict a
|
||||
= Hashdict
|
||||
{ hash : a -> String
|
||||
, values : Dict String a
|
||||
}
|
||||
|
||||
|
||||
{-| Decode a hashdict from a JSON value. To create a hashdict, you are expected
|
||||
to insert a hash function. If the hash function doesn't properly hash the values
|
||||
as expected, the decoder will fail to decode the hashdict.
|
||||
-}
|
||||
decoder : (a -> String) -> D.Decoder a -> D.Decoder (Hashdict a)
|
||||
decoder f xDecoder =
|
||||
D.keyValuePairs xDecoder
|
||||
|> D.andThen
|
||||
(\items ->
|
||||
if List.all (\( hash, value ) -> f value == hash) items then
|
||||
items
|
||||
|> Dict.fromList
|
||||
|> (\d -> { hash = f, values = d })
|
||||
|> Hashdict
|
||||
|> D.succeed
|
||||
|
||||
else
|
||||
D.fail "Hash function fails to properly hash all values"
|
||||
)
|
||||
|
||||
|
||||
{-| Create an empty hashdict.
|
||||
-}
|
||||
empty : (a -> String) -> Hashdict a
|
||||
empty hash =
|
||||
Hashdict { hash = hash, values = Dict.empty }
|
||||
|
||||
|
||||
{-| Encode a Hashdict into a JSON value. Keep in mind that an Elm function
|
||||
cannot be universally converted to JSON, so it is up to you to preserve that
|
||||
hash function!
|
||||
-}
|
||||
encode : (a -> E.Value) -> Hashdict a -> E.Value
|
||||
encode encodeX (Hashdict h) =
|
||||
h.values
|
||||
|> Dict.toList
|
||||
|> List.map (Tuple.mapSecond encodeX)
|
||||
|> E.object
|
||||
|
||||
|
||||
{-| Convert an association list into a hashdict.
|
||||
-}
|
||||
fromList : (a -> String) -> List a -> Hashdict a
|
||||
fromList hash xs =
|
||||
Hashdict
|
||||
{ hash = hash
|
||||
, values =
|
||||
xs
|
||||
|> List.map (\x -> ( hash x, x ))
|
||||
|> Dict.fromList
|
||||
}
|
||||
|
||||
|
||||
{-| Get the value associated with a hash. If the hash is not found, return
|
||||
`Nothing`. This is useful when you are not sure if a hash will be in the
|
||||
hashdict.
|
||||
-}
|
||||
get : String -> Hashdict a -> Maybe a
|
||||
get k (Hashdict h) =
|
||||
Dict.get k h.values
|
||||
|
||||
|
||||
{-| Insert a value into a hashdict. The key is automatically generated by the
|
||||
hash function. If the function generates a collision, it replaces the existing
|
||||
value in the hashdict.
|
||||
-}
|
||||
insert : a -> Hashdict a -> Hashdict a
|
||||
insert v (Hashdict h) =
|
||||
Hashdict { h | values = Dict.insert (h.hash v) v h.values }
|
||||
|
||||
|
||||
{-| Determine if a hashdict is empty.
|
||||
-}
|
||||
isEmpty : Hashdict a -> Bool
|
||||
isEmpty (Hashdict h) =
|
||||
Dict.isEmpty h.values
|
||||
|
||||
|
||||
{-| Get all of the hashes in a hashdict, sorted from lowest to highest.
|
||||
-}
|
||||
keys : Hashdict a -> List String
|
||||
keys (Hashdict h) =
|
||||
Dict.keys h.values
|
||||
|
||||
|
||||
{-| Determine if a value's hash is in a hashdict.
|
||||
-}
|
||||
member : a -> Hashdict a -> Bool
|
||||
member value (Hashdict h) =
|
||||
Dict.member (h.hash value) h.values
|
||||
|
||||
|
||||
{-| Determine if a hash is in a hashdict.
|
||||
-}
|
||||
memberKey : String -> Hashdict a -> Bool
|
||||
memberKey key (Hashdict h) =
|
||||
Dict.member key h.values
|
||||
|
||||
|
||||
{-| Remap a hashdict using a new hashing algorithm.
|
||||
-}
|
||||
rehash : (a -> String) -> Hashdict a -> Hashdict a
|
||||
rehash f (Hashdict h) =
|
||||
Hashdict
|
||||
{ hash = f
|
||||
, values =
|
||||
h.values
|
||||
|> Dict.values
|
||||
|> List.map (\v -> ( f v, v ))
|
||||
|> Dict.fromList
|
||||
}
|
||||
|
||||
|
||||
{-| Remove a value from a hashdict. If the value's hash is found, the key-value
|
||||
pair is removed. If the value's hash is not found, no changes are made.
|
||||
|
||||
hdict |> Hashdict.remove (User "Alice" 19 1.82)
|
||||
|
||||
-}
|
||||
remove : a -> Hashdict a -> Hashdict a
|
||||
remove v (Hashdict h) =
|
||||
Hashdict { h | values = Dict.remove (h.hash v) h.values }
|
||||
|
||||
|
||||
{-| Remove a key from a hashdict. If the key is not found, no changes are made.
|
||||
|
||||
hdict |> Hashdict.removeKey "Alice"
|
||||
|
||||
-}
|
||||
removeKey : String -> Hashdict a -> Hashdict a
|
||||
removeKey k (Hashdict h) =
|
||||
Hashdict { h | values = Dict.remove k h.values }
|
||||
|
||||
|
||||
{-| Create a hashdict with a single key-value pair.
|
||||
-}
|
||||
singleton : (a -> String) -> a -> Hashdict a
|
||||
singleton f v =
|
||||
empty f |> insert v
|
||||
|
||||
|
||||
{-| Determine the number of values in a hashdict.
|
||||
-}
|
||||
size : Hashdict a -> Int
|
||||
size (Hashdict h) =
|
||||
Dict.size h.values
|
||||
|
||||
|
||||
{-| Decode a hashdict from a JSON value. If you cannot deduce the originally
|
||||
used hash function, (or if you simply do not care) you can use this function to
|
||||
decode and rehash the Hashdict using your new hash function.
|
||||
-}
|
||||
softDecoder : (a -> String) -> D.Decoder a -> D.Decoder (Hashdict a)
|
||||
softDecoder f xDecoder =
|
||||
D.keyValuePairs xDecoder
|
||||
|> D.map (List.map Tuple.second >> fromList f)
|
||||
|
||||
|
||||
{-| Convert a hashdict into an association list of key-value pairs, sorted by
|
||||
keys.
|
||||
-}
|
||||
toList : Hashdict a -> List ( String, a )
|
||||
toList (Hashdict h) =
|
||||
Dict.toList h.values
|
||||
|
||||
|
||||
{-| Combine two hashdicts under the hash function of the first. If there is a
|
||||
collision, preference is given to the first hashdict.
|
||||
-}
|
||||
union : Hashdict a -> Hashdict a -> Hashdict a
|
||||
union (Hashdict h1) hd2 =
|
||||
case rehash h1.hash hd2 of
|
||||
Hashdict h2 ->
|
||||
Hashdict
|
||||
{ hash = h1.hash
|
||||
, values = Dict.union h1.values h2.values
|
||||
}
|
||||
|
||||
|
||||
{-| Get all values stored in the hashdict, in the order of their keys.
|
||||
-}
|
||||
values : Hashdict a -> List a
|
||||
values (Hashdict h) =
|
||||
Dict.values h.values
|
|
@ -0,0 +1,197 @@
|
|||
module Internal.Tools.Iddict exposing
|
||||
( Iddict
|
||||
, empty, singleton, insert, map, remove
|
||||
, isEmpty, member, get, size
|
||||
, keys, values
|
||||
, encode, decoder
|
||||
)
|
||||
|
||||
{-| The id-dict is a data type that lets us store values in a dictionary using
|
||||
unique identifiers. This can be used as a dictionary where the keys do not
|
||||
matter.
|
||||
|
||||
The benefit of the iddict is that it generates the keys FOR you. This way, you
|
||||
do not need to generate identifiers yourself.
|
||||
|
||||
|
||||
## Id-dict
|
||||
|
||||
@docs Iddict
|
||||
|
||||
|
||||
## Build
|
||||
|
||||
@docs empty, singleton, insert, map, remove
|
||||
|
||||
|
||||
## Query
|
||||
|
||||
@docs isEmpty, member, get, size
|
||||
|
||||
|
||||
## Lists
|
||||
|
||||
@docs keys, values
|
||||
|
||||
|
||||
## JSON coders
|
||||
|
||||
@docs encode, decoder
|
||||
|
||||
-}
|
||||
|
||||
import FastDict as Dict exposing (Dict)
|
||||
import Json.Decode as D
|
||||
import Json.Encode as E
|
||||
|
||||
|
||||
{-| The Iddict data type.
|
||||
-}
|
||||
type Iddict a
|
||||
= Iddict
|
||||
{ cursor : Int
|
||||
, dict : Dict Int a
|
||||
}
|
||||
|
||||
|
||||
{-| Decode an id-dict from a JSON value.
|
||||
-}
|
||||
decoder : D.Decoder a -> D.Decoder (Iddict a)
|
||||
decoder xDecoder =
|
||||
D.map2
|
||||
(\c pairs ->
|
||||
let
|
||||
dict : Dict Int a
|
||||
dict =
|
||||
pairs
|
||||
|> List.filterMap
|
||||
(\( k, v ) ->
|
||||
k
|
||||
|> String.toInt
|
||||
|> Maybe.map (\n -> ( n, v ))
|
||||
)
|
||||
|> Dict.fromList
|
||||
in
|
||||
Iddict
|
||||
{ cursor =
|
||||
Dict.keys dict
|
||||
-- Larger than all values in the list
|
||||
|> List.map ((+) 1)
|
||||
|> List.maximum
|
||||
|> Maybe.withDefault 0
|
||||
|> max (Dict.size dict)
|
||||
-- At least the dict size
|
||||
|> max c
|
||||
|
||||
-- At least the given value
|
||||
, dict = dict
|
||||
}
|
||||
)
|
||||
(D.field "cursor" D.int)
|
||||
(D.field "dict" <| D.keyValuePairs xDecoder)
|
||||
|
||||
|
||||
{-| Create an empty id-dict.
|
||||
-}
|
||||
empty : Iddict a
|
||||
empty =
|
||||
Iddict
|
||||
{ cursor = 0
|
||||
, dict = Dict.empty
|
||||
}
|
||||
|
||||
|
||||
{-| Encode an id-dict to a JSON value.
|
||||
-}
|
||||
encode : (a -> E.Value) -> Iddict a -> E.Value
|
||||
encode encodeX (Iddict d) =
|
||||
E.object
|
||||
[ ( "cursor", E.int d.cursor )
|
||||
, ( "dict"
|
||||
, d.dict
|
||||
|> Dict.toCoreDict
|
||||
|> E.dict String.fromInt encodeX
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
{-| Get a value from the id-dict using its key.
|
||||
-}
|
||||
get : Int -> Iddict a -> Maybe a
|
||||
get k (Iddict { dict }) =
|
||||
Dict.get k dict
|
||||
|
||||
|
||||
{-| Insert a new value into the id-dict. Given that the id-dict generates its
|
||||
key, the function returns both the updated id-dict as the newly generated key.
|
||||
|
||||
x = empty |> insert "hello" -- ( 0, <Iddict with value "hello"> )
|
||||
|
||||
case x of
|
||||
( _, iddict ) ->
|
||||
get 0 iddict -- Just "hello"
|
||||
|
||||
-}
|
||||
insert : a -> Iddict a -> ( Int, Iddict a )
|
||||
insert v (Iddict d) =
|
||||
( d.cursor
|
||||
, Iddict { cursor = d.cursor + 1, dict = Dict.insert d.cursor v d.dict }
|
||||
)
|
||||
|
||||
|
||||
{-| Determine if an id-dict is empty.
|
||||
-}
|
||||
isEmpty : Iddict a -> Bool
|
||||
isEmpty (Iddict d) =
|
||||
Dict.isEmpty d.dict
|
||||
|
||||
|
||||
{-| Get all of the keys from the id-dict, sorted from lowest to highest.
|
||||
-}
|
||||
keys : Iddict a -> List Int
|
||||
keys (Iddict { dict }) =
|
||||
Dict.keys dict
|
||||
|
||||
|
||||
{-| Map an existing value at a given key, if it exists. If it does not exist,
|
||||
the operation does nothing.
|
||||
-}
|
||||
map : Int -> (a -> a) -> Iddict a -> Iddict a
|
||||
map k f (Iddict d) =
|
||||
Iddict { d | dict = Dict.update k (Maybe.map f) d.dict }
|
||||
|
||||
|
||||
{-| Determine if a key is in an id-dict.
|
||||
-}
|
||||
member : Int -> Iddict a -> Bool
|
||||
member k (Iddict d) =
|
||||
k < d.cursor && Dict.member k d.dict
|
||||
|
||||
|
||||
{-| Remove a key-value pair from the id-dict. If the key is not found, no
|
||||
changes are made.
|
||||
-}
|
||||
remove : Int -> Iddict a -> Iddict a
|
||||
remove k (Iddict d) =
|
||||
Iddict { d | dict = Dict.remove k d.dict }
|
||||
|
||||
|
||||
{-| Create an id-dict with a single value.
|
||||
-}
|
||||
singleton : a -> ( Int, Iddict a )
|
||||
singleton v =
|
||||
insert v empty
|
||||
|
||||
|
||||
{-| Determine the number of key-value pairs in the id-dict.
|
||||
-}
|
||||
size : Iddict a -> Int
|
||||
size (Iddict d) =
|
||||
Dict.size d.dict
|
||||
|
||||
|
||||
{-| Get all of the values from an id-dict, in the order of their keys.
|
||||
-}
|
||||
values : Iddict a -> List a
|
||||
values (Iddict { dict }) =
|
||||
Dict.values dict
|
|
@ -0,0 +1,43 @@
|
|||
module Internal.Tools.Timestamp exposing
|
||||
( Timestamp
|
||||
, encode, decoder
|
||||
)
|
||||
|
||||
{-| The Timestamp module is a simplification of the Timestamp as delivered by
|
||||
elm/time. This module offers ways to work with the timestamp in meaningful ways.
|
||||
|
||||
|
||||
## Timestamp
|
||||
|
||||
@docs Timestamp
|
||||
|
||||
|
||||
## JSON coders
|
||||
|
||||
@docs encode, decoder
|
||||
|
||||
-}
|
||||
|
||||
import Json.Decode as D
|
||||
import Json.Encode as E
|
||||
import Time
|
||||
|
||||
|
||||
{-| The Timestamp data type representing a moment in time.
|
||||
-}
|
||||
type alias Timestamp =
|
||||
Time.Posix
|
||||
|
||||
|
||||
{-| Encode a timestamp into a JSON value.
|
||||
-}
|
||||
encode : Timestamp -> E.Value
|
||||
encode =
|
||||
Time.posixToMillis >> E.int
|
||||
|
||||
|
||||
{-| Decode a timestamp from a JSON value.
|
||||
-}
|
||||
decoder : D.Decoder Timestamp
|
||||
decoder =
|
||||
D.map Time.millisToPosix D.int
|
|
@ -0,0 +1,365 @@
|
|||
module Internal.Tools.VersionControl exposing
|
||||
( VersionControl, withBottomLayer
|
||||
, sameForVersion, MiddleLayer, addMiddleLayer
|
||||
, isSupported, toDict, fromVersion, mostRecentFromVersionList, fromVersionList
|
||||
)
|
||||
|
||||
{-|
|
||||
|
||||
|
||||
# Version Control module
|
||||
|
||||
This module helps you maintain different functions based on their version.
|
||||
|
||||
Not every Matrix homeserver is the same. Some keep up with the latest Matrix
|
||||
specifications, while others stay behind because they have to support legacy
|
||||
projects who do not support new API endpoints (yet). The Elm SDK aims to support
|
||||
as many homeserver versions as possible - at the same time.
|
||||
|
||||
Support for legacy versions can be difficult! The Elm SDK expects one way of
|
||||
getting information, and translating every Matrix spec(ification) version to it
|
||||
can take time. But what if a new Matrix spec version adds a new feature? Do we
|
||||
need to re-translate every single version to accomodate any future updates?
|
||||
|
||||
The VersionControl helps define different API rules for different spec versions
|
||||
in an easy way. The VersionControl module puts all the versions in a linear
|
||||
timeline. (Because, you know, updates are usually newer versions of older
|
||||
versions.) This way, you can define different behaviour while still having only
|
||||
one input, one output.
|
||||
|
||||
The module can be best described as a layered version type.
|
||||
|
||||
|----------------------------------------------|
|
||||
| VersionControl |
|
||||
| input output |
|
||||
| | ^ |
|
||||
|---------------------- | -------------- | ----|
|
||||
| |
|
||||
|---------------------- | -------------- | ----|
|
||||
| MiddleLayer v3 | | |
|
||||
| [---> current ---] |
|
||||
| | | |
|
||||
| downcast upcast |
|
||||
| | ^ |
|
||||
|---------------------- | -------------- | ----|
|
||||
| |
|
||||
|---------------------- | -------------- | ----|
|
||||
| MiddleLayer v2 | | |
|
||||
| [---> current ---] |
|
||||
| | | |
|
||||
| downcast upcast |
|
||||
| | ^ |
|
||||
|---------------------- | -------------- | ----|
|
||||
| |
|
||||
|---------------------- | -------------- | ----|
|
||||
| BottomLayer v1 | | |
|
||||
| \---> current ---/ |
|
||||
| |
|
||||
|----------------------------------------------|
|
||||
|
||||
This method means you only need to write one downcast, one current and one
|
||||
upcast whenever you introduce a new version. In other words, you can instantly
|
||||
update all functions without having to write every version!
|
||||
|
||||
The VersionControl keeps tracks the version order. This way, you can either get
|
||||
the VersionControl type to render the function for the most recent supported
|
||||
version, or you can choose for yourself which version you prefer to use.
|
||||
|
||||
|
||||
## Building a VersionControl
|
||||
|
||||
To build a VersionControl type, one must start with the bottom layer and start
|
||||
building up to newer versions with middle layers.
|
||||
|
||||
|
||||
### Create
|
||||
|
||||
@docs VersionControl, withBottomLayer
|
||||
|
||||
|
||||
### Expand
|
||||
|
||||
@docs sameForVersion, MiddleLayer, addMiddleLayer
|
||||
|
||||
|
||||
## Getting functions
|
||||
|
||||
Once you've successfully built the VersionControl type, there's a variety of
|
||||
ways in which you can find an appropriate function.
|
||||
|
||||
@docs isSupported, toDict, fromVersion, mostRecentFromVersionList, fromVersionList
|
||||
|
||||
-}
|
||||
|
||||
import Dict exposing (Dict)
|
||||
|
||||
|
||||
{-| The VersionControl layer is the layer on top that keeps track of all the
|
||||
available versions. It is usually defined with a bottom layer and a few layers
|
||||
on top.
|
||||
-}
|
||||
type VersionControl input output
|
||||
= VersionControl
|
||||
{ latestVersion : input -> output
|
||||
, order : List String
|
||||
, versions : Dict String (input -> output)
|
||||
}
|
||||
|
||||
|
||||
{-| The middle layer is placed between a VersionControl and a BottomLayer to
|
||||
support a new function for a new version. The abbreviations stand for the
|
||||
following:
|
||||
|
||||
- `cin` means **current in**. It is the Middle Layer's input.
|
||||
|
||||
- `cout` means **current out**. It is the Middle Layer's output.
|
||||
|
||||
- `din` means **downcast in**. It is the Bottom Layer's input.
|
||||
|
||||
- `dout` means **downcast out**. It is the Bottom Layer's output.
|
||||
|
||||
As a result, we have the following model to explain the MiddleLayer:
|
||||
|
||||
|----------------------------------------------|
|
||||
| VersionControl |
|
||||
| input output |
|
||||
| | ^ |
|
||||
|---------------------- | -------------- | ----|
|
||||
[cin] [cout]
|
||||
|---------------------- | -------------- | ----|
|
||||
| MiddleLayer | | |
|
||||
| [---> current ---] |
|
||||
| | | |
|
||||
| downcast upcast |
|
||||
| | ^ |
|
||||
|---------------------- | -------------- | ----|
|
||||
[din] [dout]
|
||||
|---------------------- | -------------- | ----|
|
||||
| BottomLayer | | |
|
||||
| \---> current ---/ |
|
||||
| |
|
||||
|----------------------------------------------|
|
||||
|
||||
To sew a MiddleLayer type, we need the `downcast` and `upcast` functions to
|
||||
translate the `cin` and `cout` to meaningful values `din` and `dout` for the
|
||||
BottomLayer function.
|
||||
|
||||
Usually, this means transforming the data. For example, say our BottomLayer
|
||||
still has an old version where people had just one name, and our MiddleLayer
|
||||
version has two fields: a first and last name.
|
||||
|
||||
type alias NewUser =
|
||||
{ firstName : String, lastName : String, age : Int }
|
||||
|
||||
type alias OldUser =
|
||||
{ name : String, age : Int }
|
||||
|
||||
An appropriate downcasting function could then something like the following:
|
||||
|
||||
downcast : NewUser -> OldUser
|
||||
downcast user =
|
||||
{ name = user.firstName ++ " " ++ user.lastName, age = user.age }
|
||||
|
||||
-}
|
||||
type alias MiddleLayer cin cout din dout =
|
||||
{ current : cin -> cout
|
||||
, downcast : cin -> din
|
||||
, upcast : dout -> cout
|
||||
, version : String
|
||||
}
|
||||
|
||||
|
||||
{-| Add a MiddleLayer to the VersionControl, effectively updating all old
|
||||
functions with a downcast and upcast to deal with the inputs and outputs of all
|
||||
functions at the same time.
|
||||
|
||||
For example, using the `NewUser` and `OldUser` types, one could create the
|
||||
following example to get the user's names:
|
||||
|
||||
vc : VersionControl NewUser String
|
||||
vc =
|
||||
withBottomLayer
|
||||
{ current = .name
|
||||
, version = "v1"
|
||||
}
|
||||
|> sameForVersion "v2"
|
||||
|> sameForVersion "v3"
|
||||
|> sameForVersion "v4"
|
||||
|> sameForVersion "v5"
|
||||
|> sameForVersion "v6"
|
||||
|> addMiddleLayer
|
||||
{ downcast = \user -> { name = user.firstName ++ " " ++ user.lastName, age = user.age }
|
||||
, current = \user -> user.firstName ++ " " ++ user.lastName
|
||||
, upcast = identity
|
||||
, version = "v7"
|
||||
}
|
||||
|
||||
Effectively, even though versions `v1` through `v6` still require an `OldUser`
|
||||
type as an input, all functions have now been updated to the new standard of
|
||||
getting a `NewUser` as an input thanks to the `downcast` function.
|
||||
|
||||
-}
|
||||
addMiddleLayer : MiddleLayer cin cout din dout -> VersionControl din dout -> VersionControl cin cout
|
||||
addMiddleLayer { current, downcast, upcast, version } (VersionControl d) =
|
||||
VersionControl
|
||||
{ latestVersion = current
|
||||
, order = version :: d.order
|
||||
, versions =
|
||||
d.versions
|
||||
|> Dict.map (\_ f -> downcast >> f >> upcast)
|
||||
|> Dict.insert version current
|
||||
}
|
||||
|
||||
|
||||
{-| Get the function that corresponds with a given version. Returns `Nothing` if
|
||||
the version has never been inserted into the VersionControl type.
|
||||
-}
|
||||
fromVersion : String -> VersionControl a b -> Maybe (a -> b)
|
||||
fromVersion version (VersionControl { versions }) =
|
||||
Dict.get version versions
|
||||
|
||||
|
||||
{-| Provided a list of versions, this function will provide a list of compatible versions to you in your preferred order.
|
||||
|
||||
If you just care about getting the most recent function, you will be better off using `mostRecentFromVersionList`,
|
||||
but this function can help if you care about knowing which Matrix spec version you're using.
|
||||
|
||||
-}
|
||||
fromVersionList : List String -> VersionControl a b -> List ( String, a -> b )
|
||||
fromVersionList versionList vc =
|
||||
List.filterMap
|
||||
(\version ->
|
||||
vc
|
||||
|> fromVersion version
|
||||
|> Maybe.map (\f -> ( version, f ))
|
||||
)
|
||||
versionList
|
||||
|
||||
|
||||
{-| Determine if a version is supported by the VersionControl.
|
||||
|
||||
vc : VersionControl NewUser String
|
||||
vc =
|
||||
withBottomLayer
|
||||
{ current = .name
|
||||
, version = "v1"
|
||||
}
|
||||
|> sameForVersion "v2"
|
||||
|> sameForVersion "v3"
|
||||
|> sameForVersion "v4"
|
||||
|
||||
isSupported "v3" vc -- True
|
||||
isSupported "v9" vc -- False
|
||||
|
||||
-}
|
||||
isSupported : String -> VersionControl a b -> Bool
|
||||
isSupported version (VersionControl d) =
|
||||
Dict.member version d.versions
|
||||
|
||||
|
||||
{-| Get the most recent event based on a list of versions. Returns `Nothing` if
|
||||
the list is empty, or if none of the versions are supported.
|
||||
|
||||
vc : VersionControl a b
|
||||
vc =
|
||||
withBottomLayer
|
||||
{ current = foo
|
||||
, version = "v1"
|
||||
}
|
||||
|> sameForVersion "v2"
|
||||
|> sameForVersion "v3"
|
||||
|> sameForVersion "v4"
|
||||
|> sameForVersion "v5"
|
||||
|> sameForVersion "v6"
|
||||
|
||||
-- This returns the function for v6 because that is the most recent version
|
||||
-- in the provided version list
|
||||
mostRecentFromVersionList [ "v5", "v3", "v7", "v6", "v8" ] vc
|
||||
|
||||
-}
|
||||
mostRecentFromVersionList : List String -> VersionControl a b -> Maybe (a -> b)
|
||||
mostRecentFromVersionList versionList ((VersionControl { order }) as vc) =
|
||||
order
|
||||
|> List.filter (\o -> List.member o versionList)
|
||||
|> List.filterMap (\v -> fromVersion v vc)
|
||||
|> List.head
|
||||
|
||||
|
||||
{-| Not every version overhauls every interaction. For this reason, many version
|
||||
functions are identical to their previous functions.
|
||||
|
||||
This function adds a new version to the VersionControl and tells it that the
|
||||
version uses the same function as the previous version.
|
||||
|
||||
vc : VersionControl User String
|
||||
vc =
|
||||
withBottomLayer
|
||||
{ current = .name
|
||||
, version = "v1"
|
||||
}
|
||||
|> sameForVersion "v2"
|
||||
|> sameForVersion "v3"
|
||||
|> sameForVersion "v4"
|
||||
|> sameForVersion "v5"
|
||||
|> sameForVersion "v6"
|
||||
|
||||
The example above lists the function `.name` for versions `v1` through `v6`.
|
||||
|
||||
-}
|
||||
sameForVersion : String -> VersionControl a b -> VersionControl a b
|
||||
sameForVersion version (VersionControl data) =
|
||||
VersionControl
|
||||
{ data
|
||||
| order = version :: data.order
|
||||
, versions = Dict.insert version data.latestVersion data.versions
|
||||
}
|
||||
|
||||
|
||||
{-| Get a dict of all available functions.
|
||||
|
||||
|
||||
vc : VersionControl NewUser String
|
||||
vc =
|
||||
withBottomLayer
|
||||
{ current = .name
|
||||
, version = "v1"
|
||||
}
|
||||
|> sameForVersion "v2"
|
||||
|> sameForVersion "v3"
|
||||
|> sameForVersion "v4"
|
||||
|> toDict
|
||||
|
||||
-- Dict.fromList
|
||||
-- [ ( "v1", <internal> )
|
||||
-- , ( "v2", <internal> )
|
||||
-- , ( "v3", <internal> )
|
||||
-- , ( "v4", <internal> )
|
||||
-- ]
|
||||
|
||||
-}
|
||||
toDict : VersionControl a b -> Dict String (a -> b)
|
||||
toDict (VersionControl d) =
|
||||
d.versions
|
||||
|
||||
|
||||
{-| You cannot create an empty VersionControl layer, you must always start with a BottomLayer
|
||||
and then stack MiddleLayer types on top until you've reached the version that you're happy with.
|
||||
|
||||
vc : VersionControl User String
|
||||
vc =
|
||||
withBottomLayer
|
||||
{ current = .name
|
||||
, version = "v1"
|
||||
}
|
||||
|
||||
type alias User =
|
||||
{ name : String, age : Int }
|
||||
|
||||
-}
|
||||
withBottomLayer : { current : input -> output, version : String } -> VersionControl input output
|
||||
withBottomLayer { current, version } =
|
||||
VersionControl
|
||||
{ latestVersion = current
|
||||
, order = List.singleton version
|
||||
, versions = Dict.singleton version current
|
||||
}
|
|
@ -0,0 +1,196 @@
|
|||
module Internal.Values.Context exposing
|
||||
( Context, init, encode, decoder
|
||||
, APIContext, apiFormat
|
||||
, setAccessToken, getAccessToken
|
||||
, setBaseUrl, getBaseUrl
|
||||
, setTransaction, getTransaction
|
||||
, setVersions, getVersions
|
||||
)
|
||||
|
||||
{-| The Context is the set of variables that the user (mostly) cannot control.
|
||||
The Context contains tokens, values and other bits that the Vault receives from
|
||||
the Matrix API.
|
||||
|
||||
|
||||
## Context
|
||||
|
||||
@docs Context, init, encode, decoder
|
||||
|
||||
|
||||
## APIContext
|
||||
|
||||
Once the API starts needing information, that's when we use the APIContext type
|
||||
to build the right environment for the API communication to work with.
|
||||
|
||||
@docs APIContext, apiFormat
|
||||
|
||||
Once the APIContext is ready, there's helper functions for each piece of
|
||||
information that can be inserted.
|
||||
|
||||
|
||||
### Access token
|
||||
|
||||
@docs setAccessToken, getAccessToken
|
||||
|
||||
|
||||
### Base URL
|
||||
|
||||
@docs setBaseUrl, getBaseUrl
|
||||
|
||||
|
||||
### Transaction id
|
||||
|
||||
@docs setTransaction, getTransaction
|
||||
|
||||
|
||||
### Versions
|
||||
|
||||
@docs setVersions, getVersions
|
||||
|
||||
-}
|
||||
|
||||
import Internal.Config.Leaks as L
|
||||
import Internal.Tools.Decode as D
|
||||
import Internal.Tools.Encode as E
|
||||
import Json.Decode as D
|
||||
import Json.Encode as E
|
||||
|
||||
|
||||
{-| The Context type stores all the information in the Vault. This data type is
|
||||
static and hence can be passed on easily.
|
||||
-}
|
||||
type alias Context =
|
||||
{ accessToken : Maybe String
|
||||
, baseUrl : Maybe String
|
||||
, password : Maybe String
|
||||
, refreshToken : Maybe String
|
||||
, username : Maybe String
|
||||
, transaction : Maybe String
|
||||
, versions : Maybe (List String)
|
||||
}
|
||||
|
||||
|
||||
{-| The APIContext is a separate type that uses a phantom type to trick the
|
||||
compiler into requiring values to be present. This data type is used to gather
|
||||
the right variables (like an access token) before accessing the Matrix API.
|
||||
-}
|
||||
type APIContext ph
|
||||
= APIContext
|
||||
{ accessToken : String
|
||||
, baseUrl : String
|
||||
, context : Context
|
||||
, transaction : String
|
||||
, versions : List String
|
||||
}
|
||||
|
||||
|
||||
{-| Create an unformatted APIContext type.
|
||||
-}
|
||||
apiFormat : Context -> APIContext {}
|
||||
apiFormat context =
|
||||
APIContext
|
||||
{ accessToken = context.accessToken |> Maybe.withDefault L.accessToken
|
||||
, baseUrl = context.baseUrl |> Maybe.withDefault L.baseUrl
|
||||
, context = context
|
||||
, transaction = context.transaction |> Maybe.withDefault L.transaction
|
||||
, versions = context.versions |> Maybe.withDefault L.versions
|
||||
}
|
||||
|
||||
|
||||
{-| Decode a Context type from a JSON value.
|
||||
-}
|
||||
decoder : D.Decoder Context
|
||||
decoder =
|
||||
D.map7 Context
|
||||
(D.opField "accessToken" D.string)
|
||||
(D.opField "baseUrl" D.string)
|
||||
(D.opField "password" D.string)
|
||||
(D.opField "refreshToken" D.string)
|
||||
(D.opField "username" D.string)
|
||||
(D.opField "transaction" D.string)
|
||||
(D.opField "versions" (D.list D.string))
|
||||
|
||||
|
||||
{-| Encode a Context type into a JSON value.
|
||||
-}
|
||||
encode : Context -> E.Value
|
||||
encode context =
|
||||
E.maybeObject
|
||||
[ ( "accessToken", Maybe.map E.string context.accessToken )
|
||||
, ( "baseUrl", Maybe.map E.string context.baseUrl )
|
||||
, ( "password", Maybe.map E.string context.password )
|
||||
, ( "refreshToken", Maybe.map E.string context.refreshToken )
|
||||
, ( "username", Maybe.map E.string context.username )
|
||||
, ( "transaction", Maybe.map E.string context.transaction )
|
||||
, ( "versions", Maybe.map (E.list E.string) context.versions )
|
||||
]
|
||||
|
||||
|
||||
{-| A basic, untouched version of the Context, containing no information.
|
||||
-}
|
||||
init : Context
|
||||
init =
|
||||
{ accessToken = Nothing
|
||||
, baseUrl = Nothing
|
||||
, refreshToken = Nothing
|
||||
, password = Nothing
|
||||
, username = Nothing
|
||||
, transaction = Nothing
|
||||
, versions = Nothing
|
||||
}
|
||||
|
||||
|
||||
{-| Get an inserted access token.
|
||||
-}
|
||||
getAccessToken : APIContext { a | accessToken : () } -> String
|
||||
getAccessToken (APIContext c) =
|
||||
c.accessToken
|
||||
|
||||
|
||||
{-| Insert an access token into the APIContext.
|
||||
-}
|
||||
setAccessToken : String -> APIContext a -> APIContext { a | accessToken : () }
|
||||
setAccessToken value (APIContext c) =
|
||||
APIContext { c | accessToken = value }
|
||||
|
||||
|
||||
{-| Get an inserted base URL.
|
||||
-}
|
||||
getBaseUrl : APIContext { a | baseUrl : () } -> String
|
||||
getBaseUrl (APIContext c) =
|
||||
c.baseUrl
|
||||
|
||||
|
||||
{-| Insert a base URL into the APIContext.
|
||||
-}
|
||||
setBaseUrl : String -> APIContext a -> APIContext { a | baseUrl : () }
|
||||
setBaseUrl value (APIContext c) =
|
||||
APIContext { c | baseUrl = value }
|
||||
|
||||
|
||||
{-| Get an inserted transaction id.
|
||||
-}
|
||||
getTransaction : APIContext { a | transaction : () } -> String
|
||||
getTransaction (APIContext c) =
|
||||
c.transaction
|
||||
|
||||
|
||||
{-| Insert a transaction id into the APIContext.
|
||||
-}
|
||||
setTransaction : String -> APIContext a -> APIContext { a | transaction : () }
|
||||
setTransaction value (APIContext c) =
|
||||
APIContext { c | transaction = value }
|
||||
|
||||
|
||||
{-| Get an inserted versions list.
|
||||
-}
|
||||
getVersions : APIContext { a | versions : () } -> List String
|
||||
getVersions (APIContext c) =
|
||||
c.versions
|
||||
|
||||
|
||||
{-| Insert a versions list into the APIContext.
|
||||
-}
|
||||
setVersions : List String -> APIContext a -> APIContext { a | versions : () }
|
||||
setVersions value (APIContext c) =
|
||||
APIContext { c | versions = value }
|
|
@ -0,0 +1,301 @@
|
|||
module Internal.Values.Envelope exposing
|
||||
( Envelope, init
|
||||
, map, mapMaybe, mapList
|
||||
, Settings, mapSettings, extractSettings
|
||||
, mapContext
|
||||
, getContent, extract
|
||||
, encode, decoder
|
||||
)
|
||||
|
||||
{-| The Envelope module wraps existing data types with lots of values and
|
||||
settings that can be adjusted manually.
|
||||
|
||||
|
||||
## Create
|
||||
|
||||
@docs Envelope, init
|
||||
|
||||
|
||||
## Manipulate
|
||||
|
||||
@docs map, mapMaybe, mapList
|
||||
|
||||
|
||||
## Settings
|
||||
|
||||
@docs Settings, mapSettings, extractSettings
|
||||
|
||||
|
||||
## Context
|
||||
|
||||
@docs mapContext
|
||||
|
||||
|
||||
## Extract
|
||||
|
||||
@docs getContent, extract
|
||||
|
||||
|
||||
## JSON coders
|
||||
|
||||
@docs encode, decoder
|
||||
|
||||
-}
|
||||
|
||||
import Internal.Config.Default as Default
|
||||
import Internal.Tools.Decode as D
|
||||
import Internal.Tools.Encode as E
|
||||
import Internal.Values.Context as Context exposing (Context)
|
||||
import Json.Decode as D
|
||||
import Json.Encode as E
|
||||
|
||||
|
||||
{-| There are lots of different data types in the Elm SDK, and many of them
|
||||
need the same values. The Envelope type wraps settings, tokens and values around
|
||||
each data type so they can all enjoy those values without needing to explicitly
|
||||
define them in their type.
|
||||
-}
|
||||
type Envelope a
|
||||
= Envelope
|
||||
{ content : a
|
||||
, context : Context
|
||||
, settings : Settings
|
||||
}
|
||||
|
||||
|
||||
{-| Custom settings that can be manipulated by the user. These serve as a
|
||||
configuration for how the Elm SDK should behave.
|
||||
|
||||
Custom settings are always part of the Envelope, allowing all functions to
|
||||
behave under the user's preferred settings.
|
||||
|
||||
-}
|
||||
type alias Settings =
|
||||
{ currentVersion : String
|
||||
, deviceName : String
|
||||
, syncTime : Int
|
||||
}
|
||||
|
||||
|
||||
{-| Decode an enveloped type from a JSON value. The decoder also imports any
|
||||
potential tokens, values and settings included in the JSON.
|
||||
-}
|
||||
decoder : D.Decoder a -> D.Decoder (Envelope a)
|
||||
decoder xDecoder =
|
||||
D.map3 (\a b c -> Envelope { content = a, context = b, settings = c })
|
||||
(D.field "content" xDecoder)
|
||||
(D.field "context" Context.decoder)
|
||||
(D.field "settings" decoderSettings)
|
||||
|
||||
|
||||
{-| Decode settings from a JSON value.
|
||||
-}
|
||||
decoderSettings : D.Decoder Settings
|
||||
decoderSettings =
|
||||
D.map3 Settings
|
||||
(D.opFieldWithDefault "currentVersion" Default.currentVersion D.string)
|
||||
(D.opFieldWithDefault "deviceName" Default.deviceName D.string)
|
||||
(D.opFieldWithDefault "syncTime" Default.syncTime D.int)
|
||||
|
||||
|
||||
{-| Encode an enveloped type into a JSON value. The function encodes all
|
||||
non-standard settings, tokens and values.
|
||||
-}
|
||||
encode : (a -> E.Value) -> Envelope a -> E.Value
|
||||
encode encodeX (Envelope data) =
|
||||
E.object
|
||||
[ ( "content", encodeX data.content )
|
||||
, ( "context", Context.encode data.context )
|
||||
, ( "settings", encodeSettings data.settings )
|
||||
, ( "version", E.string Default.currentVersion )
|
||||
]
|
||||
|
||||
|
||||
{-| Encode the settings into a JSON value.
|
||||
-}
|
||||
encodeSettings : Settings -> E.Value
|
||||
encodeSettings settings =
|
||||
let
|
||||
differentFrom : b -> b -> Maybe b
|
||||
differentFrom defaultValue currentValue =
|
||||
if currentValue == defaultValue then
|
||||
Nothing
|
||||
|
||||
else
|
||||
Just currentValue
|
||||
in
|
||||
E.maybeObject
|
||||
[ ( "currentVersion"
|
||||
, settings.currentVersion
|
||||
|> differentFrom Default.currentVersion
|
||||
|> Maybe.map E.string
|
||||
)
|
||||
, ( "deviceName"
|
||||
, settings.deviceName
|
||||
|> differentFrom Default.deviceName
|
||||
|> Maybe.map E.string
|
||||
)
|
||||
, ( "syncTime"
|
||||
, settings.syncTime
|
||||
|> differentFrom Default.syncTime
|
||||
|> Maybe.map E.int
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
{-| Map a function, then get its content. This is useful for getting information
|
||||
from a data type inside an Envelope.
|
||||
|
||||
type alias User =
|
||||
{ name : String, age : Int }
|
||||
|
||||
getName : Envelope User -> String
|
||||
getName =
|
||||
Envelope.extract .name
|
||||
|
||||
-}
|
||||
extract : (a -> b) -> Envelope a -> b
|
||||
extract f (Envelope data) =
|
||||
f data.content
|
||||
|
||||
|
||||
{-| Map a function on the settings, effectively getting data that way.
|
||||
|
||||
This can be helpful if you have a UI that displays custom settings to a user.
|
||||
|
||||
-}
|
||||
extractSettings : (Settings -> b) -> Envelope a -> b
|
||||
extractSettings f (Envelope data) =
|
||||
f data.settings
|
||||
|
||||
|
||||
{-| Get the original item that is stored inside an Envelope.
|
||||
|
||||
Make sure that you're only using this if you're interested in the actual value!
|
||||
If you'd like to get the content, run a function on it, and put it back in an
|
||||
Envelope, consider using [map](#map) instead.
|
||||
|
||||
-}
|
||||
getContent : Envelope a -> a
|
||||
getContent =
|
||||
extract identity
|
||||
|
||||
|
||||
{-| Create a new enveloped data type. All settings are set to default values
|
||||
from the [Internal.Config.Default](Internal-Config-Default) module.
|
||||
-}
|
||||
init : a -> Envelope a
|
||||
init x =
|
||||
Envelope
|
||||
{ content = x
|
||||
, context = Context.init
|
||||
, settings =
|
||||
{ currentVersion = Default.currentVersion
|
||||
, deviceName = Default.deviceName
|
||||
, syncTime = Default.syncTime
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
{-| Map a function on the content of the Envelope.
|
||||
|
||||
type alias User =
|
||||
{ name : String, age : Int }
|
||||
|
||||
getName : Envelope User -> Envelope String
|
||||
getName =
|
||||
Envelope.map .name
|
||||
|
||||
-}
|
||||
map : (a -> b) -> Envelope a -> Envelope b
|
||||
map f (Envelope data) =
|
||||
Envelope
|
||||
{ content = f data.content
|
||||
, context = data.context
|
||||
, settings = data.settings
|
||||
}
|
||||
|
||||
|
||||
{-| Update the Context in the Envelope.
|
||||
-}
|
||||
mapContext : (Context -> Context) -> Envelope a -> Envelope a
|
||||
mapContext f (Envelope data) =
|
||||
Envelope
|
||||
{ content = data.content
|
||||
, context = f data.context
|
||||
, settings = data.settings
|
||||
}
|
||||
|
||||
|
||||
{-| Map the contents of a function, where the result is wrapped in a `List`
|
||||
type. This can be useful when you are mapping to a list of individual values
|
||||
that you would all like to see enveloped.
|
||||
|
||||
type alias User =
|
||||
{ name : String, age : Int }
|
||||
|
||||
type alias Company =
|
||||
{ name : String, employees : List User }
|
||||
|
||||
getEmployees : Envelope Company -> List (Envelope User)
|
||||
getEmployees envelope =
|
||||
mapList .employees envelope
|
||||
|
||||
-}
|
||||
mapList : (a -> List b) -> Envelope a -> List (Envelope b)
|
||||
mapList f =
|
||||
map f >> toList
|
||||
|
||||
|
||||
{-| Map the contents of a function, where the result is wrapped in a `Maybe`
|
||||
type. This can be useful when you are not guaranteed to find the value you're
|
||||
looking for.
|
||||
|
||||
type alias User =
|
||||
{ name : String, age : Int }
|
||||
|
||||
type alias UserDatabase =
|
||||
List User
|
||||
|
||||
getFirstUser : Envelope UserDatabase -> Maybe (Envelope User)
|
||||
getFirstUser envelope =
|
||||
mapMaybe List.head envelope
|
||||
|
||||
-}
|
||||
mapMaybe : (a -> Maybe b) -> Envelope a -> Maybe (Envelope b)
|
||||
mapMaybe f =
|
||||
map f >> toMaybe
|
||||
|
||||
|
||||
{-| Update the settings in the Envelope.
|
||||
|
||||
setDeviceName : String -> Envelope a -> Envelope a
|
||||
setDeviceName name envelope =
|
||||
mapSettings
|
||||
(\settings ->
|
||||
{ settings | deviceName = name }
|
||||
)
|
||||
envelope
|
||||
|
||||
-}
|
||||
mapSettings : (Settings -> Settings) -> Envelope a -> Envelope a
|
||||
mapSettings f (Envelope data) =
|
||||
Envelope
|
||||
{ content = data.content
|
||||
, context = data.context
|
||||
, settings = f data.settings
|
||||
}
|
||||
|
||||
|
||||
toList : Envelope (List a) -> List (Envelope a)
|
||||
toList (Envelope data) =
|
||||
List.map
|
||||
(\content -> map (always content) (Envelope data))
|
||||
data.content
|
||||
|
||||
|
||||
toMaybe : Envelope (Maybe a) -> Maybe (Envelope a)
|
||||
toMaybe (Envelope data) =
|
||||
Maybe.map
|
||||
(\content -> map (always content) (Envelope data))
|
||||
data.content
|
|
@ -0,0 +1,15 @@
|
|||
module Internal.Values.Vault exposing (Vault)
|
||||
|
||||
{-| This module hosts the Vault module.
|
||||
|
||||
@docs Vault
|
||||
|
||||
-}
|
||||
|
||||
import Internal.Values.Envelope as Envelope
|
||||
|
||||
|
||||
{-| This is the Vault type.
|
||||
-}
|
||||
type alias Vault =
|
||||
Envelope.Envelope {}
|
|
@ -19,11 +19,13 @@ support a monolithic public registry. (:
|
|||
|
||||
-}
|
||||
|
||||
import Types
|
||||
|
||||
|
||||
{-| The Vault type stores all relevant information about the Matrix API.
|
||||
|
||||
It currently supports no functionality and it will just stay here - for fun.
|
||||
|
||||
-}
|
||||
type Vault
|
||||
= Vault
|
||||
type alias Vault =
|
||||
Types.Vault
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
module Matrix.Settings exposing
|
||||
( getDeviceName, setDeviceName
|
||||
, getSyncTime, setSyncTime
|
||||
)
|
||||
|
||||
{-| The Matrix Vault has lots of configurable variables that you rarely want to
|
||||
interact with. Usually, you configure these variables only when creating a new
|
||||
Vault, or when a user explicitly changes one of their preferred settings.
|
||||
|
||||
|
||||
## Device name
|
||||
|
||||
The default device name that is being communicated with the Matrix API.
|
||||
|
||||
This is mostly useful for users who are logged in with multiple sessions. They
|
||||
will see device names like "Element for Android" or "Element on iOS". For the
|
||||
Elm SDK, they will by default see the Elm SDK with its version included. If you
|
||||
are writing a custom client, however, you are free to change this to something
|
||||
more meaningful to the user.
|
||||
|
||||
@docs getDeviceName, setDeviceName
|
||||
|
||||
|
||||
## Sync time
|
||||
|
||||
Whenever the Matrix API has nothing new to report, the Elm SDK is kept on
|
||||
hold until something new happens. The `syncTime` indicates a timeout to how long
|
||||
the Elm SDK tolerates being held on hold.
|
||||
|
||||
- ↗️ A high value is good because it significantly reduces traffic between the
|
||||
user and the homeserver.
|
||||
- ↘️ A low value is good because it reduces the risk of
|
||||
the connection ending abruptly or unexpectedly.
|
||||
|
||||
Nowadays, most libraries use 30 seconds as the standard, as does the Elm SDK.
|
||||
The value is in miliseconds, so it is set at 30,000.
|
||||
|
||||
@docs getSyncTime, setSyncTime
|
||||
|
||||
-}
|
||||
|
||||
import Internal.Values.Envelope as Envelope
|
||||
import Types exposing (Vault(..))
|
||||
|
||||
|
||||
{-| Determine the device name.
|
||||
-}
|
||||
getDeviceName : Vault -> String
|
||||
getDeviceName (Vault vault) =
|
||||
Envelope.extractSettings .deviceName vault
|
||||
|
||||
|
||||
{-| Override the device name.
|
||||
-}
|
||||
setDeviceName : String -> Vault -> Vault
|
||||
setDeviceName name (Vault vault) =
|
||||
Vault <| Envelope.mapSettings (\s -> { s | deviceName = name }) vault
|
||||
|
||||
|
||||
{-| Determine the sync timeout value.
|
||||
-}
|
||||
getSyncTime : Vault -> Int
|
||||
getSyncTime (Vault vault) =
|
||||
Envelope.extractSettings .syncTime vault
|
||||
|
||||
|
||||
{-| Override the sync timeout value.
|
||||
-}
|
||||
setSyncTime : Int -> Vault -> Vault
|
||||
setSyncTime time (Vault vault) =
|
||||
Vault <| Envelope.mapSettings (\s -> { s | syncTime = time }) vault
|
|
@ -0,0 +1,25 @@
|
|||
module Types exposing (Vault(..))
|
||||
|
||||
{-| The Elm SDK uses a lot of records and values that are easy to manipulate.
|
||||
Yet, the [Elm design guidelines](https://package.elm-lang.org/help/design-guidelines#keep-tags-and-record-constructors-secret)
|
||||
highly recommend using opaque types in order to avoid breaking everyone's code
|
||||
in a future major release.
|
||||
|
||||
This module forms as a protective layer between the internal modules and the
|
||||
exposed modules, hiding all exposed types behind opaque types so the user cannot
|
||||
access their content directly.
|
||||
|
||||
The opaque types are placed in a central module so all exposed modules can
|
||||
safely access all exposed data types without risking to create circular imports.
|
||||
|
||||
@docs Vault
|
||||
|
||||
-}
|
||||
|
||||
import Internal.Values.Vault as Vault
|
||||
|
||||
|
||||
{-| Opaque type for Matrix Vault
|
||||
-}
|
||||
type Vault
|
||||
= Vault Vault.Vault
|
Loading…
Reference in New Issue