diff --git a/CHANGELOG.md b/CHANGELOG.md index 2cdfed1..b688535 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,10 +9,18 @@ The `Unreleased` section name is replaced by the expected version of next releas ## [Unreleased] ### Added + +- `Core.EventData/TimelineEvent`: Exposed default ctors [#83](https://github.com/jet/FsCodec/pull/83) + ### Changed + +- `Codec.Create`: Made timestamp mandatory in low level `up` / `down` signature [#83](https://github.com/jet/FsCodec/pull/83) + ### Removed ### Fixed +- `EventData.Create`: restored defaulting of `EventId` to `Guid.NewGuid` broken in [#82](https://github.com/jet/FsCodec/pull/82) [#83](https://github.com/jet/FsCodec/pull/83) + ## [3.0.0-rc.6] - 2022-09-02 diff --git a/src/FsCodec.Box/Codec.fs b/src/FsCodec.Box/Codec.fs index 54243a6..698abc4 100755 --- a/src/FsCodec.Box/Codec.fs +++ b/src/FsCodec.Box/Codec.fs @@ -1,4 +1,4 @@ -// Equivalent of FsCodec.NewtonsoftJson/SystemTextJson.Codec intended to provide equivalent calls and functionality, without actually serializing/deserializing as JSON +// Mirror of FsCodec.NewtonsoftJson/SystemTextJson.Codec intended to provide equivalent calls and functionality, without actually serializing/deserializing as JSON // This is a useful facility for in-memory stores such as Equinox's MemoryStore as it enables you to // - efficiently test behaviors from an event sourced decision processing perspective (e.g. with Property Based Tests) // - without paying a serialization cost and/or having to deal with sanitization of generated data in order to make it roundtrippable through same @@ -27,7 +27,7 @@ type Codec private () = // Maps a fresh Event resulting from a Decision in the Domain representation type down to the TypeShape UnionConverter 'Contract
// The function is also expected to derive an optional meta object that will be serialized with the same encoder, // and eventId, correlationId, causationId and an Event Creationtimestamp
. - down : struct ('Context * 'Event) -> struct ('Contract * 'Meta voption * Guid * string * string * DateTimeOffset voption), + down : struct ('Context * 'Event) -> struct ('Contract * 'Meta voption * Guid * string * string * DateTimeOffset), // Enables one to fail encoder generation if union contains nullary cases. Defaults to false, i.e. permitting them. [] ?rejectNullaryCases) : FsCodec.IEventCodec<'Event, obj, 'Context> = @@ -45,7 +45,7 @@ type Codec private () = // Maps a fresh Event resulting from a Decision in the Domain representation type down to the TypeShape UnionConverter 'Contract // The function is also expected to derive // a meta object that will be serialized with the same options (if it's not None) - // and an Event Creation timestamp. + // and an Event Creation timestamp (Default: DateTimeOffset.UtcNow). down : 'Event -> struct ('Contract * 'Meta voption * DateTimeOffset voption), // Uses the 'Context passed to the Encode call and the 'Meta emitted by down to a) the final metadata b) the eventId c) the correlationId and d) the causationId mapCausation : struct ('Context * 'Meta voption) -> struct ('Meta voption * Guid * string * string), @@ -66,7 +66,7 @@ type Codec private () = // Maps a fresh 'Event resulting from a Decision in the Domain representation type down to the TypeShape UnionConverter 'Contract // The function is also expected to derive // a meta object that will be serialized with the same options (if it's not None) - // and an Event Creation timestamp. + // and an Event Creation timestamp (Default: DateTimeOffset.UtcNow). down : 'Event -> struct ('Contract * 'Meta voption * DateTimeOffset voption), // Enables one to fail encoder generation if union contains nullary cases. Defaults to false, i.e. permitting them. [] ?rejectNullaryCases) diff --git a/src/FsCodec.Box/CoreCodec.fs b/src/FsCodec.Box/CoreCodec.fs index 2c7ac87..0609c45 100755 --- a/src/FsCodec.Box/CoreCodec.fs +++ b/src/FsCodec.Box/CoreCodec.fs @@ -22,7 +22,7 @@ type Codec private () = // Maps a fresh Event resulting from a Decision in the Domain representation type down to the TypeShape UnionConverter 'Contract
// The function is also expected to derive an optional meta object that will be serialized with the same encoder, // and eventId, correlationId, causationId and an Event Creationtimestamp
. - down : struct ('Context * 'Event) -> struct ('Contract * 'Meta voption * Guid * string * string * DateTimeOffset voption), + down : struct ('Context * 'Event) -> struct ('Contract * 'Meta voption * Guid * string * string * DateTimeOffset), // Enables one to fail encoder generation if union contains nullary cases. Defaults to false, i.e. permitting them. [] ?rejectNullaryCases) : FsCodec.IEventCodec<'Event, 'Body, 'Context> = @@ -36,12 +36,12 @@ type Codec private () = allowNullaryCases = not (defaultArg rejectNullaryCases false)) { new FsCodec.IEventCodec<'Event, 'Body, 'Context> with + member _.Encode(context, event) = - let struct (c, meta : 'Meta voption, eventId, correlationId, causationId, timestamp : DateTimeOffset voption) = down struct (context, event) + let struct (c, meta : 'Meta voption, eventId, correlationId, causationId, timestamp) = down (context, event) let enc = dataCodec.Encode c - let meta' = match meta with ValueSome x -> encoder.Encode<'Meta> x | ValueNone -> Unchecked.defaultof<_> - let ts = match timestamp with ValueNone -> None | ValueSome v -> Some v - EventData.Create(enc.CaseName, enc.Payload, meta', eventId, correlationId, causationId, ?timestamp = ts) + let meta' = match meta with ValueSome x -> encoder.Encode<'Meta> x | ValueNone -> Unchecked.defaultof<'Body> + EventData(enc.CaseName, enc.Payload, meta', eventId, correlationId, causationId, timestamp) member _.TryDecode encoded = match dataCodec.TryDecode { CaseName = encoded.EventType; Payload = encoded.Data } with @@ -61,9 +61,9 @@ type Codec private () = // Maps a fresh Event resulting from a Decision in the Domain representation type down to the TypeShape UnionConverter 'Contract // The function is also expected to derive // a meta object that will be serialized with the same options (if it's not None) - // and an Event Creation timestamp. + // and an Event Creation timestamp (Default: DateTimeOffset.UtcNow). down : 'Event -> struct ('Contract * 'Meta voption * DateTimeOffset voption), - // Uses the 'Context passed to the Encode call and the 'Meta emitted by down to a) the final metadata b) the eventId c) the correlationId and d) the causationId + // Uses the 'Context passed to the Encode call and the 'Meta emitted by down to produce a) the final metadata b) the eventId c) the correlationId and d) the causationId mapCausation : struct ('Context * 'Meta voption) -> struct ('Meta voption * Guid * string * string), // Enables one to fail encoder generation if union contains nullary cases. Defaults to false, i.e. permitting them. [] ?rejectNullaryCases) @@ -72,7 +72,7 @@ type Codec private () = let down struct (context, union) = let struct (c, m, t) = down union let struct (m', eventId, correlationId, causationId) = mapCausation (context, m) - struct (c, m', eventId, correlationId, causationId, t) + struct (c, m', eventId, correlationId, causationId, match t with ValueSome t -> t | ValueNone -> DateTimeOffset.Now) Codec.Create(encoder, up = up, down = down, ?rejectNullaryCases = rejectNullaryCases) /// Generate an IEventCodec using the supplied encoder.
@@ -88,7 +88,7 @@ type Codec private () = // Maps a fresh 'Event resulting from a Decision in the Domain representation type down to the TypeShape UnionConverter 'Contract // The function is also expected to derive // a meta object that will be serialized with the same options (if it's not None) - // and an Event Creation timestamp. + // and an Event Creation timestamp (Default: DateTimeOffset.UtcNow).
down : 'Event -> struct ('Contract * 'Meta voption * DateTimeOffset voption), // Enables one to fail encoder generation if union contains nullary cases. Defaults to false, i.e. permitting them. [] ?rejectNullaryCases) @@ -107,5 +107,5 @@ type Codec private () = : FsCodec.IEventCodec<'Union, 'Body, unit> = let up struct (_e : FsCodec.ITimelineEvent<'Body>, u : 'Union) : 'Union = u - let down (event : 'Union) = struct (event, ValueNone, ValueNone) + let down (event : 'Union) = struct (event, ValueNone (*Meta*), ValueNone (*Timestamp*)) Codec.Create(encoder, up = up, down = down, ?rejectNullaryCases = rejectNullaryCases) diff --git a/src/FsCodec.NewtonsoftJson/Codec.fs b/src/FsCodec.NewtonsoftJson/Codec.fs index caeb808..e889d70 100755 --- a/src/FsCodec.NewtonsoftJson/Codec.fs +++ b/src/FsCodec.NewtonsoftJson/Codec.fs @@ -74,7 +74,7 @@ type Codec private () = // Maps a fresh Event resulting from a Decision in the Domain representation type down to the TypeShape UnionConverter 'Contract
// The function is also expected to derive an optional meta object that will be serialized with the same encoder, // and eventId, correlationId, causationId and an Event Creationtimestamp
. - down : struct ('Context * 'Event) -> struct ('Contract * 'Meta voption * Guid * string * string * DateTimeOffset voption), + down : struct ('Context * 'Event) -> struct ('Contract * 'Meta voption * Guid * string * string * DateTimeOffset), // Configuration to be used by the underlying Newtonsoft.Json Serializer when encoding/decoding. Defaults to same as Options.Default [] ?options, // Enables one to fail encoder generation if union contains nullary cases. Defaults to false, i.e. permitting them. @@ -94,7 +94,7 @@ type Codec private () = // Maps a fresh Event resulting from a Decision in the Domain representation type down to the TypeShape UnionConverter 'Contract // The function is also expected to derive // a meta object that will be serialized with the same options (if it's not None) - // and an Event Creation timestamp. + // and an Event Creation timestamp (Default: DateTimeOffset.UtcNow). down : 'Event -> struct ('Contract * 'Meta voption * DateTimeOffset voption), // Uses the 'Context passed to the Encode call and the 'Meta emitted by down to a) the final metadata b) the eventId c) the correlationId and d) the causationId mapCausation : struct ('Context * 'Meta voption) -> struct ('Meta voption * Guid * string * string), diff --git a/src/FsCodec.SystemTextJson/Codec.fs b/src/FsCodec.SystemTextJson/Codec.fs index d1abf8e..1057246 100755 --- a/src/FsCodec.SystemTextJson/Codec.fs +++ b/src/FsCodec.SystemTextJson/Codec.fs @@ -44,7 +44,7 @@ type Codec private () = // Maps a fresh Event resulting from a Decision in the Domain representation type down to the TypeShape UnionConverter 'Contract
// The function is also expected to derive an optional meta object that will be serialized with the same encoder, // and eventId, correlationId, causationId and an Event Creationtimestamp
. - down : struct ('Context * 'Event) -> struct ('Contract * 'Meta voption * Guid * string * string * DateTimeOffset voption), + down : struct ('Context * 'Event) -> struct ('Contract * 'Meta voption * Guid * string * string * DateTimeOffset), // Configuration to be used by the underlying System.Text.Json Serializer when encoding/decoding. Defaults to same as Options.Default [] ?options, // Enables one to fail encoder generation if union contains nullary cases. Defaults to false, i.e. permitting them. @@ -64,7 +64,7 @@ type Codec private () = // Maps a fresh Event resulting from a Decision in the Domain representation type down to the TypeShape UnionConverter 'Contract // The function is also expected to derive // a meta object that will be serialized with the same options (if it's not None) - // and an Event Creation timestamp. + // and an Event Creation timestamp (Default: DateTimeOffset.UtcNow). down : 'Event -> struct ('Contract * 'Meta voption * DateTimeOffset voption), // Uses the 'Context passed to the Encode call and the 'Meta emitted by down to a) the final metadata b) the eventId c) the correlationId and d) the causationId mapCausation : struct ('Context * 'Meta voption) -> struct ('Meta voption * Guid * string * string), diff --git a/src/FsCodec.SystemTextJson/CodecJsonElement.fs b/src/FsCodec.SystemTextJson/CodecJsonElement.fs index ec5009a..faeb1c0 100755 --- a/src/FsCodec.SystemTextJson/CodecJsonElement.fs +++ b/src/FsCodec.SystemTextJson/CodecJsonElement.fs @@ -43,7 +43,7 @@ type CodecJsonElement private () = // Maps a fresh Event resulting from a Decision in the Domain representation type down to the TypeShape UnionConverter 'Contract
// The function is also expected to derive an optional meta object that will be serialized with the same encoder, // and eventId, correlationId, causationId and an Event Creationtimestamp
. - down : struct ('Context * 'Event) -> struct ('Contract * 'Meta voption * Guid * string * string * DateTimeOffset voption), + down : struct ('Context * 'Event) -> struct ('Contract * 'Meta voption * Guid * string * string * DateTimeOffset), // Configuration to be used by the underlying System.Text.Json Serializer when encoding/decoding. Defaults to same as Options.Default [] ?options, // Enables one to fail encoder generation if union contains nullary cases. Defaults to false, i.e. permitting them. @@ -63,7 +63,7 @@ type CodecJsonElement private () = // Maps a fresh Event resulting from a Decision in the Domain representation type down to the TypeShape UnionConverter 'Contract // The function is also expected to derive // a meta object that will be serialized with the same options (if it's not None) - // and an Event Creation timestamp. + // and an Event Creation timestamp (Default: DateTimeOffset.UtcNow). down : 'Event -> struct ('Contract * 'Meta voption * DateTimeOffset voption), // Uses the 'Context passed to the Encode call and the 'Meta emitted by down to a) the final metadata b) the eventId c) the correlationId and d) the causationId mapCausation : struct ('Context * 'Meta voption) -> struct ('Meta voption * Guid * string * string), diff --git a/src/FsCodec/Codec.fs b/src/FsCodec/Codec.fs index b654526..e0ae474 100755 --- a/src/FsCodec/Codec.fs +++ b/src/FsCodec/Codec.fs @@ -13,16 +13,15 @@ type Codec = static member private Create<'Event, 'Format, 'Context> ( // Maps an 'Event to: an Event Type Name, a pair of 'Format's representing the Data and Meta together with the // eventId, correlationId, causationId and timestamp. - encode : struct ('Context * 'Event) -> struct (string * 'Format * 'Format * Guid * string * string * DateTimeOffset voption), + encode : struct ('Context * 'Event) -> struct (string * 'Format * 'Format * Guid * string * string * DateTimeOffset), // Attempts to map from an Event's stored data to Some 'Event, or None if not mappable. tryDecode : ITimelineEvent<'Format> -> 'Event voption) : IEventCodec<'Event, 'Format, 'Context> = { new IEventCodec<'Event, 'Format, 'Context> with member _.Encode(context, event) = - let struct (eventType, data, metadata, eventId, correlationId, causationId, timestamp) = encode struct (context, event) - let ts = match timestamp with ValueSome x -> Some x | ValueNone -> None - Core.EventData.Create(eventType, data, metadata, eventId, correlationId, causationId, ?timestamp = ts) + let struct (eventType, data, metadata, eventId, correlationId, causationId, timestamp) = encode (context, event) + Core.EventData(eventType, data, metadata, eventId, correlationId, causationId, timestamp) member _.TryDecode encoded = tryDecode encoded } @@ -30,10 +29,10 @@ type Codec = /// Generate an IEventCodec suitable using the supplied encode and tryDecode functions to map to/from the stored form. /// mapCausation provides metadata generation and correlation/causationId mapping based on the context passed to the encoder static member Create<'Event, 'Format, 'Context> - ( // Maps a fresh 'Event resulting from a Decision in the Domain representation type down to the TypeShape UnionConverter 'Contract - // The function is also expected to derive + ( // Maps a fresh 'Event resulting from the Domain representation type down to the TypeShape UnionConverter 'Contract + // The function is also responsible for deriving: // a meta object that will be serialized with the same settings (if it's not None) - // and an Event Creation timestamp. + // and an Event Creation timestamp (Default: DateTimeOffset.UtcNow). encode : 'Event -> struct (string * 'Format * DateTimeOffset voption), // Maps from the TypeShape UnionConverter 'Contract case the Event has been mapped to (with the raw event data as context) // to the 'Event representation (typically a Discriminated Union) that is to be presented to the programming model. @@ -44,20 +43,22 @@ type Codec = let encode struct (context, event) = let struct (et, d, t) = encode event + let ts = match t with ValueSome x -> x | ValueNone -> DateTimeOffset.UtcNow let struct (m, eventId, correlationId, causationId) = mapCausation (context, event) - struct (et, d, m, eventId, correlationId, causationId, t) + struct (et, d, m, eventId, correlationId, causationId, ts) Codec.Create(encode, tryDecode) /// Generate an IEventCodec using the supplied pair of encode and tryDecode functions. static member Create<'Event, 'Format> - ( // Maps a 'Event to an Event Type Name and a UTF-8 array representing the Data. + ( // Maps a 'Event to an Event Type Name and an encoded body (to be used as the Data). encode : 'Event -> struct (string * 'Format), - // Attempts to map an Event Type Name and a UTF-8 array Data to Some 'Event case, or None if not mappable. + // Attempts to map an Event Type Name and an encoded Data to Some 'Event case, or None if not mappable. tryDecode : struct (string * 'Format) -> 'Event voption) : IEventCodec<'Event, 'Format, unit> = let encode' struct (_context, event) = let struct (eventType, data : 'Format) = encode event - struct (eventType, data, Unchecked.defaultof<'Format> (* metadata *), Guid.NewGuid() (* eventId *), null (* correlationId *), null (* causationId *), ValueNone (* timestamp *)) + struct (eventType, data, Unchecked.defaultof<'Format> (* metadata *), + Guid.NewGuid() (* eventId *), null (* correlationId *), null (* causationId *), DateTimeOffset.UtcNow (* timestamp *)) let tryDecode' (encoded : ITimelineEvent<'Format>) = tryDecode (encoded.EventType, encoded.Data) Codec.Create(encode', tryDecode') diff --git a/src/FsCodec/FsCodec.fs b/src/FsCodec/FsCodec.fs index 0bd6bab..a15239c 100755 --- a/src/FsCodec/FsCodec.fs +++ b/src/FsCodec/FsCodec.fs @@ -44,11 +44,11 @@ open System /// An Event about to be written, see IEventData for further information [] -type EventData<'Format> private (eventType, data, meta, eventId, correlationId, causationId, timestamp) = +type EventData<'Format>(eventType, data, meta, eventId, correlationId, causationId, timestamp) = static member Create(eventType, data, ?meta, ?eventId, ?correlationId, ?causationId, ?timestamp : DateTimeOffset) : IEventData<'Format> = - let meta = match meta with Some x -> x | None -> Unchecked.defaultof<_> - let eventId = match eventId with Some x -> x | None -> Guid.Empty + let meta = match meta with Some x -> x | None -> Unchecked.defaultof<'Format> + let eventId = match eventId with Some x -> x | None -> Guid.NewGuid() let ts = match timestamp with Some ts -> ts | None -> DateTimeOffset.UtcNow EventData(eventType, data, meta, eventId, Option.toObj correlationId, Option.toObj causationId, ts) :> _ @@ -74,7 +74,7 @@ type EventData<'Format> private (eventType, data, meta, eventId, correlationId, /// An Event or Unfold that's been read from a Store and hence has a defined Index on the Event Timeline [] -type TimelineEvent<'Format> private (index, eventType, data, meta, eventId, correlationId, causationId, timestamp, isUnfold, context, size) = +type TimelineEvent<'Format>(index, eventType, data, meta, eventId, correlationId, causationId, timestamp, isUnfold, context, size) = static member Create(index, eventType, data, ?meta, ?eventId, ?correlationId, ?causationId, ?timestamp, ?isUnfold, ?context, ?size) : ITimelineEvent<'Format> = let isUnfold = defaultArg isUnfold false @@ -122,6 +122,7 @@ type EventCodec<'Event, 'Format, 'Context> private () = let downConvert = TimelineEvent.Map down { new IEventCodec<'Event, 'TargetFormat, 'Context> with + member _.Encode(context, event) = let encoded = native.Encode(context, event) upConvert encoded diff --git a/tests/FsCodec.SystemTextJson.Tests/CodecTests.fs b/tests/FsCodec.SystemTextJson.Tests/CodecTests.fs index 8962145..93656e1 100644 --- a/tests/FsCodec.SystemTextJson.Tests/CodecTests.fs +++ b/tests/FsCodec.SystemTextJson.Tests/CodecTests.fs @@ -57,8 +57,10 @@ let [] roundtrips value = let des = serdes.Deserialize ser let wrapped = FsCodec.Core.TimelineEvent.Create(-1L, eventType, des.d) + test <@ wrapped.EventId = System.Guid.Empty + && (let d = System.DateTimeOffset.UtcNow - wrapped.Timestamp + abs d.TotalMinutes < 1) @> let decoded = eventCodec.TryDecode wrapped |> ValueOption.get - let expected = match value with | AO ({ opt = Some null } as v) -> AO { v with opt = None } @@ -69,3 +71,22 @@ let [] roundtrips value = // Also validate the adapters work when put in series (NewtonsoftJson tests are responsible for covering the individual hops) let decodedMultiHop = multiHopCodec.TryDecode wrapped |> ValueOption.get test <@ expected = decodedMultiHop @> + +let [] ``EventData.Create basics`` () = + let e = FsCodec.Core.EventData.Create("et", "data") + + test <@ e.EventId <> System.Guid.Empty + && e.EventType = "et" + && e.Data = "data" + && (let d = System.DateTimeOffset.UtcNow - e.Timestamp + abs d.TotalMinutes < 1) @> + +let [] ``TimelineEvent.Create basics`` () = + let e = FsCodec.Core.TimelineEvent.Create(42, "et", "data") + + test <@ e.EventId = System.Guid.Empty + && not e.IsUnfold + && e.EventType = "et" + && e.Data = "data" + && (let d = System.DateTimeOffset.UtcNow - e.Timestamp + abs d.TotalMinutes < 1) @> diff --git a/tests/FsCodec.SystemTextJson.Tests/PicklerTests.fs b/tests/FsCodec.SystemTextJson.Tests/PicklerTests.fs index 6ea01d2..734efc8 100644 --- a/tests/FsCodec.SystemTextJson.Tests/PicklerTests.fs +++ b/tests/FsCodec.SystemTextJson.Tests/PicklerTests.fs @@ -60,4 +60,4 @@ let [] ``Global GuidConverter roundtrips`` () = // With the converter, things roundtrip either way for result in [defaultHandlingHasDashes; resNoDashes] do let des = serdesWithConverter.Deserialize result - test <@ value= des @> + test <@ value = des @>