Envelope refactor

Effectively, the Envelope type has been moved to the Types module, effectively keeping it separate from other data types.
3-event
Bram 2023-12-24 02:16:52 +01:00
parent 3e54ea9cbe
commit 959642499b
12 changed files with 179 additions and 375 deletions

View File

@ -1,5 +1,5 @@
module Internal.Values.Envelope exposing
( Envelope(..), init
( Envelope, init
, map, mapMaybe, mapList
, Settings, mapSettings, extractSettings
, mapContext
@ -46,6 +46,7 @@ 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 Internal.Values.Settings as Settings
import Json.Decode as D
import Json.Encode as E
@ -55,26 +56,19 @@ 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
}
type alias Envelope a =
{ 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.
{-| Settings value from
[Internal.Values.Settings](Internal-Values-Settings#Settings). Can be used to
manipulate the Matrix Vault.
-}
type alias Settings =
{ currentVersion : String
, deviceName : String
, syncTime : Int
}
Settings.Settings
{-| Decode an enveloped type from a JSON value. The decoder also imports any
@ -82,67 +76,25 @@ 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.map3 Envelope
(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)
(D.field "settings" Settings.decoder)
{-| 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) =
encode encodeX data =
E.object
[ ( "content", encodeX data.content )
, ( "context", Context.encode data.context )
, ( "settings", encodeSettings data.settings )
, ( "settings", Settings.encode 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.
@ -155,7 +107,7 @@ from a data type inside an Envelope.
-}
extract : (a -> b) -> Envelope a -> b
extract f (Envelope data) =
extract f data =
f data.content
@ -165,7 +117,7 @@ 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) =
extractSettings f data =
f data.settings
@ -186,15 +138,10 @@ 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
}
}
{ content = x
, context = Context.init
, settings = Settings.init
}
{-| Map a function on the content of the Envelope.
@ -208,23 +155,18 @@ init x =
-}
map : (a -> b) -> Envelope a -> Envelope b
map f (Envelope data) =
Envelope
{ content = f data.content
, context = data.context
, settings = data.settings
}
map f data =
{ 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
}
mapContext f data =
{ data | context = f data.context }
{-| Map the contents of a function, where the result is wrapped in a `List`
@ -279,23 +221,19 @@ mapMaybe f =
-}
mapSettings : (Settings -> Settings) -> Envelope a -> Envelope a
mapSettings f (Envelope data) =
Envelope
{ content = data.content
, context = data.context
, settings = f data.settings
}
mapSettings f data =
{ data | settings = f data.settings }
toList : Envelope (List a) -> List (Envelope a)
toList (Envelope data) =
toList data =
List.map
(\content -> map (always content) (Envelope data))
(\content -> map (always content) data)
data.content
toMaybe : Envelope (Maybe a) -> Maybe (Envelope a)
toMaybe (Envelope data) =
toMaybe data =
Maybe.map
(\content -> map (always content) (Envelope data))
(\content -> map (always content) data)
data.content

View File

@ -1,9 +1,7 @@
module Internal.Values.Event exposing
( Event
, content, eventId, eventType, originServerTs, roomId, sender, stateKey
, UnsignedData(..), age, prevContent, redactedBecause, transactionId
, encode, decoder
, IEvent
)
{-|
@ -17,11 +15,6 @@ of a room.
@docs Event
## Get information
@docs content, eventId, eventType, originServerTs, roomId, sender, stateKey
## Unsigned data
@docs UnsignedData, age, prevContent, redactedBecause, transactionId
@ -37,7 +30,6 @@ import Internal.Config.Default as Default
import Internal.Tools.Decode as D
import Internal.Tools.Encode as E
import Internal.Tools.Timestamp as Timestamp exposing (Timestamp)
import Internal.Values.Envelope as Envelope
import Json.Decode as D
import Json.Encode as E
@ -45,10 +37,6 @@ import Json.Encode as E
{-| The Event type occurs everywhere on a user's timeline.
-}
type alias Event =
Envelope.Envelope IEvent
type alias IEvent =
{ content : E.Value
, eventId : String
, originServerTs : Timestamp
@ -67,7 +55,7 @@ type UnsignedData
= UnsignedData
{ age : Maybe Int
, prevContent : Maybe E.Value
, redactedBecause : Maybe IEvent
, redactedBecause : Maybe Event
, transactionId : Maybe String
}
@ -75,33 +63,13 @@ type UnsignedData
{-| Get the event's age, if at all provided by the homeserver.
-}
age : Event -> Maybe Int
age envelope =
Envelope.extract
(\event ->
Maybe.andThen
(\(UnsignedData data) -> data.age)
event.unsigned
)
envelope
age event =
Maybe.andThen (\(UnsignedData data) -> data.age) event.unsigned
{-| Determine the body of this event, as created by the user that sent it.
-}
content : Event -> E.Value
content =
Envelope.extract .content
{-| Decode an Event from a JSON value.
-}
decoder : D.Decoder Event
decoder =
Envelope.decoder decoderInternal
decoderInternal : D.Decoder IEvent
decoderInternal =
D.map8 IEvent
D.map8 Event
(D.field "content" D.value)
(D.field "eventId" D.string)
(D.field "originServerTs" Timestamp.decoder)
@ -119,19 +87,14 @@ decoderUnsignedData =
D.map4 (\a b c d -> UnsignedData { age = a, prevContent = b, redactedBecause = c, transactionId = d })
(D.opField "age" D.int)
(D.opField "prevContent" D.value)
(D.opField "redactedBecause" (D.lazy (\_ -> decoderInternal)))
(D.opField "redactedBecause" (D.lazy (\_ -> decoder)))
(D.opField "transactionId" D.string)
{-| Encode an Event into a JSON value.
-}
encode : Event -> E.Value
encode =
Envelope.encode encodeInternal
encodeInternal : IEvent -> E.Value
encodeInternal event =
encode event =
E.maybeObject
[ ( "content", Just event.content )
, ( "eventId", Just <| E.string event.eventId )
@ -152,113 +115,31 @@ encodeUnsignedData (UnsignedData data) =
E.maybeObject
[ ( "age", Maybe.map E.int data.age )
, ( "prevContent", data.prevContent )
, ( "redactedBecause", Maybe.map encodeInternal data.redactedBecause )
, ( "redactedBecause", Maybe.map encode data.redactedBecause )
, ( "transactionId", Maybe.map E.string data.transactionId )
]
{-| Determine the globally unique identifier for an event.
-}
eventId : Event -> String
eventId =
Envelope.extract .eventId
{-| To give a hint what the event's [content](#content) might look like, users
can use this eventType value to hint at how the JSON might be decoded.
Standard examples of event types are `m.room.message`, `m.room.member` and
`me.noordstar.game.chess.move`.
-}
eventType : Event -> String
eventType =
Envelope.extract .eventType
{-| Determine the timestamp of at what time the event was originally received by
the original homeserver.
Generally, this timestamp offers a relatively accurate indicator of when a
message was sent. However, this number isn't completely reliable! The timestamp
can be far in the past due to long network lag, and a (malicious) homeserver can
spoof this number to make it seem like something was sent ridiculously far in
the past - or even in the future.
-}
originServerTs : Event -> Timestamp
originServerTs =
Envelope.extract .originServerTs
{-| Determine the previous `content` value for this event. This field is only a
`Just value` if the event is a state event, and the Matrix Vault has permission
to see the previous content.
-}
prevContent : Event -> Maybe E.Value
prevContent envelope =
Envelope.extract
(\event ->
Maybe.andThen
(\(UnsignedData data) -> data.prevContent)
event.unsigned
)
envelope
prevContent event =
Maybe.andThen (\(UnsignedData data) -> data.prevContent) event.unsigned
{-| If the event has been redacted, the homeserver can display the event that
redacted it here.
-}
redactedBecause : Event -> Maybe Event
redactedBecause =
Envelope.mapMaybe
(\event ->
Maybe.andThen
(\(UnsignedData data) -> data.redactedBecause)
event.unsigned
)
{-| Unique id assigned to the Matrix room. You can use this room id to reference
or look up rooms.
-}
roomId : Event -> String
roomId =
Envelope.extract .roomId
{-| Determine the fully-qualified ID of the user who sent an event.
-}
sender : Event -> String
sender =
Envelope.extract .sender
{-| Determine an event's state key.
It is present if, and only if, the event is a _state_ event. The key makes the
piece of state unique in the room. Note that it is often `Just ""`. If it is not
present, its value is `Nothing`.
State keys starting with an `@` are reserved for referencing user IDs, such as
room members. With the exception of a few events, state events set with a given
user'd ID as the state key can only be set by that user.
-}
stateKey : Event -> Maybe String
stateKey =
Envelope.extract .stateKey
redactedBecause event =
Maybe.andThen (\(UnsignedData data) -> data.redactedBecause) event.unsigned
{-| If the user has sent this event to the homeserver, then the homeserver might
display the original transaction id used for the event.
-}
transactionId : Event -> Maybe String
transactionId envelope =
Envelope.extract
(\event ->
Maybe.andThen
(\(UnsignedData data) -> data.transactionId)
event.unsigned
)
envelope
transactionId event =
Maybe.andThen (\(UnsignedData data) -> data.transactionId) event.unsigned

View File

@ -11,5 +11,4 @@ import Internal.Values.Envelope as Envelope
{-| This is the Vault type.
-}
type alias Vault =
Envelope.Envelope {}
type alias Vault = ()

View File

@ -32,6 +32,7 @@ information isn't always applicable, it doesn't always exist.
-}
import Internal.Values.Envelope as Envelope
import Internal.Values.Event as Internal
import Json.Encode
import Time
@ -56,14 +57,14 @@ type alias Event =
-}
content : Event -> Json.Encode.Value
content (Event event) =
Internal.content event
Envelope.extract .content event
{-| Determine the globally unique identifier for an event.
-}
eventId : Event -> String
eventId (Event event) =
Internal.eventId event
Envelope.extract .eventId event
{-| To give a hint what the event's [content](#content) might look like, users
@ -75,7 +76,7 @@ Standard examples of event types are `m.room.message`, `m.room.member` and
-}
eventType : Event -> String
eventType (Event event) =
Internal.eventType event
Envelope.extract .eventType event
{-| Determine the timestamp of at what time the event was originally received by
@ -90,7 +91,7 @@ the past - or even in the future.
-}
originServerTs : Event -> Time.Posix
originServerTs (Event event) =
Internal.originServerTs event
Envelope.extract .originServerTs event
{-| Determine the previous `content` value for this event. This field is only a
@ -99,7 +100,7 @@ to see the previous content.
-}
previousContent : Event -> Maybe Json.Encode.Value
previousContent (Event event) =
Internal.prevContent event
Envelope.extract Internal.prevContent event
{-| If the event has been redacted, the homeserver can display the event that
@ -107,7 +108,7 @@ redacted it here.
-}
redactedBecause : Event -> Maybe Event
redactedBecause (Event event) =
Internal.redactedBecause event
Envelope.mapMaybe Internal.redactedBecause event
|> Maybe.map Event
@ -116,14 +117,14 @@ or look up rooms.
-}
roomId : Event -> String
roomId (Event event) =
Internal.roomId event
Envelope.extract .roomId event
{-| Determine the fully-qualified ID of the user who sent an event.
-}
sender : Event -> String
sender (Event event) =
Internal.sender event
Envelope.extract .sender event
{-| Determine an event's state key.
@ -139,4 +140,4 @@ user'd ID as the state key can only be set by that user.
-}
stateKey : Event -> Maybe String
stateKey (Event event) =
Internal.stateKey event
Envelope.extract .stateKey event

View File

@ -16,6 +16,7 @@ safely access all exposed data types without risking to create circular imports.
-}
import Internal.Values.Envelope as Envelope
import Internal.Values.Event as Event
import Internal.Values.Vault as Vault
@ -23,10 +24,10 @@ import Internal.Values.Vault as Vault
{-| Opaque type for Matrix Event
-}
type Event
= Event Event.Event
= Event (Envelope.Envelope Event.Event)
{-| Opaque type for Matrix Vault
-}
type Vault
= Vault Vault.Vault
= Vault (Envelope.Envelope Vault.Vault)

View File

@ -1,42 +0,0 @@
module Envelope exposing (..)
import Context as TestContext
import Fuzz exposing (Fuzzer)
import Internal.Config.Default as Default
import Internal.Values.Envelope exposing (Envelope(..), Settings)
import Test exposing (..)
fuzzer : Fuzzer a -> Fuzzer (Envelope a)
fuzzer fuzz =
Fuzz.map3
(\content context settings ->
Envelope
{ content = content
, context = context
, settings = settings
}
)
fuzz
TestContext.fuzzer
fuzzerSettings
fuzzerSettings : Fuzzer Settings
fuzzerSettings =
Fuzz.map3 Settings
(Fuzz.oneOf
[ Fuzz.constant Default.currentVersion
, Fuzz.string
]
)
(Fuzz.oneOf
[ Fuzz.constant Default.deviceName
, Fuzz.string
]
)
(Fuzz.oneOf
[ Fuzz.constant Default.syncTime
, Fuzz.int
]
)

View File

@ -1,89 +0,0 @@
module Event exposing (..)
import Envelope as TestEnvelope
import Expect
import Fuzz exposing (Fuzzer)
import Iddict as TestIddict
import Internal.Tools.Iddict as Iddict
import Internal.Tools.Timestamp as Timestamp
import Internal.Values.Envelope as Envelope
import Internal.Values.Event as Event
import Json.Decode as D
import Json.Encode as E
import Test exposing (..)
import Timestamp as TestTimestamp
{-| Example values that can be used for arbitrary JSON values
-}
valueFuzzer : Fuzzer E.Value
valueFuzzer =
Fuzz.oneOf
[ Fuzz.map (Iddict.encode E.int) (TestIddict.fuzzer Fuzz.int)
, Fuzz.map Timestamp.encode TestTimestamp.fuzzer
, Fuzz.map E.int Fuzz.int
, Fuzz.map E.string Fuzz.string
, Fuzz.map (E.list E.int) (Fuzz.list Fuzz.int)
, Fuzz.map (E.list E.string) (Fuzz.list Fuzz.string)
, Fuzz.map Event.encode (Fuzz.lazy (\_ -> TestEnvelope.fuzzer fuzzer))
]
fuzzer : Fuzzer Event.IEvent
fuzzer =
Fuzz.map8
(\c ei et o r se sk u ->
{ content = c
, eventId = ei
, eventType = et
, originServerTs = o
, roomId = r
, sender = se
, stateKey = sk
, unsigned = u
}
)
valueFuzzer
Fuzz.string
Fuzz.string
TestTimestamp.fuzzer
Fuzz.string
Fuzz.string
(Fuzz.maybe Fuzz.string)
(Fuzz.maybe unsignedDataFuzzer)
fuzzerFull : Fuzzer Event.Event
fuzzerFull =
TestEnvelope.fuzzer fuzzer
unsignedDataFuzzer : Fuzzer Event.UnsignedData
unsignedDataFuzzer =
Fuzz.map4
(\age prev redact trans ->
Event.UnsignedData
{ age = age
, prevContent = prev
, redactedBecause = redact
, transactionId = trans
}
)
(Fuzz.maybe Fuzz.int)
(Fuzz.maybe valueFuzzer)
(Fuzz.maybe <| Fuzz.lazy (\_ -> fuzzer))
(Fuzz.maybe Fuzz.string)
json : Test
json =
describe "JSON tests"
[ fuzz fuzzerFull
"JSON encode + JSON decode"
(\event ->
event
|> Event.encode
|> D.decodeValue Event.decoder
|> Expect.equal (Ok event)
)
]

View File

@ -1,4 +1,4 @@
module Timestamp exposing (..)
module Test.Tools.Timestamp exposing (..)
import Fuzz exposing (Fuzzer)
import Internal.Tools.Timestamp exposing (Timestamp)

View File

@ -1,4 +1,4 @@
module Context exposing (..)
module Test.Values.Context exposing (..)
import Expect
import Fuzz exposing (Fuzzer)

View File

@ -0,0 +1,65 @@
module Test.Values.Envelope exposing (..)
import Expect
import Fuzz exposing (Fuzzer)
import Internal.Config.Default as Default
import Internal.Values.Envelope as Envelope exposing (Envelope)
import Json.Decode as D
import Json.Encode as E
import Test exposing (..)
import Test.Values.Context as TestContext
import Test.Values.Settings as TestSettings
fuzzer : Fuzzer a -> Fuzzer (Envelope a)
fuzzer fuz =
Fuzz.map3 Envelope
fuz
TestContext.fuzzer
TestSettings.fuzzer
suite : Test
suite =
describe "Envelope value"
[ describe "init"
[ describe "Default settings"
[ fuzz Fuzz.string
"currentVersion"
(\s ->
s
|> Envelope.init
|> Envelope.extractSettings .currentVersion
|> Expect.equal Default.currentVersion
)
, fuzz Fuzz.string
"deviceName"
(\s ->
s
|> Envelope.init
|> Envelope.extractSettings .deviceName
|> Expect.equal Default.deviceName
)
, fuzz Fuzz.string
"syncTime"
(\s ->
s
|> Envelope.init
|> Envelope.extractSettings .syncTime
|> Expect.equal Default.syncTime
)
]
]
, describe "JSON"
[ fuzz2 (fuzzer Fuzz.string)
Fuzz.int
"JSON encode -> JSON decode"
(\envelope indent ->
envelope
|> Envelope.encode E.string
|> E.encode indent
|> D.decodeString (Envelope.decoder D.string)
|> Expect.equal (Ok envelope)
)
]
]

View File

@ -0,0 +1,50 @@
module Test.Values.Event exposing (..)
import Fuzz exposing (Fuzzer)
import Internal.Values.Event as Event exposing (Event)
import Json.Encode as E
import Test exposing (..)
import Test.Tools.Timestamp as TestTimestamp
fuzzer : Fuzzer Event
fuzzer =
Fuzz.map8 Event
valueFuzzer
Fuzz.string
TestTimestamp.fuzzer
Fuzz.string
Fuzz.string
(Fuzz.maybe Fuzz.string)
Fuzz.string
(Fuzz.maybe unsignedDataFuzzer)
unsignedDataFuzzer : Fuzzer Event.UnsignedData
unsignedDataFuzzer =
Fuzz.map4
(\age prev redact trans ->
Event.UnsignedData
{ age = age
, prevContent = prev
, redactedBecause = redact
, transactionId = trans
}
)
(Fuzz.maybe Fuzz.int)
(Fuzz.maybe valueFuzzer)
(Fuzz.maybe <| Fuzz.lazy (\_ -> fuzzer))
(Fuzz.maybe Fuzz.string)
{-| Example values that can be used for arbitrary JSON values
-}
valueFuzzer : Fuzzer E.Value
valueFuzzer =
Fuzz.oneOf
[ Fuzz.map E.int Fuzz.int
, Fuzz.map E.string Fuzz.string
, Fuzz.map (E.list E.int) (Fuzz.list Fuzz.int)
, Fuzz.map (E.list E.string) (Fuzz.list Fuzz.string)
, Fuzz.map Event.encode (Fuzz.lazy (\_ -> fuzzer))
]

View File

@ -12,7 +12,7 @@ import Types
fuzzer : Fuzzer Matrix.Vault
fuzzer =
Fuzz.constant <| Types.Vault <| Envelope.init {}
Fuzz.constant <| Types.Vault <| Envelope.init ()
settings : Test