diff --git a/elm.json b/elm.json index add5509..3c502e6 100644 --- a/elm.json +++ b/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": {} } diff --git a/src/Internal/Config/Default.elm b/src/Internal/Config/Default.elm new file mode 100644 index 0000000..d51eec7 --- /dev/null +++ b/src/Internal/Config/Default.elm @@ -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 diff --git a/src/Internal/Config/Leaks.elm b/src/Internal/Config/Leaks.elm new file mode 100644 index 0000000..d9f2d07 --- /dev/null +++ b/src/Internal/Config/Leaks.elm @@ -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" ] diff --git a/src/Internal/Config/Text.elm b/src/Internal/Config/Text.elm new file mode 100644 index 0000000..0a7bd62 --- /dev/null +++ b/src/Internal/Config/Text.elm @@ -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" diff --git a/src/Internal/Tools/Decode.elm b/src/Internal/Tools/Decode.elm new file mode 100644 index 0000000..c0ea7b2 --- /dev/null +++ b/src/Internal/Tools/Decode.elm @@ -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) diff --git a/src/Internal/Tools/Encode.elm b/src/Internal/Tools/Encode.elm new file mode 100644 index 0000000..53649d9 --- /dev/null +++ b/src/Internal/Tools/Encode.elm @@ -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 diff --git a/src/Internal/Tools/Hashdict.elm b/src/Internal/Tools/Hashdict.elm new file mode 100644 index 0000000..b8be6a9 --- /dev/null +++ b/src/Internal/Tools/Hashdict.elm @@ -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 diff --git a/src/Internal/Tools/Iddict.elm b/src/Internal/Tools/Iddict.elm new file mode 100644 index 0000000..115816f --- /dev/null +++ b/src/Internal/Tools/Iddict.elm @@ -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, ) + + 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 diff --git a/src/Internal/Tools/Timestamp.elm b/src/Internal/Tools/Timestamp.elm new file mode 100644 index 0000000..a0ed35c --- /dev/null +++ b/src/Internal/Tools/Timestamp.elm @@ -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 diff --git a/src/Internal/Tools/VersionControl.elm b/src/Internal/Tools/VersionControl.elm new file mode 100644 index 0000000..71127af --- /dev/null +++ b/src/Internal/Tools/VersionControl.elm @@ -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", ) + -- , ( "v2", ) + -- , ( "v3", ) + -- , ( "v4", ) + -- ] + +-} +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 + } diff --git a/src/Internal/Values/Context.elm b/src/Internal/Values/Context.elm new file mode 100644 index 0000000..88efd33 --- /dev/null +++ b/src/Internal/Values/Context.elm @@ -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 } diff --git a/src/Internal/Values/Envelope.elm b/src/Internal/Values/Envelope.elm new file mode 100644 index 0000000..617973c --- /dev/null +++ b/src/Internal/Values/Envelope.elm @@ -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 diff --git a/src/Internal/Values/Vault.elm b/src/Internal/Values/Vault.elm new file mode 100644 index 0000000..fd6495b --- /dev/null +++ b/src/Internal/Values/Vault.elm @@ -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 {} diff --git a/src/Matrix.elm b/src/Matrix.elm index 5042f58..b68789e 100644 --- a/src/Matrix.elm +++ b/src/Matrix.elm @@ -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 diff --git a/src/Matrix/Settings.elm b/src/Matrix/Settings.elm new file mode 100644 index 0000000..7703695 --- /dev/null +++ b/src/Matrix/Settings.elm @@ -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 diff --git a/src/Types.elm b/src/Types.elm new file mode 100644 index 0000000..022835b --- /dev/null +++ b/src/Types.elm @@ -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