Skip to content

Commit

Permalink
refactor!: Rename IEventCodec.TryDecode to Decode (#107)
Browse files Browse the repository at this point in the history
  • Loading branch information
nordfjord committed Nov 17, 2023
1 parent 4da2fc4 commit 02ec614
Show file tree
Hide file tree
Showing 11 changed files with 53 additions and 52 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ The `Unreleased` section name is replaced by the expected version of next releas

### Added
### Changed
- `IEventCodec.TryDecode`: Rename to `Decode` (to align with the primary assumption of a `Try` prefix per BCL conventions: It won't throw, no matter what!) [#107](https://github.com/jet/FsCodec/pull/107) :pray: [@nordfjord](https://github.com/nordfjord)
### Removed
### Fixed

Expand Down
28 changes: 14 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ The components within this repository are delivered as multi-targeted Nuget pack
- [![Codec NuGet](https://img.shields.io/nuget/v/FsCodec.svg)](https://www.nuget.org/packages/FsCodec/) `FsCodec` Defines interfaces with trivial implementation helpers.
- No dependencies.
- [`FsCodec.IEventCodec`](https://github.com/jet/FsCodec/blob/master/src/FsCodec/FsCodec.fs#L19): defines a base interface for serializers.
- [`FsCodec.Codec`](https://github.com/jet/FsCodec/blob/master/src/FsCodec/Codec.fs#L5): enables plugging in a serializer and/or Union Encoder of your choice (typically this is used to supply a pair `encode` and `tryDecode` functions)
- [`FsCodec.Codec`](https://github.com/jet/FsCodec/blob/master/src/FsCodec/Codec.fs#L5): enables plugging in custom serialization (a trivial implementation of the interface that simply delegates to a pair of `encode` and `decode` functions you supply)
- [`FsCodec.StreamName`](https://github.com/jet/FsCodec/blob/master/src/FsCodec/StreamName.fs): strongly-typed wrapper for a Stream Name, together with factory functions and active patterns for parsing same
- [`FsCodec.StreamId`](https://github.com/jet/FsCodec/blob/master/src/FsCodec/StreamId.fs): strongly-typed wrapper for a Stream Id, together with factory functions and active patterns for parsing same
- [![Box Codec NuGet](https://img.shields.io/nuget/v/FsCodec.Box.svg)](https://www.nuget.org/packages/FsCodec.Box/) `FsCodec.Box`: See [`FsCodec.Box.Codec`](#boxcodec); `IEventCodec<obj>` implementation that provides a null encode/decode step in order to enable decoupling of serialization/deserialization concerns from the encoding aspect, typically used together with [`Equinox.MemoryStore`](https://www.fuget.org/packages/Equinox.MemoryStore)
Expand All @@ -30,8 +30,8 @@ The purpose of the `FsCodec` package is to provide a minimal interface on which

- [`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)
- [`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.
- [`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`
- [`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)
- [`FsCodec.IEventCodec`](https://github.com/jet/FsCodec/blob/master/src/FsCodec/FsCodec.fs#L31) presents `Encode: 'Context option * 'Event -> IEventData` and `Decode: ITimelineEvent -> 'Event voption` 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`
- [`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 `decode: string * byte[] -> 'Event voption` functions (other overloads are available for advanced cases)
- [`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.
- [`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.

Expand Down Expand Up @@ -479,7 +479,7 @@ type IEventCodec<'Event, 'Format, 'Context> =
/// Encodes a <c>'Event</c> instance into a <c>'Format</c> representation
abstract Encode: context: 'Context * value: 'Event -> IEventData<'Format>
/// Decodes a formatted representation into a <c>'Event<c> instance. Does not throw exception on undefined <c>EventType</c>s
abstract TryDecode: encoded: ITimelineEvent<'Format> -> 'Event voption
abstract Decode: encoded: ITimelineEvent<'Format> -> 'Event voption
```

`IEventCodec` represents a standard contract for the encoding and decoding of events used in event sourcing and event based notification scenarios:
Expand Down Expand Up @@ -728,13 +728,13 @@ module private Stream =
/// Inverse of `id`; decodes a StreamId into its constituent parts; throws if the presented StreamId does not adhere to the expected format
let decodeId: FsCodec.StreamId -> ClientId = FsCodec.StreamId.dec ClientId.parse
/// Inspects a stream name; if for this Category, decodes the elements into application level ids. Throws if it's malformed.
let tryDecode: FsCodec.StreamName -> ClientId voption = FsCodec.StreamName.tryFind Category >> ValueOption.map decodeId
let decode: FsCodec.StreamName -> ClientId voption = FsCodec.StreamName.tryFind Category >> ValueOption.map decodeId
module Reactions =
/// Active Pattern to determine whether a given {category}-{streamId} StreamName represents the stream associated with this Aggregate
/// Yields a strongly typed id from the streamId if the Category matches
let [<return: Struct>] (|For|_|) = Stream.tryDecode
let [<return: Struct>] (|For|_|) = Stream.decode
let private dec = Streams.codec<Events.Event>
/// Yields decoded events and relevant strongly typed ids if the Category of the Stream Name matches
Expand Down Expand Up @@ -772,12 +772,12 @@ let streamCodec<'E when 'E :> TypeShape.UnionContract.IUnionContract> : Codec<'E
Codec.Create<'E>(serdes = Store.serdes)
let dec = streamCodec<Events.Event>
let [<return:Struct>] (|TryDecodeEvent|_|) (codec: Codec<'E>) event = codec.TryDecode event
let [<return:Struct>] (|DecodeEvent|_|) (codec: Codec<'E>) event = codec.Decode event
let runCodecExplicit () =
for stream, event in events do
match stream, event with
| Reactions.For clientId, TryDecodeEvent dec e ->
| Reactions.For clientId, DecodeEvent dec e ->
printfn $"Client %s{ClientId.toString clientId}, event %A{e}"
| FsCodec.StreamName.Split struct (cat, sid), e ->
printfn $"Unhandled Event: Category %s{cat}, Ids %s{FsCodec.StreamId.toString sid}, Index %d{e.Index}, Event: %A{e.EventType}"
Expand Down Expand Up @@ -817,7 +817,7 @@ module ReactionsBasic =
let dec = streamCodec<Events.Event>
let (|DecodeSingle|_|): FsCodec.StreamName * Event -> (ClientId * Events.Event) option = function
| Reactions.For clientId, TryDecodeEvent dec event -> Some (clientId, event)
| Reactions.For clientId, DecodeEvent dec event -> Some (clientId, event)
| _ -> None
```

Expand Down Expand Up @@ -862,8 +862,8 @@ module Streams =
System.Text.Encoding.UTF8.GetString(x.Span)
/// Uses the supplied codec to decode the supplied event record `x`
/// (iff at LogEventLevel.Debug, detail fails to `log` citing the `streamName` and body)
let tryDecode<'E> (log: Serilog.ILogger) (codec: Codec<'E>) (streamName: FsCodec.StreamName) (x: Event) =
match codec.TryDecode x with
let decode<'E> (log: Serilog.ILogger) (codec: Codec<'E>) (streamName: FsCodec.StreamName) (x: Event) =
match codec.Decode x with
| ValueNone ->
if log.IsEnabled Serilog.Events.LogEventLevel.Debug then
log.ForContext("event", render x.Data, true)
Expand All @@ -872,12 +872,12 @@ module Streams =
| ValueSome x -> ValueSome x
/// Attempts to decode the supplied Event using the supplied Codec
let [<return: Struct>] (|TryDecode|_|) (codec: Codec<'E>) struct (streamName, event) =
tryDecode Serilog.Log.Logger codec streamName event
let [<return: Struct>] (|Decode|_|) (codec: Codec<'E>) struct (streamName, event) =
decode Serilog.Log.Logger codec streamName event
module Array = let inline chooseV f xs = [| for item in xs do match f item with ValueSome v -> yield v | ValueNone -> () |]
/// Yields the subset of events that successfully decoded (could be Array.empty)
let decode<'E> (codec: Codec<'E>) struct (streamName, events: Event[]): 'E[] =
events |> Array.chooseV (tryDecode<'E> Serilog.Log.Logger codec streamName)
events |> Array.chooseV (decode<'E> Serilog.Log.Logger codec streamName)
let (|Decode|) = decode
```

Expand Down
2 changes: 1 addition & 1 deletion src/FsCodec.Box/Compression.fs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ module private EncodedMaybeCompressed =
let [<Literal>] Deflate = 1 // Deprecated encoding produced by versions pre 3.0.0-rc.13; no longer produced
let [<Literal>] Brotli = 2 // Default encoding as of 3.0.0-rc.13

(* Decompression logic: triggered by extension methods below at the point where the Codec's TryDecode retrieves the Data or Meta properties *)
(* Decompression logic: triggered by extension methods below at the point where the Codec's Decode retrieves the Data or Meta properties *)

// In versions pre 3.0.0-rc.13, the compression was implemented as follows; NOTE: use of Flush vs Close saves space but is unconventional
// let private deflate (eventBody: ReadOnlyMemory<byte>): System.IO.MemoryStream =
Expand Down
2 changes: 1 addition & 1 deletion src/FsCodec.Box/CoreCodec.fs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ type Codec private () =
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 =
member _.Decode encoded =
match dataCodec.TryDecode { CaseName = encoded.EventType; Payload = encoded.Data } with
| None -> ValueNone
| Some contract -> up.Invoke(encoded, contract) |> ValueSome }
Expand Down
22 changes: 11 additions & 11 deletions src/FsCodec/Codec.fs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ open System
[<AbstractClass; Sealed>]
type Codec =

/// <summary>Generate an <code>IEventCodec</code> suitable using the supplied pair of <c>encode</c> and <c>tryDecode</c> functions.</summary>
/// <summary>Generate an <code>IEventCodec</code> suitable using the supplied pair of <c>encode</c> and <c>decode</c> functions.</summary>
// Leaving this helper private until we have a real use case which will e.g. enable us to decide whether to align the signature with the up/down functions
// employed in the convention-based Codec
// (IME, while many systems have some code touching the metadata, it's not something one typically wants to encourage)
Expand All @@ -16,18 +16,18 @@ type Codec =
// <c>eventId</c>, <c>correlationId</c>, <c>causationId</c> and <c>timestamp</c>.</summary>
encode: Func<'Context, 'Event, struct (string * 'Format * 'Format * Guid * string * string * DateTimeOffset)>,
// <summary>Attempts to map from an Event's stored data to <c>Some 'Event</c>, or <c>None</c> if not mappable.</summary>
tryDecode: Func<ITimelineEvent<'Format>, 'Event voption>)
decode: Func<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.Invoke(context, event)
Core.EventData(eventType, data, metadata, eventId, correlationId, causationId, timestamp)

member _.TryDecode encoded =
tryDecode.Invoke encoded }
member _.Decode encoded =
decode.Invoke encoded }

/// <summary>Generate an <c>IEventCodec</c> suitable using the supplied <c>encode</c> and <c>tryDecode</c> functions to map to/from the stored form.
/// <summary>Generate an <c>IEventCodec</c> suitable using the supplied <c>encode</c> and <c>decode</c> functions to map to/from the stored form.
/// <c>mapCausation</c> provides metadata generation and correlation/causationId mapping based on the <c>context</c> passed to the encoder</summary>
static member Create<'Event, 'Format, 'Context>
( // Maps a fresh <c>'Event</c> resulting from the Domain representation type down to the TypeShape <c>UnionConverter</c> <c>'Contract</c>
Expand All @@ -37,7 +37,7 @@ type Codec =
encode: Func<'Event, struct (string * 'Format * DateTimeOffset voption)>,
// Maps from the TypeShape <c>UnionConverter</c> <c>'Contract</c> case the Event has been mapped to (with the raw event data as context)
// to the <c>'Event</c> representation (typically a Discriminated Union) that is to be presented to the programming model.
tryDecode: Func<ITimelineEvent<'Format>, 'Event voption>,
decode: Func<ITimelineEvent<'Format>, 'Event voption>,
// Uses the 'Context passed to the Encode call and the 'Meta emitted by <c>down</c> to a) the final metadata b) the <c>correlationId</c> and c) the correlationId
mapCausation: Func<'Context, 'Event, struct ('Format * Guid * string * string)>)
: IEventCodec<'Event, 'Format, 'Context> =
Expand All @@ -47,19 +47,19 @@ type Codec =
let ts = match t with ValueSome x -> x | ValueNone -> DateTimeOffset.UtcNow
let struct (m, eventId, correlationId, causationId) = mapCausation.Invoke(context, event)
struct (et, d, m, eventId, correlationId, causationId, ts)
Codec.Create(encode, tryDecode)
Codec.Create(encode, decode)

/// Generate an <code>IEventCodec</code> using the supplied pair of <c>encode</c> and <c>tryDecode</code> functions.
/// Generate an <code>IEventCodec</code> using the supplied pair of <c>encode</c> and <c>decode</code> functions.
static member Create<'Event, 'Format>
( // Maps a <c>'Event</c> to an Event Type Name and an encoded body (to be used as the <c>Data</c>).
encode: Func<'Event, struct (string * 'Format)>,
// Attempts to map an Event Type Name and an encoded <c>Data</c> to <c>Some 'Event</c> case, or <c>None</c> if not mappable.
tryDecode: Func<string, 'Format, 'Event voption>)
decode: Func<string, 'Format, 'Event voption>)
: IEventCodec<'Event, 'Format, unit> =

let encode' _context event =
let struct (eventType, data : 'Format) = encode.Invoke event
struct (eventType, data, Unchecked.defaultof<'Format> (* metadata *),
Guid.NewGuid() (* eventId *), null (* correlationId *), null (* causationId *), DateTimeOffset.UtcNow (* timestamp *))
let tryDecode' (encoded : ITimelineEvent<'Format>) = tryDecode.Invoke(encoded.EventType, encoded.Data)
Codec.Create(encode', tryDecode')
let decode' (encoded : ITimelineEvent<'Format>) = decode.Invoke(encoded.EventType, encoded.Data)
Codec.Create(encode', decode')
6 changes: 3 additions & 3 deletions src/FsCodec/FsCodec.fs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ type IEventCodec<'Event, 'Format, 'Context> =
/// <summary>Encodes a <c>'Event</c> instance into a <c>'Format</c> representation</summary>
abstract Encode: context: 'Context * value : 'Event -> IEventData<'Format>
/// <summary>Decodes a formatted representation into a <c>'Event</c> instance. Returns <c>None</c> on undefined <c>EventType</c>s</summary>
abstract TryDecode: encoded: ITimelineEvent<'Format> -> 'Event voption
abstract Decode: encoded: ITimelineEvent<'Format> -> 'Event voption

namespace FsCodec.Core

Expand Down Expand Up @@ -132,6 +132,6 @@ type EventCodec<'Event, 'Format, 'Context> private () =
let encoded = native.Encode(context, event)
upConvert encoded

member _.TryDecode target =
member _.Decode target =
let encoded = downConvert target
native.TryDecode encoded }
native.Decode encoded }
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ module VerbatimUtf8Tests = // not a module or CI will fail for net461
test <@ res.Contains """"d":{"embed":"\""}""" @>
let des = JsonConvert.DeserializeObject<Batch>(res)
let loaded = FsCodec.Core.TimelineEvent.Create(-1L, des.e[0].c, ReadOnlyMemory des.e[0].d)
let decoded = eventCodec.TryDecode loaded |> ValueOption.get
let decoded = eventCodec.Decode loaded |> ValueOption.get
input =! decoded

let defaultSettings = Options.CreateDefault()
Expand All @@ -79,23 +79,23 @@ module VerbatimUtf8Tests = // not a module or CI will fail for net461
let ser = JsonConvert.SerializeObject(e, defaultSettings)
let des = JsonConvert.DeserializeObject<Batch>(ser, defaultSettings)
let loaded = FsCodec.Core.TimelineEvent.Create(-1L, des.e[0].c, ReadOnlyMemory des.e[0].d)
let decoded = defaultEventCodec.TryDecode loaded |> ValueOption.get
let decoded = defaultEventCodec.Decode loaded |> ValueOption.get
x =! decoded

// https://github.com/JamesNK/Newtonsoft.Json/issues/862 // doesnt apply to this case
let [<Fact>] ``Codec does not fall prey to Date-strings being mutilated`` () =
let x = ES { embed = "2016-03-31T07:02:00+07:00" }
let encoded = defaultEventCodec.Encode((), x)
let adapted = FsCodec.Core.TimelineEvent.Create(-1L, encoded)
let decoded = defaultEventCodec.TryDecode adapted |> ValueOption.get
let decoded = defaultEventCodec.Decode adapted |> ValueOption.get
test <@ x = decoded @>

//// NB while this aspect works, we don't support it as it gets messy when you then use the VerbatimUtf8Converter
//let sEncoder = Codec.Create<US>(defaultSettings)
//let [<Theory; InlineData ""; InlineData null>] ``Codec can roundtrip strings`` (value: string) =
// let x = SS value
// let encoded = sEncoder.Encode x
// let decoded = sEncoder.TryDecode encoded |> Option.get
// let decoded = sEncoder.Decode encoded |> Option.get
// test <@ x = decoded @>

module VerbatimUtf8NullHandling =
Expand Down
4 changes: 2 additions & 2 deletions tests/FsCodec.SystemTextJson.Tests/CodecTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ let [<Property>] roundtrips value =
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 decoded = eventCodec.Decode wrapped |> ValueOption.get
let expected =
match value with
| AO ({ opt = Some null } as v) -> AO { v with opt = None }
Expand All @@ -69,7 +69,7 @@ let [<Property>] roundtrips value =
test <@ expected = decoded @>

// 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
let decodedMultiHop = multiHopCodec.Decode wrapped |> ValueOption.get
test <@ expected = decodedMultiHop @>

let [<Xunit.Fact>] ``EventData.Create basics`` () =
Expand Down
Loading

0 comments on commit 02ec614

Please sign in to comment.