From c5e1b756a263122ae22a5d3833544c6d2744b262 Mon Sep 17 00:00:00 2001 From: Bram van den Heuvel Date: Thu, 14 Dec 2023 14:23:29 +0100 Subject: [PATCH 01/23] Add id-dict --- elm.json | 8 ++- src/Internal/Tools/Iddict.elm | 113 ++++++++++++++++++++++++++++++++++ 2 files changed, 119 insertions(+), 2 deletions(-) create mode 100644 src/Internal/Tools/Iddict.elm diff --git a/elm.json b/elm.json index add5509..c0d3ecf 100644 --- a/elm.json +++ b/elm.json @@ -4,10 +4,14 @@ "summary": "Matrix SDK for instant communication. Unstable beta version for testing only.", "license": "EUPL-1.1", "version": "1.0.0", + "exposed-modules": [ + "Matrix", + "Internal.Tools.Iddict" + ], "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", + "miniBill/elm-fast-dict": "1.0.0 <= v < 2.0.0" }, "test-dependencies": {} } diff --git a/src/Internal/Tools/Iddict.elm b/src/Internal/Tools/Iddict.elm new file mode 100644 index 0000000..1f95deb --- /dev/null +++ b/src/Internal/Tools/Iddict.elm @@ -0,0 +1,113 @@ +module Internal.Tools.Iddict exposing (Iddict, empty, get, insert, isEmpty, keys, map, member, remove, singleton, size, values) +{-| 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 +-} + +import FastDict as Dict exposing (Dict) + +{-| The Iddict data type. +-} +type Iddict a + = Iddict + { cursor : Int + , dict : Dict Int a + } + +{-| Create an empty id-dict. +-} +empty : Iddict a +empty = + Iddict + { cursor = 0 + , dict = Dict.empty + } + +{-| 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 From 6fa7904b8de942929bde69ae933989823a34cf86 Mon Sep 17 00:00:00 2001 From: Bram van den Heuvel Date: Thu, 14 Dec 2023 15:29:05 +0100 Subject: [PATCH 02/23] Add JSON coders for id-dict --- elm.json | 1 + src/Internal/Tools/Iddict.elm | 53 ++++++++++++++++++++++++++++++++++- 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/elm.json b/elm.json index c0d3ecf..322c34d 100644 --- a/elm.json +++ b/elm.json @@ -11,6 +11,7 @@ "elm-version": "0.19.0 <= v < 0.20.0", "dependencies": { "elm/core": "1.0.0 <= v < 2.0.0", + "elm/json": "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/Tools/Iddict.elm b/src/Internal/Tools/Iddict.elm index 1f95deb..2a6b944 100644 --- a/src/Internal/Tools/Iddict.elm +++ b/src/Internal/Tools/Iddict.elm @@ -1,4 +1,4 @@ -module Internal.Tools.Iddict exposing (Iddict, empty, get, insert, isEmpty, keys, map, member, remove, singleton, size, values) +module Internal.Tools.Iddict exposing (Iddict, decoder, empty, encode, get, insert, isEmpty, keys, map, member, remove, singleton, size, values) {-| 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. @@ -21,9 +21,15 @@ do not need to generate identifiers yourself. ## 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. -} @@ -33,6 +39,38 @@ type Iddict a , 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 @@ -42,6 +80,19 @@ empty = , 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 From 41ede5bbff352c94547d2999c584a9c36362b7de Mon Sep 17 00:00:00 2001 From: Bram van den Heuvel Date: Thu, 14 Dec 2023 18:35:27 +0100 Subject: [PATCH 03/23] Add Hashdict --- elm.json | 1 + src/Internal/Tools/Hashdict.elm | 228 ++++++++++++++++++++++++++++++++ 2 files changed, 229 insertions(+) create mode 100644 src/Internal/Tools/Hashdict.elm diff --git a/elm.json b/elm.json index 322c34d..d1d37a0 100644 --- a/elm.json +++ b/elm.json @@ -6,6 +6,7 @@ "version": "1.0.0", "exposed-modules": [ "Matrix", + "Internal.Tools.Hashdict", "Internal.Tools.Iddict" ], "elm-version": "0.19.0 <= v < 0.20.0", diff --git a/src/Internal/Tools/Hashdict.elm b/src/Internal/Tools/Hashdict.elm new file mode 100644 index 0000000..e9ff0fc --- /dev/null +++ b/src/Internal/Tools/Hashdict.elm @@ -0,0 +1,228 @@ +module Internal.Tools.Hashdict exposing (Hashdict, decoder, empty, encode, fromList, get, insert, isEmpty, keys, member, memberKey, rehash, remove, removeKey, singleton, size, softDecoder, toList, union, values) + +{-| 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 From fcc7699a44ec7038ed151ef725ed30500bb8b3a3 Mon Sep 17 00:00:00 2001 From: Bram van den Heuvel Date: Thu, 14 Dec 2023 20:43:23 +0100 Subject: [PATCH 04/23] elm-format Hashdict & Iddict I should attempt to not forget this! --- src/Internal/Tools/Hashdict.elm | 47 +++++++++++++++++++-- src/Internal/Tools/Iddict.elm | 75 ++++++++++++++++++++++++--------- 2 files changed, 97 insertions(+), 25 deletions(-) diff --git a/src/Internal/Tools/Hashdict.elm b/src/Internal/Tools/Hashdict.elm index e9ff0fc..45c13d1 100644 --- a/src/Internal/Tools/Hashdict.elm +++ b/src/Internal/Tools/Hashdict.elm @@ -1,39 +1,54 @@ -module Internal.Tools.Hashdict exposing (Hashdict, decoder, empty, encode, fromList, get, insert, isEmpty, keys, member, memberKey, rehash, remove, removeKey, singleton, size, softDecoder, toList, union, values) +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: @@ -52,10 +67,12 @@ example, this can be useful when every user is identifiable by their username: , 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 @@ -63,6 +80,7 @@ type Hashdict a , 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. @@ -72,22 +90,25 @@ decoder f xDecoder = D.keyValuePairs xDecoder |> D.andThen (\items -> - if List.all (\(hash, value) -> f value == hash) items then + 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! @@ -99,6 +120,7 @@ encode encodeX (Hashdict h) = |> List.map (Tuple.mapSecond encodeX) |> E.object + {-| Convert an association list into a hashdict. -} fromList : (a -> String) -> List a -> Hashdict a @@ -111,6 +133,7 @@ fromList hash xs = |> 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. @@ -119,6 +142,7 @@ 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. @@ -127,30 +151,35 @@ 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 @@ -164,35 +193,42 @@ rehash f (Hashdict h) = |> 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. @@ -202,13 +238,15 @@ 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 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. -} @@ -221,6 +259,7 @@ union (Hashdict h1) hd2 = , 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 diff --git a/src/Internal/Tools/Iddict.elm b/src/Internal/Tools/Iddict.elm index 2a6b944..115816f 100644 --- a/src/Internal/Tools/Iddict.elm +++ b/src/Internal/Tools/Iddict.elm @@ -1,4 +1,11 @@ -module Internal.Tools.Iddict exposing (Iddict, decoder, empty, encode, get, insert, isEmpty, keys, map, member, remove, singleton, size, values) +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. @@ -6,39 +13,47 @@ 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 + = Iddict { cursor : Int , dict : Dict Int a } + {-| Decode an id-dict from a JSON value. -} decoder : D.Decoder a -> D.Decoder (Iddict a) @@ -50,27 +65,32 @@ decoder xDecoder = dict = pairs |> List.filterMap - (\(k, v) -> + (\( k, v ) -> k - |> String.toInt - |> Maybe.map (\n -> (n, v)) + |> 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 - } + 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 @@ -80,25 +100,28 @@ empty = , 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" + , 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. @@ -107,25 +130,29 @@ key, the function returns both the updated id-dict as the newly generated key. case x of ( _, iddict ) -> get 0 iddict -- Just "hello" + -} -insert : a -> Iddict a -> (Int, Iddict a) +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. -} @@ -133,23 +160,28 @@ 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 + +{-| 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 +singleton : a -> ( Int, Iddict a ) +singleton v = + insert v empty + {-| Determine the number of key-value pairs in the id-dict. -} @@ -157,6 +189,7 @@ 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 From da81a09eb8db40d77db5e8f942b1e33f4a3caef6 Mon Sep 17 00:00:00 2001 From: Bram van den Heuvel Date: Fri, 15 Dec 2023 15:02:02 +0100 Subject: [PATCH 05/23] elm-format Hashdict documentation --- src/Internal/Tools/Hashdict.elm | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/Internal/Tools/Hashdict.elm b/src/Internal/Tools/Hashdict.elm index 45c13d1..b8be6a9 100644 --- a/src/Internal/Tools/Hashdict.elm +++ b/src/Internal/Tools/Hashdict.elm @@ -56,18 +56,17 @@ example, this can be useful when every user is identifiable by their username: users : Hashdict User users = - Hashdict.fromList .name - [ User "Alice" 28 1.65 - , User "Bob" 19 1.82 - , User "Chuck" 33 1.75 - ] + 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 - } - + { 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 From a53fca33266b67a344d9328b8cda2430f9ecd827 Mon Sep 17 00:00:00 2001 From: Bram van den Heuvel Date: Fri, 15 Dec 2023 15:03:01 +0100 Subject: [PATCH 06/23] Add VersionControl --- elm.json | 3 +- src/Internal/Tools/VersionControl.elm | 365 ++++++++++++++++++++++++++ 2 files changed, 367 insertions(+), 1 deletion(-) create mode 100644 src/Internal/Tools/VersionControl.elm diff --git a/elm.json b/elm.json index d1d37a0..869ba7a 100644 --- a/elm.json +++ b/elm.json @@ -7,7 +7,8 @@ "exposed-modules": [ "Matrix", "Internal.Tools.Hashdict", - "Internal.Tools.Iddict" + "Internal.Tools.Iddict", + "Internal.Tools.VersionControl" ], "elm-version": "0.19.0 <= v < 0.20.0", "dependencies": { 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 + } From dd10d43da1d9d9af607b94293796541d7d4b62ab Mon Sep 17 00:00:00 2001 From: Bram Date: Fri, 15 Dec 2023 23:55:03 +0100 Subject: [PATCH 07/23] Add Timestamp data type --- elm.json | 2 ++ src/Internal/Tools/Timestamp.elm | 43 ++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 src/Internal/Tools/Timestamp.elm diff --git a/elm.json b/elm.json index 869ba7a..0b6d564 100644 --- a/elm.json +++ b/elm.json @@ -8,12 +8,14 @@ "Matrix", "Internal.Tools.Hashdict", "Internal.Tools.Iddict", + "Internal.Tools.Timestamp", "Internal.Tools.VersionControl" ], "elm-version": "0.19.0 <= v < 0.20.0", "dependencies": { "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/Tools/Timestamp.elm b/src/Internal/Tools/Timestamp.elm new file mode 100644 index 0000000..8eddec6 --- /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 Timetsamp as delivered by +elm/time. This module offers ways to work with the timestamp in meaningful ways. + + +## Timetstamp + +@docs Timestamp + + +## JSON coders + +@docs encode, decoder + +-} + +import Json.Decode as D +import Json.Encode as E +import Time + + +{-| The Timetstamp 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 From b7c2e28f71f87fb5ce97576a82cd248199fda604 Mon Sep 17 00:00:00 2001 From: Bram Date: Sat, 16 Dec 2023 00:28:14 +0100 Subject: [PATCH 08/23] Add default config values --- elm.json | 1 + src/Internal/Config/Default.elm | 54 +++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 src/Internal/Config/Default.elm diff --git a/elm.json b/elm.json index 0b6d564..d81df1c 100644 --- a/elm.json +++ b/elm.json @@ -6,6 +6,7 @@ "version": "1.0.0", "exposed-modules": [ "Matrix", + "Internal.Config.Default", "Internal.Tools.Hashdict", "Internal.Tools.Iddict", "Internal.Tools.Timestamp", diff --git a/src/Internal/Config/Default.elm b/src/Internal/Config/Default.elm new file mode 100644 index 0000000..d05dd7f --- /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 refuces 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 From 7c7e05d42a9fbf582538cefe16239cfdcd78c2bd Mon Sep 17 00:00:00 2001 From: Bram Date: Sat, 16 Dec 2023 01:25:02 +0100 Subject: [PATCH 09/23] Add standardized text values --- elm.json | 1 + src/Internal/Config/Text.elm | 108 +++++++++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+) create mode 100644 src/Internal/Config/Text.elm diff --git a/elm.json b/elm.json index d81df1c..c41f50f 100644 --- a/elm.json +++ b/elm.json @@ -7,6 +7,7 @@ "exposed-modules": [ "Matrix", "Internal.Config.Default", + "Internal.Config.Text", "Internal.Tools.Hashdict", "Internal.Tools.Iddict", "Internal.Tools.Timestamp", diff --git a/src/Internal/Config/Text.elm b/src/Internal/Config/Text.elm new file mode 100644 index 0000000..f8bc7c2 --- /dev/null +++ b/src/Internal/Config/Text.elm @@ -0,0 +1,108 @@ +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 on 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 + + +## 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" From 1b0be9bffa7424558b05762f19de1c9180fe7d8b Mon Sep 17 00:00:00 2001 From: Bram Date: Sun, 17 Dec 2023 15:18:54 +0100 Subject: [PATCH 10/23] Add Envelope type --- elm.json | 3 +- src/Internal/Values/Envelope.elm | 166 +++++++++++++++++++++++++++++++ 2 files changed, 168 insertions(+), 1 deletion(-) create mode 100644 src/Internal/Values/Envelope.elm diff --git a/elm.json b/elm.json index c41f50f..3e1edc0 100644 --- a/elm.json +++ b/elm.json @@ -11,7 +11,8 @@ "Internal.Tools.Hashdict", "Internal.Tools.Iddict", "Internal.Tools.Timestamp", - "Internal.Tools.VersionControl" + "Internal.Tools.VersionControl", + "Internal.Values.Envelope" ], "elm-version": "0.19.0 <= v < 0.20.0", "dependencies": { diff --git a/src/Internal/Values/Envelope.elm b/src/Internal/Values/Envelope.elm new file mode 100644 index 0000000..bf80445 --- /dev/null +++ b/src/Internal/Values/Envelope.elm @@ -0,0 +1,166 @@ +module Internal.Values.Envelope exposing + ( Envelope, init + , map, mapMaybe + , Settings, mapSettings + , getContent, extract + ) + +{-| 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 + + +## Settings + +@docs Settings, mapSettings + + +## Extract + +@docs getContent, extract + +-} + +import Internal.Config.Default as Default + + +{-| 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 + , 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 + } + + +{-| 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 + + +{-| 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 + , 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 + , settings = data.settings + } + + +{-| 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 + , settings = f data.settings + } + + +{-| 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 + + +toMaybe : Envelope (Maybe a) -> Maybe (Envelope a) +toMaybe (Envelope data) = + Maybe.map + (\content -> map (always content) (Envelope data)) + data.content From 848d83a18e8e3acb1c30808abdc38cb0c1e3068d Mon Sep 17 00:00:00 2001 From: Bram Date: Sun, 17 Dec 2023 21:29:45 +0100 Subject: [PATCH 11/23] Add JSON coder helper files --- elm.json | 2 + src/Internal/Tools/Decode.elm | 155 ++++++++++++++++++++++++++++++++++ src/Internal/Tools/Encode.elm | 52 ++++++++++++ 3 files changed, 209 insertions(+) create mode 100644 src/Internal/Tools/Decode.elm create mode 100644 src/Internal/Tools/Encode.elm diff --git a/elm.json b/elm.json index 3e1edc0..0ab26bd 100644 --- a/elm.json +++ b/elm.json @@ -8,6 +8,8 @@ "Matrix", "Internal.Config.Default", "Internal.Config.Text", + "Internal.Tools.Decode", + "Internal.Tools.Encode", "Internal.Tools.Hashdict", "Internal.Tools.Iddict", "Internal.Tools.Timestamp", 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 From f910c0225b89c9e26b10b408df3cd663b624391b Mon Sep 17 00:00:00 2001 From: Bram Date: Mon, 18 Dec 2023 01:08:40 +0100 Subject: [PATCH 12/23] Add Envelope JSON coders --- src/Internal/Values/Envelope.elm | 74 ++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/src/Internal/Values/Envelope.elm b/src/Internal/Values/Envelope.elm index bf80445..ba60a6b 100644 --- a/src/Internal/Values/Envelope.elm +++ b/src/Internal/Values/Envelope.elm @@ -3,6 +3,7 @@ module Internal.Values.Envelope exposing , map, mapMaybe , Settings, mapSettings , getContent, extract + , encode, decoder ) {-| The Envelope module wraps existing data types with lots of values and @@ -28,9 +29,18 @@ settings that can be adjusted manually. @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 Json.Decode as D +import Json.Encode as E {-| There are lots of different data types in the Elm SDK, and many of them @@ -59,6 +69,70 @@ type alias Settings = } +{-| 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.map2 (\a b -> Envelope { content = a, settings = b }) + (D.field "content" xDecoder) + (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 ) + , ( "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. From 5293eb40032cdd361cf98de5e631070c8ad64ff8 Mon Sep 17 00:00:00 2001 From: Bram Date: Mon, 18 Dec 2023 01:30:15 +0100 Subject: [PATCH 13/23] Add Envelope function to extract settings values --- src/Internal/Values/Envelope.elm | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/Internal/Values/Envelope.elm b/src/Internal/Values/Envelope.elm index ba60a6b..4f9a13b 100644 --- a/src/Internal/Values/Envelope.elm +++ b/src/Internal/Values/Envelope.elm @@ -1,7 +1,7 @@ module Internal.Values.Envelope exposing ( Envelope, init , map, mapMaybe - , Settings, mapSettings + , Settings, mapSettings, extractSettings , getContent, extract , encode, decoder ) @@ -22,7 +22,7 @@ settings that can be adjusted manually. ## Settings -@docs Settings, mapSettings +@docs Settings, mapSettings, extractSettings ## Extract @@ -149,6 +149,16 @@ 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! From 8b7b2aa3125167fb47a65f07b6f3dcfb01253b2e Mon Sep 17 00:00:00 2001 From: Bram Date: Mon, 18 Dec 2023 01:32:21 +0100 Subject: [PATCH 14/23] Add internal Vault type --- elm.json | 4 +++- src/Internal/Values/Vault.elm | 15 +++++++++++++++ src/Types.elm | 25 +++++++++++++++++++++++++ 3 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 src/Internal/Values/Vault.elm create mode 100644 src/Types.elm diff --git a/elm.json b/elm.json index 0ab26bd..d50267e 100644 --- a/elm.json +++ b/elm.json @@ -14,7 +14,9 @@ "Internal.Tools.Iddict", "Internal.Tools.Timestamp", "Internal.Tools.VersionControl", - "Internal.Values.Envelope" + "Internal.Values.Envelope", + "Internal.Values.Vault", + "Types" ], "elm-version": "0.19.0 <= v < 0.20.0", "dependencies": { 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/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 From 29f9482b74899c5a5de35c0dc3da158e3743dca7 Mon Sep 17 00:00:00 2001 From: Bram Date: Mon, 18 Dec 2023 01:32:57 +0100 Subject: [PATCH 15/23] Add public settings interface --- elm.json | 1 + src/Internal/Config/Default.elm | 2 +- src/Matrix/Settings.elm | 71 +++++++++++++++++++++++++++++++++ 3 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 src/Matrix/Settings.elm diff --git a/elm.json b/elm.json index d50267e..befcf14 100644 --- a/elm.json +++ b/elm.json @@ -6,6 +6,7 @@ "version": "1.0.0", "exposed-modules": [ "Matrix", + "Matrix.Settings", "Internal.Config.Default", "Internal.Config.Text", "Internal.Tools.Decode", diff --git a/src/Internal/Config/Default.elm b/src/Internal/Config/Default.elm index d05dd7f..d51eec7 100644 --- a/src/Internal/Config/Default.elm +++ b/src/Internal/Config/Default.elm @@ -42,7 +42,7 @@ 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 refuces the risk of + - ↘️ 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. 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 From 3b927dc46008a062ef9903be8b93225325cd2e69 Mon Sep 17 00:00:00 2001 From: Bram Date: Mon, 18 Dec 2023 02:11:31 +0100 Subject: [PATCH 16/23] Update exposed Vault to match internal Vault --- src/Matrix.elm | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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 From b3479cf2c98d81f69f156b3d8a1568f887ddaf61 Mon Sep 17 00:00:00 2001 From: Bram Date: Mon, 18 Dec 2023 05:51:24 +0100 Subject: [PATCH 17/23] Add leaking values --- elm.json | 1 + src/Internal/Config/Leaks.elm | 39 +++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 src/Internal/Config/Leaks.elm diff --git a/elm.json b/elm.json index befcf14..168fd69 100644 --- a/elm.json +++ b/elm.json @@ -8,6 +8,7 @@ "Matrix", "Matrix.Settings", "Internal.Config.Default", + "Internal.Config.Leaks", "Internal.Config.Text", "Internal.Tools.Decode", "Internal.Tools.Encode", diff --git a/src/Internal/Config/Leaks.elm b/src/Internal/Config/Leaks.elm new file mode 100644 index 0000000..c71b81c --- /dev/null +++ b/src/Internal/Config/Leaks.elm @@ -0,0 +1,39 @@ +module Internal.Config.Leaks exposing (accessToken) + +{-| + + +# 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 + +-} + + +{-| Placeholder access token. +-} +accessToken : String +accessToken = + "elm-sdk-placeholder-access-token-leaks" From 7254fcfaa422182416364520e38d0abee190e399 Mon Sep 17 00:00:00 2001 From: Bram Date: Mon, 18 Dec 2023 14:26:57 +0100 Subject: [PATCH 18/23] Add Context type --- elm.json | 1 + src/Internal/Config/Leaks.elm | 25 +++- src/Internal/Values/Context.elm | 196 ++++++++++++++++++++++++++++++++ 3 files changed, 220 insertions(+), 2 deletions(-) create mode 100644 src/Internal/Values/Context.elm diff --git a/elm.json b/elm.json index 168fd69..3c502e6 100644 --- a/elm.json +++ b/elm.json @@ -16,6 +16,7 @@ "Internal.Tools.Iddict", "Internal.Tools.Timestamp", "Internal.Tools.VersionControl", + "Internal.Values.Context", "Internal.Values.Envelope", "Internal.Values.Vault", "Types" diff --git a/src/Internal/Config/Leaks.elm b/src/Internal/Config/Leaks.elm index c71b81c..d9f2d07 100644 --- a/src/Internal/Config/Leaks.elm +++ b/src/Internal/Config/Leaks.elm @@ -1,4 +1,4 @@ -module Internal.Config.Leaks exposing (accessToken) +module Internal.Config.Leaks exposing (accessToken, baseUrl, transaction, versions) {-| @@ -27,7 +27,7 @@ know 100% sure that the value isn't `Nothing`. Just 5 |> Maybe.withDefault Leaks.number -@docs accessToken +@docs accessToken, baseUrl, transaction, versions -} @@ -37,3 +37,24 @@ know 100% sure that the value isn't `Nothing`. 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/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 } From 87a5919921463b5eaad6ba7ed51895a98bf25fcf Mon Sep 17 00:00:00 2001 From: Bram Date: Mon, 18 Dec 2023 14:27:22 +0100 Subject: [PATCH 19/23] Include Context in Envelope --- src/Internal/Values/Envelope.elm | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/Internal/Values/Envelope.elm b/src/Internal/Values/Envelope.elm index 4f9a13b..08e9973 100644 --- a/src/Internal/Values/Envelope.elm +++ b/src/Internal/Values/Envelope.elm @@ -2,6 +2,7 @@ module Internal.Values.Envelope exposing ( Envelope, init , map, mapMaybe , Settings, mapSettings, extractSettings + , mapContext , getContent, extract , encode, decoder ) @@ -25,6 +26,11 @@ settings that can be adjusted manually. @docs Settings, mapSettings, extractSettings +## Context + +@docs mapContext + + ## Extract @docs getContent, extract @@ -39,6 +45,7 @@ settings that can be adjusted manually. 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 @@ -51,6 +58,7 @@ define them in their type. type Envelope a = Envelope { content : a + , context : Context , settings : Settings } @@ -74,8 +82,9 @@ potential tokens, values and settings included in the JSON. -} decoder : D.Decoder a -> D.Decoder (Envelope a) decoder xDecoder = - D.map2 (\a b -> Envelope { content = a, settings = b }) + 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) @@ -96,6 +105,7 @@ 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 ) ] @@ -178,6 +188,7 @@ init : a -> Envelope a init x = Envelope { content = x + , context = Context.init , settings = { currentVersion = Default.currentVersion , deviceName = Default.deviceName @@ -200,6 +211,18 @@ 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 } @@ -219,6 +242,7 @@ mapSettings : (Settings -> Settings) -> Envelope a -> Envelope a mapSettings f (Envelope data) = Envelope { content = data.content + , context = data.context , settings = f data.settings } From 447a18ab0489fff5c23a355e332280b1d2d0784d Mon Sep 17 00:00:00 2001 From: Bram Date: Mon, 18 Dec 2023 17:10:04 +0100 Subject: [PATCH 20/23] Update Internal.Config.Text docs --- src/Internal/Config/Text.elm | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Internal/Config/Text.elm b/src/Internal/Config/Text.elm index f8bc7c2..d0c87f9 100644 --- a/src/Internal/Config/Text.elm +++ b/src/Internal/Config/Text.elm @@ -7,12 +7,20 @@ module Internal.Config.Text exposing {-| Throughout the Elm SDK, there are lots of pieces of text being used for various purposes. Some of these are: - - To log on what is happening during an API call. + - 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 From 5bd95699d236d3d3279300cd9d6b0efcf0a732eb Mon Sep 17 00:00:00 2001 From: Bram Date: Mon, 18 Dec 2023 17:19:12 +0100 Subject: [PATCH 21/23] Add function for Envelope --- src/Internal/Values/Envelope.elm | 31 +++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/src/Internal/Values/Envelope.elm b/src/Internal/Values/Envelope.elm index 08e9973..6d302d0 100644 --- a/src/Internal/Values/Envelope.elm +++ b/src/Internal/Values/Envelope.elm @@ -1,6 +1,6 @@ module Internal.Values.Envelope exposing ( Envelope, init - , map, mapMaybe + , map, mapMaybe, mapList , Settings, mapSettings, extractSettings , mapContext , getContent, extract @@ -18,7 +18,7 @@ settings that can be adjusted manually. ## Manipulate -@docs map, mapMaybe +@docs map, mapMaybe, mapList ## Settings @@ -247,6 +247,26 @@ mapSettings f (Envelope data) = } +{-| 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. @@ -267,6 +287,13 @@ mapMaybe f = map f >> toMaybe +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 From d9d5760928c9f02de4c5954f3d4b0936b083d99c Mon Sep 17 00:00:00 2001 From: Bram Date: Mon, 18 Dec 2023 17:20:43 +0100 Subject: [PATCH 22/23] Reorder Envelope functions alphabetically --- src/Internal/Values/Envelope.elm | 40 ++++++++++++++++---------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/src/Internal/Values/Envelope.elm b/src/Internal/Values/Envelope.elm index 6d302d0..617973c 100644 --- a/src/Internal/Values/Envelope.elm +++ b/src/Internal/Values/Envelope.elm @@ -227,26 +227,6 @@ mapContext f (Envelope data) = } -{-| 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 - } - - {-| 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. @@ -287,6 +267,26 @@ 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 From 1d3ceb9b2dcba57ac4a1253abcc1157466a6e3b0 Mon Sep 17 00:00:00 2001 From: Bram Date: Mon, 18 Dec 2023 17:30:52 +0100 Subject: [PATCH 23/23] Fix typos --- src/Internal/Config/Text.elm | 4 ++-- src/Internal/Tools/Timestamp.elm | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Internal/Config/Text.elm b/src/Internal/Config/Text.elm index d0c87f9..0a7bd62 100644 --- a/src/Internal/Config/Text.elm +++ b/src/Internal/Config/Text.elm @@ -102,14 +102,14 @@ versionsFailedToDecode = "Matrix API returned an invalid version list" -{-| Logs when the Vault remembers how to communicate with the Matrix homeserver +{-| 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 +{-| Logs when the Matrix API has returned how to best communicate with them. -} versionsReceived : String versionsReceived = diff --git a/src/Internal/Tools/Timestamp.elm b/src/Internal/Tools/Timestamp.elm index 8eddec6..a0ed35c 100644 --- a/src/Internal/Tools/Timestamp.elm +++ b/src/Internal/Tools/Timestamp.elm @@ -3,11 +3,11 @@ module Internal.Tools.Timestamp exposing , encode, decoder ) -{-| The Timestamp module is a simplification of the Timetsamp as delivered by +{-| 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. -## Timetstamp +## Timestamp @docs Timestamp @@ -23,7 +23,7 @@ import Json.Encode as E import Time -{-| The Timetstamp data type representing a moment in time. +{-| The Timestamp data type representing a moment in time. -} type alias Timestamp = Time.Posix