Skip to content

Commit e236c8d

Browse files
authored
Expose Map on TimelineEvent/EventData/EventCodec (#77)
1 parent a3d2345 commit e236c8d

File tree

10 files changed

+78
-59
lines changed

10 files changed

+78
-59
lines changed

CHANGELOG.md

+6-3
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ The `Unreleased` section name is replaced by the expected version of next releas
99
## [Unreleased]
1010

1111
### Added
12+
13+
- `Core.EventData/TimelineEvent/EventCodec.Map`: Exposed building blocks for mapping event envelopes and/or codecs over Body Format types [#77](https://github.com/jet/FsCodec/pull/77)
14+
1215
### Changed
1316
### Removed
1417
### Fixed
@@ -27,8 +30,8 @@ The `Unreleased` section name is replaced by the expected version of next releas
2730
- Updated build and tests to use `net6.0`, all test package dependencies
2831
- Updated `TypeShape` reference to v `10`, triggering min `FSharp.Core` target moving to `4.5.4`
2932
- `SystemTextJson.Codec`: Switched Event body type from `JsonElement` to `ReadOnlyMemory<byte>` [#75](https://github.com/jet/FsCodec/pull/75)
30-
- `NewtonsoftJson.Codec`: Switched Event body type from `byte[]` to `ReadOnlyMemory<byte>` [#75](https://github.com/jet/FsCodec/pull/75)
31-
- `ToByteArrayCodec`: now adapts a `ReadOnlyMemory<byte>` encoder (was from `JsonElement`) (to `byte[]` bodies); Moved from `FsCodec.SystemTextJson` to `FsCodec.Box` [#75](https://github.com/jet/FsCodec/pull/75)
33+
- `NewtonsoftJson.Codec`: Switched Event body type from `byte array` to `ReadOnlyMemory<byte>` [#75](https://github.com/jet/FsCodec/pull/75)
34+
- `ToByteArrayCodec`: now adapts a `ReadOnlyMemory<byte>` encoder (was from `JsonElement`) (to `byte array` bodies); Moved from `FsCodec.SystemTextJson` to `FsCodec.Box` [#75](https://github.com/jet/FsCodec/pull/75)
3235

3336
### Removed
3437

@@ -199,7 +202,7 @@ The `Unreleased` section name is replaced by the expected version of next releas
199202

200203
### Changed
201204

202-
- Generalized `Codec.Create` to no longer presume `Data` and `Metadata` should always be `byte[]` [#24](https://github.com/jet/FsCodec/pull/24)
205+
- Generalized `Codec.Create` to no longer presume `Data` and `Metadata` should always be `byte array` [#24](https://github.com/jet/FsCodec/pull/24)
203206

204207
### Removed
205208

README.md

+4-4
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ The purpose of the `FsCodec` package is to provide a minimal interface on which
3030
- [`FsCodec.IEventData`](https://github.com/jet/FsCodec/blob/master/src/FsCodec/FsCodec.fs#L4) represents a single event and/or related metadata in raw form (i.e. still as a UTF8 string etc, not yet bound to a specific Event Type)
3131
- [`FsCodec.ITimelineEvent`](https://github.com/jet/FsCodec/blob/master/src/FsCodec/FsCodec.fs#L23) represents a single stored event and/or related metadata in raw form (i.e. still as a UTF8 string etc, not yet bound to a specific Event Type). Inherits `IEventData`, adding `Index` and `IsUnfold` in order to represent the position on the timeline that the event logically occupies.
3232
- [`FsCodec.IEventCodec`](https://github.com/jet/FsCodec/blob/master/src/FsCodec/FsCodec.fs#L31) presents `Encode : 'Context option * 'Event -> IEventData` and `TryDecode : ITimelineEvent -> 'Event option` methods that can be used in low level application code to generate `IEventData`s or decode `ITimelineEvent`s based on a contract defined by `'Union`
33-
- [`FsCodec.Codec.Create`](https://github.com/jet/FsCodec/blob/master/src/FsCodec/Codec.fs#L27) implements `IEventCodec` in terms of supplied `encode : 'Event -> string * byte[]` and `tryDecode : string * byte[] -> 'Event option` functions (other overloads are available for advanced cases)
33+
- [`FsCodec.Codec.Create`](https://github.com/jet/FsCodec/blob/master/src/FsCodec/Codec.fs#L27) implements `IEventCodec` in terms of supplied `encode : 'Event -> string * byte array` and `tryDecode : string * byte array -> 'Event option` functions (other overloads are available for advanced cases)
3434
- [`FsCodec.Core.EventData.Create`](https://github.com/jet/FsCodec/blob/master/src/FsCodec/FsCodec.fs#L44) is a low level helper to create an `IEventData` directly for purposes such as tests etc.
3535
- [`FsCodec.Core.TimelineEvent.Create`](https://github.com/jet/FsCodec/blob/master/src/FsCodec/FsCodec.fs#L58) is a low level helper to create an `ITimelineEvent` directly for purposes such as tests etc.
3636

@@ -94,7 +94,7 @@ The respective concrete Codec packages include relevant `Converter`/`JsonConvert
9494
### `Newtonsoft.Json`-specific low level converters
9595

9696
- [`OptionConverter`](https://github.com/jet/FsCodec/blob/master/src/FsCodec.NewtonsoftJson/OptionConverter.fs#L7) represents F#'s `Option<'t>` as a value or `null`; included in the standard `Options.Create` profile.
97-
- [`VerbatimUtf8JsonConverter`](https://github.com/jet/FsCodec/blob/master/src/FsCodec.NewtonsoftJson/VerbatimUtf8JsonConverter.fs#L7) captures/renders known valid UTF8 JSON data into a `byte[]` without decomposing it into an object model (not typically relevant for application level code, used in `Equinox.Cosmos` versions prior to `3.0`).
97+
- [`VerbatimUtf8JsonConverter`](https://github.com/jet/FsCodec/blob/master/src/FsCodec.NewtonsoftJson/VerbatimUtf8JsonConverter.fs#L7) captures/renders known valid UTF8 JSON data into a `byte array` without decomposing it into an object model (not typically relevant for application level code, used in `Equinox.Cosmos` versions prior to `3.0`).
9898

9999
### `System.Text.Json`-specific low level converters
100100

@@ -637,7 +637,7 @@ The following helper (which uses the [`Serilog`](https://github.com/serilog/seri
637637
module EventCodec =
638638
639639
// Uses the supplied codec to decode the supplied event record `x` (iff at LogEventLevel.Debug, detail fails to `log` citing the `stream` and content)
640-
let tryDecode (codec : FsCodec.IEventCodec<_,_,_>) (log : Serilog.ILogger) streamName (x : FsCodec.ITimelineEvent<byte[]>) =
640+
let tryDecode (codec : FsCodec.IEventCodec<_,_,_>) (log : Serilog.ILogger) streamName (x : FsCodec.ITimelineEvent<ReadOnlyMemory<byte>>) =
641641
match codec.TryDecode x with
642642
| None ->
643643
if log.IsEnabled Serilog.Events.LogEventLevel.Debug then
@@ -681,7 +681,7 @@ module Reactions =
681681
682682
type Event = int64 * DateTimeOffset * Events.Event
683683
let codec =
684-
let up (raw : FsCodec.ITimelineEvent<byte[]>, contract : Events.Event) : Event = raw.Index, raw.Timestamp, contract
684+
let up (raw : FsCodec.ITimelineEvent<ReadOnlyMemory<byte>>, contract : Events.Event) : Event = raw.Index, raw.Timestamp, contract
685685
let down ((_index, timestamp, event) : Event) = event, None, Some timestamp
686686
FsCodec.NewtonsoftJson.Codec.Create(up, down)
687687

src/FsCodec.Box/Interop.fs

+8-38
Original file line numberDiff line numberDiff line change
@@ -4,50 +4,20 @@ open System.Runtime.CompilerServices
44
open System
55

66
[<Extension>]
7-
type InteropExtensions =
7+
type InteropHelpers =
88

9-
static member public Adapt<'From, 'To, 'Event, 'Context>
10-
( native : FsCodec.IEventCodec<'Event, 'From, 'Context>,
11-
up : 'From -> 'To,
12-
down : 'To -> 'From) : FsCodec.IEventCodec<'Event, 'To, 'Context> =
13-
14-
{ new FsCodec.IEventCodec<'Event, 'To, 'Context> with
15-
member _.Encode(context, event) =
16-
let encoded = native.Encode(context, event)
17-
{ new FsCodec.IEventData<_> with
18-
member _.EventType = encoded.EventType
19-
member _.Data = up encoded.Data
20-
member _.Meta = up encoded.Meta
21-
member _.EventId = encoded.EventId
22-
member _.CorrelationId = encoded.CorrelationId
23-
member _.CausationId = encoded.CausationId
24-
member _.Timestamp = encoded.Timestamp }
25-
26-
member _.TryDecode encoded =
27-
let mapped =
28-
{ new FsCodec.ITimelineEvent<_> with
29-
member _.Index = encoded.Index
30-
member _.IsUnfold = encoded.IsUnfold
31-
member _.Context = encoded.Context
32-
member _.EventType = encoded.EventType
33-
member _.Data = down encoded.Data
34-
member _.Meta = down encoded.Meta
35-
member _.EventId = encoded.EventId
36-
member _.CorrelationId = encoded.CorrelationId
37-
member _.CausationId = encoded.CausationId
38-
member _.Timestamp = encoded.Timestamp }
39-
native.TryDecode mapped }
40-
41-
static member private BytesToReadOnlyMemory(x : byte[]) : ReadOnlyMemory<byte> =
9+
static member BytesToReadOnlyMemory(x : byte array) : ReadOnlyMemory<byte> =
4210
if x = null then ReadOnlyMemory.Empty
4311
else ReadOnlyMemory x
44-
static member private ReadOnlyMemoryToBytes(x : ReadOnlyMemory<byte>) : byte[] =
12+
13+
static member ReadOnlyMemoryToBytes(x : ReadOnlyMemory<byte>) : byte array =
4514
if x.IsEmpty then null
4615
else x.ToArray()
4716

48-
/// Adapt an IEventCodec that handles ReadOnlyMemory<byte> Event Bodies to instead use byte[]
17+
/// Adapt an IEventCodec that handles ReadOnlyMemory<byte> Event Bodies to instead use byte array
4918
/// Ideally not used as it makes pooling problematic; only provided for interop/porting scaffolding wrt Equinox V3 and EventStore.Client etc
5019
[<Extension>]
5120
static member ToByteArrayCodec<'Event, 'Context>(native : FsCodec.IEventCodec<'Event, ReadOnlyMemory<byte>, 'Context>)
52-
: FsCodec.IEventCodec<'Event, byte[], 'Context> =
53-
InteropExtensions.Adapt(native, InteropExtensions.ReadOnlyMemoryToBytes, InteropExtensions.BytesToReadOnlyMemory)
21+
: FsCodec.IEventCodec<'Event, byte array, 'Context> =
22+
23+
FsCodec.Core.EventCodec.Map(native, InteropHelpers.ReadOnlyMemoryToBytes, InteropHelpers.BytesToReadOnlyMemory)

src/FsCodec.NewtonsoftJson/Codec.fs

+1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ module Core =
3535
type BytesEncoder(settings : JsonSerializerSettings) =
3636
let serializer = JsonSerializer.Create(settings)
3737
interface TypeShape.UnionContract.IEncoder<ReadOnlyMemory<byte>> with
38+
3839
member _.Empty = ReadOnlyMemory.Empty
3940

4041
member _.Encode (value : 'T) =

src/FsCodec.NewtonsoftJson/VerbatimUtf8Converter.fs

+2-2
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@ type VerbatimUtf8JsonConverter() =
1111
static let enc = System.Text.Encoding.UTF8
1212

1313
override _.CanConvert(t : Type) =
14-
typeof<byte[]>.Equals(t)
14+
typeof<byte array>.Equals(t)
1515

1616
override _.WriteJson(writer : JsonWriter, value : obj, serializer : JsonSerializer) =
17-
let array = value :?> byte[]
17+
let array = value :?> byte array
1818
if array = null || array.Length = 0 then serializer.Serialize(writer, null)
1919
else writer.WriteRawValue(enc.GetString(array))
2020

src/FsCodec.SystemTextJson/FsCodec.SystemTextJson.fsproj

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@
1212
<Compile Include="TypeSafeEnumConverter.fs" />
1313
<Compile Include="UnionOrTypeSafeEnumConverterFactory.fs" />
1414
<Compile Include="Options.fs" />
15+
<Compile Include="Interop.fs" />
1516
<Compile Include="Codec.fs" />
1617
<Compile Include="Serdes.fs" />
17-
<Compile Include="Interop.fs" />
1818
</ItemGroup>
1919

2020
<ItemGroup>

src/FsCodec.SystemTextJson/Interop.fs

+8-6
Original file line numberDiff line numberDiff line change
@@ -6,27 +6,29 @@ open System
66
open System.Text.Json
77

88
[<Extension>]
9-
type InteropExtensions =
9+
type InteropHelpers =
1010

11-
static member private Utf8ToJsonElement(x : ReadOnlyMemory<byte>) : JsonElement =
11+
static member Utf8ToJsonElement(x : ReadOnlyMemory<byte>) : JsonElement =
1212
if x.IsEmpty then JsonElement()
1313
else JsonSerializer.Deserialize<JsonElement>(x.Span)
1414

15-
static member private JsonElementToUtf8(x : JsonElement) : ReadOnlyMemory<byte> =
15+
static member JsonElementToUtf8(x : JsonElement) : ReadOnlyMemory<byte> =
1616
if x.ValueKind = JsonValueKind.Undefined then ReadOnlyMemory.Empty
17-
// Avoid introduction of HTML escaping for things like quotes etc (Options.Default uses Options.Create(), which defaults to unsafeRelaxedJsonEscaping=true)
17+
// Avoid introduction of HTML escaping for things like quotes etc (Options.Default uses Options.Create(), which defaults to unsafeRelaxedJsonEscaping = true)
1818
else JsonSerializer.SerializeToUtf8Bytes(x, options = Options.Default) |> ReadOnlyMemory
1919

2020
/// Adapts an IEventCodec that's rendering to <c>JsonElement</c> Event Bodies to handle <c>ReadOnlyMemory<byte></c> bodies instead.<br/>
2121
/// NOTE where possible, it's better to use <c>Codec</c> in preference to <c>CodecJsonElement</c> to encode directly in order to avoid this mapping process.
2222
[<Extension>]
2323
static member ToUtf8Codec<'Event, 'Context>(native : FsCodec.IEventCodec<'Event, JsonElement, 'Context>)
2424
: FsCodec.IEventCodec<'Event, ReadOnlyMemory<byte>, 'Context> =
25-
FsCodec.Interop.InteropExtensions.Adapt(native, InteropExtensions.JsonElementToUtf8, InteropExtensions.Utf8ToJsonElement)
25+
26+
FsCodec.Core.EventCodec.Map(native, InteropHelpers.JsonElementToUtf8, InteropHelpers.Utf8ToJsonElement)
2627

2728
/// Adapts an IEventCodec that's rendering to <c>ReadOnlyMemory<byte></c> Event Bodies to handle <c>JsonElement</c> bodies instead.<br/>
2829
/// NOTE where possible, it's better to use <c>CodecJsonElement</c> in preference to <c>Codec/c> to encode directly in order to avoid this mapping process.
2930
[<Extension>]
3031
static member ToJsonElementCodec<'Event, 'Context>(native : FsCodec.IEventCodec<'Event, ReadOnlyMemory<byte>, 'Context>)
3132
: FsCodec.IEventCodec<'Event, JsonElement, 'Context> =
32-
FsCodec.Interop.InteropExtensions.Adapt(native, InteropExtensions.Utf8ToJsonElement, InteropExtensions.JsonElementToUtf8)
33+
34+
FsCodec.Core.EventCodec.Map(native, InteropHelpers.Utf8ToJsonElement, InteropHelpers.JsonElementToUtf8)

src/FsCodec/FsCodec.fs

+44-1
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ type EventData<'Format> private (eventType, data, meta, eventId, correlationId,
4848
let eventId = match eventId with Some id -> id | None -> Guid.NewGuid()
4949
EventData(eventType, data, meta, eventId, correlationId, causationId, match timestamp with Some ts -> ts | None -> DateTimeOffset.UtcNow) :> _
5050

51-
interface FsCodec.IEventData<'Format> with
51+
interface IEventData<'Format> with
5252
member _.EventType = eventType
5353
member _.Data = data
5454
member _.Meta = meta
@@ -57,6 +57,17 @@ type EventData<'Format> private (eventType, data, meta, eventId, correlationId,
5757
member _.CausationId = causationId
5858
member _.Timestamp = timestamp
5959

60+
static member Map<'Mapped>(f : 'Format -> 'Mapped) : IEventData<'Format> -> IEventData<'Mapped> =
61+
fun x ->
62+
{ new IEventData<'Mapped> with
63+
member _.EventType = x.EventType
64+
member _.Data = f x.Data
65+
member _.Meta = f x.Meta
66+
member _.EventId = x.EventId
67+
member _.CorrelationId = x.CorrelationId
68+
member _.CausationId = x.CausationId
69+
member _.Timestamp = x.Timestamp }
70+
6071
/// An Event or Unfold that's been read from a Store and hence has a defined <c>Index</c> on the Event Timeline
6172
[<NoComparison; NoEquality>]
6273
type TimelineEvent<'Format> private (index, isUnfold, eventType, data, meta, eventId, correlationId, causationId, timestamp, context) =
@@ -78,3 +89,35 @@ type TimelineEvent<'Format> private (index, isUnfold, eventType, data, meta, eve
7889
member _.CorrelationId = correlationId
7990
member _.CausationId = causationId
8091
member _.Timestamp = timestamp
92+
93+
static member Map<'Mapped>(f : 'Format -> 'Mapped) : ITimelineEvent<'Format> -> ITimelineEvent<'Mapped> =
94+
fun x ->
95+
{ new ITimelineEvent<'Mapped> with
96+
member _.Index = x.Index
97+
member _.IsUnfold = x.IsUnfold
98+
member _.Context = x.Context
99+
member _.EventType = x.EventType
100+
member _.Data = f x.Data
101+
member _.Meta = f x.Meta
102+
member _.EventId = x.EventId
103+
member _.CorrelationId = x.CorrelationId
104+
member _.CausationId = x.CausationId
105+
member _.Timestamp = x.Timestamp }
106+
107+
[<NoComparison; NoEquality>]
108+
type EventCodec<'Event, 'Format, 'Context> private () =
109+
110+
static member Map<'TargetFormat>(native : IEventCodec<'Event, 'Format, 'Context>, up : 'Format -> 'TargetFormat, down : 'TargetFormat -> 'Format)
111+
: IEventCodec<'Event, 'TargetFormat, 'Context> =
112+
113+
let upConvert = EventData.Map up
114+
let downConvert = TimelineEvent.Map down
115+
116+
{ new IEventCodec<'Event, 'TargetFormat, 'Context> with
117+
member _.Encode(context, event) =
118+
let encoded = native.Encode(context, event)
119+
upConvert encoded
120+
121+
member _.TryDecode target =
122+
let encoded = downConvert target
123+
native.TryDecode encoded }

tests/FsCodec.NewtonsoftJson.Tests/Examples.fsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ des<Message2> """{"name":null,"outcome":"Discomfort"}"""
119119
module EventCodec =
120120

121121
/// Uses the supplied codec to decode the supplied event record `x` (iff at LogEventLevel.Debug, detail fails to `log` citing the `stream` and content)
122-
let tryDecode (codec : FsCodec.IEventCodec<_, _, _>) (log : Serilog.ILogger) streamName (x : FsCodec.ITimelineEvent<byte[]>) =
122+
let tryDecode (codec : FsCodec.IEventCodec<_, _, _>) (log : Serilog.ILogger) streamName (x : FsCodec.ITimelineEvent<ReadOnlyMemory<byte>>) =
123123
match codec.TryDecode x with
124124
| None ->
125125
if log.IsEnabled Serilog.Events.LogEventLevel.Debug then
@@ -238,7 +238,7 @@ module Reactions =
238238
type Event = int64 * DateTimeOffset * Events.Event
239239

240240
let codec =
241-
let up (raw : FsCodec.ITimelineEvent<byte[]>, contract : Events.Event) : Event = raw.Index, raw.Timestamp, contract
241+
let up (raw : FsCodec.ITimelineEvent<ReadOnlyMemory<byte>>, contract : Events.Event) : Event = raw.Index, raw.Timestamp, contract
242242
let down ((_index, timestamp, event) : Event) = event, None, Some timestamp
243243
FsCodec.NewtonsoftJson.Codec.Create(up, down)
244244

tests/FsCodec.SystemTextJson.Tests/InteropTests.fs

+2-2
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ type Batch = FsCodec.NewtonsoftJson.Tests.VerbatimUtf8ConverterTests.Batch
1111
type Union = FsCodec.NewtonsoftJson.Tests.VerbatimUtf8ConverterTests.Union
1212
let mkBatch = FsCodec.NewtonsoftJson.Tests.VerbatimUtf8ConverterTests.mkBatch
1313

14-
let indirectCodec = FsCodec.SystemTextJson.CodecJsonElement.Create() |> FsCodec.SystemTextJson.Interop.InteropExtensions.ToUtf8Codec
14+
let indirectCodec = FsCodec.SystemTextJson.CodecJsonElement.Create() |> FsCodec.SystemTextJson.Interop.InteropHelpers.ToUtf8Codec
1515
let [<Fact>] ``encodes correctly`` () =
1616
let input = Union.A { embed = "\"" }
1717
let encoded = indirectCodec.Encode(None, input)
@@ -34,7 +34,7 @@ type U =
3434

3535
let defaultSettings = FsCodec.NewtonsoftJson.Options.CreateDefault() // Test without converters, as that's what Equinox.Cosmos will do
3636
let defaultEventCodec = FsCodec.NewtonsoftJson.Codec.Create<U>(defaultSettings)
37-
let indirectCodecU = FsCodec.SystemTextJson.CodecJsonElement.Create<U>() |> FsCodec.SystemTextJson.Interop.InteropExtensions.ToUtf8Codec
37+
let indirectCodecU = FsCodec.SystemTextJson.CodecJsonElement.Create<U>() |> FsCodec.SystemTextJson.Interop.InteropHelpers.ToUtf8Codec
3838

3939
let [<Property>] ``round-trips diverse bodies correctly`` (x: U, encodeDirect, decodeDirect) =
4040
let encoder = if encodeDirect then defaultEventCodec else indirectCodecU

0 commit comments

Comments
 (0)