Compare commits

...

2 Commits

Author SHA1 Message Date
Bram van den Heuvel a53fca3326 Add VersionControl 2023-12-15 15:03:01 +01:00
Bram van den Heuvel da81a09eb8 elm-format Hashdict documentation 2023-12-15 15:02:02 +01:00
3 changed files with 376 additions and 11 deletions

View File

@ -7,7 +7,8 @@
"exposed-modules": [
"Matrix",
"Internal.Tools.Hashdict",
"Internal.Tools.Iddict"
"Internal.Tools.Iddict",
"Internal.Tools.VersionControl"
],
"elm-version": "0.19.0 <= v < 0.20.0",
"dependencies": {

View File

@ -56,18 +56,17 @@ example, this can be useful when every user is identifiable by their username:
users : Hashdict User
users =
Hashdict.fromList .name
[ User "Alice" 28 1.65
, User "Bob" 19 1.82
, User "Chuck" 33 1.75
]
Hashdict.fromList .name
[ User "Alice" 28 1.65
, User "Bob" 19 1.82
, User "Chuck" 33 1.75
]
type alias User =
{ name : String
, age : Int
, height : Float
}
{ name : String
, age : Int
, height : Float
}
In the example listed above, the users are stored by their username, which means
that all you need to know is the value "Alice" to retrieve all the information

View File

@ -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
}