Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add BoxCodec #25

Merged
merged 5 commits into from
Oct 26, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ The `Unreleased` section name is replaced by the expected version of next releas
## [Unreleased]

### Added

- `FsCodec.Box.Codec.Create`: an API equivalent substitute for `FsCodec.NewtonsoftJson.Codec.Create` for use in unit and integration tests [#25](https:/jet/FsCodec/pull/25)

### Changed
### Removed
### Fixed
Expand Down
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,21 @@ des<Message2> """{"name":null,"outcome":"Discomfort"}"""
// val it : Message = {name = None; outcome = Other;}
```

<a name="boxcodec"></a>
# Features: `FsCodec.Box.Codec`

`FsCodec.Box.Codec` is a drop-in-equivalent for `FsCodec.NewtonsoftJson.Codec` with equivalent `.Create` overloads that encode as `ITimelineEvent<obj>` (as opposed to `ITimelineEvent<byte[]>`.

This is useful when storing events in a `MemoryStore` as it allows one to take the perf cost and ancillary yak shaving induced by round-tripping arbitrary event payloads to the concrete serialization format out of the picture when writing property based unit and integration tests.

NOTE this does not imply one should avoid testing this aspect; the opposite in fact -- one should apply the [Test Pyramid principles](https://martinfowler.com/articles/practical-test-pyramid.html):
- have a focused series of tests that validate that the various data representations in the event bodies are round-trippable
a. in the chosen encoding format (i.e. UTF8 json)
b. with the selected concrete json encoder (i.e. `Newtonsoft.Json` for now 🙁)
- integration tests can in general use `BoxEncoder` and `MemoryStore`

_You should absolutely have acceptance tests that apply the actual serialization encoding with the real store for a representative number of scenarios at the top of the pyramid_

## CONTRIBUTING

The intention is to keep this set of converters minimal and interoperable, e.g., many candidates are deliberately being excluded from this set; _its definitely a non-goal for this to become a compendium of every possible converter_. **So, especially in this repo, the bar for adding converters will be exceedingly high and hence any contribution should definitely be preceded by a discussion.**
Expand Down
101 changes: 101 additions & 0 deletions src/FsCodec.NewtonsoftJson/BoxCodec.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/// Fork of FsCodec.NewtonsoftJson.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
namespace FsCodec.Box

open System
open System.Runtime.InteropServices

/// Provides Codecs that extract the Event bodies from a Union, using the conventions implied by using <c>TypeShape.UnionContract.UnionContractEncoder</c>
/// If you need full control and/or have have your own codecs, see <c>FsCodec.Codec.Create</c> instead
/// See <a href=""https:/eiriktsarpalis/TypeShape/blob/master/tests/TypeShape.Tests/UnionContractTests.fs"></a> for example usage.
type Codec private () =

/// Generate a <code>IUnionEncoder</code> Codec that roundtrips events by holding the boxed form of the Event body.
/// Uses <c>up</c> and <c>down</c> functions to facilitate upconversion/downconversion
/// and/or surfacing metadata to the Programming Model by including it in the emitted <c>'Union</c>
/// The Event Type Names are inferred based on either explicit <c>DataMember(Name=</c> Attributes, or (if unspecified) the Discriminated Union Case Name
/// <c>Contract</c> must be tagged with </c>interface TypeShape.UnionContract.IUnionContract</c> to signify this scheme applies.
static member Create<'Union,'Contract,'Meta,'Context when 'Contract :> TypeShape.UnionContract.IUnionContract>
( /// 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>'Union</c> representation (typically a Discriminated Union) that is to be presented to the programming model.
up : FsCodec.ITimelineEvent<obj> * 'Contract -> 'Union,
/// Maps a fresh Event resulting from a Decision in the Domain representation type down to the TypeShape <c>UnionConverter</c> <c>'Contract</c>
/// The function is also expected to derive a <c>meta</c> object that will be held alongside the data (if it's not <c>None</c>)
/// together with its <c>correlationId</c>, <c>causationId</c> and an Event Creation <c>timestamp</c> (defaults to <c>UtcNow</c>).
down : 'Context option * 'Union -> 'Contract * 'Meta option * string * string * DateTimeOffset option,
bartelink marked this conversation as resolved.
Show resolved Hide resolved
/// Enables one to fail encoder generation if union contains nullary cases. Defaults to <c>false</c>, i.e. permitting them
[<Optional;DefaultParameterValue(null)>]?rejectNullaryCases)
: FsCodec.IUnionEncoder<'Union,obj,'Context> =
let boxEncoder : TypeShape.UnionContract.IEncoder<obj> = new TypeShape.UnionContract.BoxEncoder() :> _
let dataCodec =
TypeShape.UnionContract.UnionContractEncoder.Create<'Contract,obj>(
boxEncoder,
requireRecordFields=true,
allowNullaryCases=not (defaultArg rejectNullaryCases false))
{ new FsCodec.IUnionEncoder<'Union,obj,'Context> with
member __.Encode(context,u) =
let (c, meta : 'Meta option, correlationId, causationId, timestamp : DateTimeOffset option) = down (context,u)
let enc = dataCodec.Encode c
let meta = meta |> Option.map boxEncoder.Encode<'Meta>
FsCodec.Core.EventData.Create(enc.CaseName, enc.Payload, defaultArg meta null, correlationId, causationId, ?timestamp=timestamp) :> _
member __.TryDecode encoded =
let cOption = dataCodec.TryDecode { CaseName = encoded.EventType; Payload = encoded.Data }
match cOption with None -> None | Some contract -> let union = up (encoded,contract) in Some union }

/// Generate a <code>IUnionEncoder</code> Codec that roundtrips events by holding the boxed form of the Event body.
/// Uses <c>up</c> and <c>down</c> and <c>mapCausation</c> functions to facilitate upconversion/downconversion and correlation/causationId mapping
/// and/or surfacing metadata to the Programming Model by including it in the emitted <c>'Union</c>
/// The Event Type Names are inferred based on either explicit <c>DataMember(Name=</c> Attributes, or (if unspecified) the Discriminated Union Case Name
/// <c>Contract</c> must be tagged with </c>interface TypeShape.UnionContract.IUnionContract</c> to signify this scheme applies.
static member Create<'Union,'Contract,'Meta,'Context when 'Contract :> TypeShape.UnionContract.IUnionContract>
( /// 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 representation (typically a Discriminated Union) that is to be presented to the programming model.
up : FsCodec.ITimelineEvent<obj> * 'Contract -> 'Union,
/// Maps a fresh Event resulting from a Decision in the Domain representation type down to the TypeShape <c>UnionConverter</c> <c>'Contract</c>
/// The function is also expected to derive
/// a <c>meta</c> object that will be serialized with the same settings (if it's not <c>None</c>)
/// and an Event Creation <c>timestamp</c>.
down : 'Union -> 'Contract * 'Meta option * DateTimeOffset option,
/// 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 : 'Context option * 'Meta option -> 'Meta option * string * string,
/// Enables one to fail encoder generation if union contains nullary cases. Defaults to <c>false</c>, i.e. permitting them
[<Optional;DefaultParameterValue(null)>]?rejectNullaryCases)
: FsCodec.IUnionEncoder<'Union,obj,'Context> =
let down (context,union) =
let c, m, t = down union
let m', correlationId, causationId = mapCausation (context,m)
c, m', correlationId, causationId, t
Codec.Create(up=up, down=down, ?rejectNullaryCases=rejectNullaryCases)

/// Generate a <code>IUnionEncoder</code> Codec that roundtrips events by holding the boxed form of the Event body.
/// Uses <c>up</c> and <c>down</c> and <c>mapCausation</c> functions to facilitate upconversion/downconversion and correlation/causationId mapping
/// and/or surfacing metadata to the Programming Model by including it in the emitted <c>'Union</c>
/// The Event Type Names are inferred based on either explicit <c>DataMember(Name=</c> Attributes, or (if unspecified) the Discriminated Union Case Name
/// <c>Contract</c> must be tagged with </c>interface TypeShape.UnionContract.IUnionContract</c> to signify this scheme applies.
static member Create<'Union,'Contract,'Meta when 'Contract :> TypeShape.UnionContract.IUnionContract>
( /// 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 representation (typically a Discriminated Union) that is to be presented to the programming model.
up : FsCodec.ITimelineEvent<obj> * 'Contract -> 'Union,
/// Maps a fresh Event resulting from a Decision in the Domain representation type down to the TypeShape <c>UnionConverter</c> <c>'Contract</c>
/// The function is also expected to derive
/// a <c>meta</c> object that will be serialized with the same settings (if it's not <c>None</c>)
/// and an Event Creation <c>timestamp</c>.
down : 'Union -> 'Contract * 'Meta option * DateTimeOffset option,
/// Enables one to fail encoder generation if union contains nullary cases. Defaults to <c>false</c>, i.e. permitting them
[<Optional;DefaultParameterValue(null)>]?rejectNullaryCases)
: FsCodec.IUnionEncoder<'Union,obj,obj> =
let mapCausation (_context : obj, m : ' Meta option) = m,null,null
Codec.Create(up=up, down=down, mapCausation=mapCausation, ?rejectNullaryCases=rejectNullaryCases)

/// Generate a <code>IUnionEncoder</code> Codec that roundtrips events by holding the boxed form of the Event body.
/// The Event Type Names are inferred based on either explicit <c>DataMember(Name=</c> Attributes, or (if unspecified) the Discriminated Union Case Name
/// <c>'Union</c> must be tagged with <c>interface TypeShape.UnionContract.IUnionContract</c> to signify this scheme applies.
static member Create<'Union when 'Union :> TypeShape.UnionContract.IUnionContract>
( /// Enables one to fail encoder generation if union contains nullary cases. Defaults to <c>false</c>, i.e. permitting them
[<Optional;DefaultParameterValue(null)>]?rejectNullaryCases)
: FsCodec.IUnionEncoder<'Union, obj, obj> =
let up : FsCodec.ITimelineEvent<_> * 'Union -> 'Union = snd
let down (u : 'Union) = u, None, None
Codec.Create(up=up, down=down, ?rejectNullaryCases=rejectNullaryCases)
1 change: 1 addition & 0 deletions src/FsCodec.NewtonsoftJson/FsCodec.NewtonsoftJson.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
<Compile Include="OptionConverter.fs" />
<Compile Include="Settings.fs" />
<Compile Include="Codec.fs" />
<Compile Include="BoxCodec.fs" />
<Compile Include="Serdes.fs" />
<Compile Include="VerbatimUtf8Converter.fs" />
</ItemGroup>
Expand Down