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