Add VersionControl
parent
da81a09eb8
commit
a53fca3326
3
elm.json
3
elm.json
|
@ -7,7 +7,8 @@
|
|||
"exposed-modules": [
|
||||
"Matrix",
|
||||
"Internal.Tools.Hashdict",
|
||||
"Internal.Tools.Iddict"
|
||||
"Internal.Tools.Iddict",
|
||||
"Internal.Tools.VersionControl"
|
||||
],
|
||||
"elm-version": "0.19.0 <= v < 0.20.0",
|
||||
"dependencies": {
|
||||
|
|
|
@ -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
|
||||
}
|
Loading…
Reference in New Issue