commit
2e6d07bc42
23
elm.json
23
elm.json
|
@ -4,10 +4,29 @@
|
||||||
"summary": "Matrix SDK for instant communication. Unstable beta version for testing only.",
|
"summary": "Matrix SDK for instant communication. Unstable beta version for testing only.",
|
||||||
"license": "EUPL-1.1",
|
"license": "EUPL-1.1",
|
||||||
"version": "1.0.0",
|
"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",
|
"elm-version": "0.19.0 <= v < 0.20.0",
|
||||||
"exposed-modules": [ "Matrix" ],
|
|
||||||
"dependencies": {
|
"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": {}
|
"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.
|
{-| The Vault type stores all relevant information about the Matrix API.
|
||||||
|
|
||||||
It currently supports no functionality and it will just stay here - for fun.
|
It currently supports no functionality and it will just stay here - for fun.
|
||||||
|
|
||||||
-}
|
-}
|
||||||
type Vault
|
type alias Vault =
|
||||||
= 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