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

Move Equinox.Codec out to FsCodec #156

Merged
merged 13 commits into from
Aug 30, 2019
Prev Previous commit
Next Next commit
renamespace Equinox.Codec.NewtonsoftJson -> Gardelloyd.NewtonsoftJson
  • Loading branch information
bartelink committed Aug 29, 2019
commit 1f5c6038510ecbfa602572a9997596829314a257
6 changes: 3 additions & 3 deletions samples/Infrastructure/Services.fs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ open System

type StreamResolver(storage) =
member __.Resolve
( codec : Equinox.Codec.IUnionEncoder<'event,byte[]>,
( codec : Gardelloyd.IUnionEncoder<'event,byte[]>,
fold: ('state -> 'event seq -> 'state),
initial: 'state,
snapshot: (('event -> bool) * ('state -> 'event))) =
Expand All @@ -21,7 +21,7 @@ type StreamResolver(storage) =
Equinox.Cosmos.Resolver<'event,'state>(store, codec, fold, initial, caching, ?access = accessStrategy).Resolve

type ICodecGen =
abstract Generate<'Union when 'Union :> TypeShape.UnionContract.IUnionContract> : unit -> Equinox.Codec.IUnionEncoder<'Union,byte[]>
abstract Generate<'Union when 'Union :> TypeShape.UnionContract.IUnionContract> : unit -> Gardelloyd.IUnionEncoder<'Union,byte[]>

type ServiceBuilder(storageConfig, handlerLog, codecGen : ICodecGen) =
let resolver = StreamResolver(storageConfig)
Expand Down Expand Up @@ -56,4 +56,4 @@ let register (services : IServiceCollection, storageConfig, handlerLog, codecGen
let serializationSettings = Newtonsoft.Json.Converters.FSharp.Settings.CreateCorrect()
type NewtonsoftJsonCodecGen() =
interface ICodecGen with
member __.Generate() = Equinox.Codec.NewtonsoftJson.Json.Create<'Union>(serializationSettings)
member __.Generate() = Gardelloyd.NewtonsoftJson.Json.Create<'Union>(serializationSettings)
2 changes: 1 addition & 1 deletion samples/Store/Integration/CodecIntegration.fs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ let serializationSettings =
Newtonsoft.Json.Converters.FSharp.OptionConverter() |])

let genCodec<'Union when 'Union :> TypeShape.UnionContract.IUnionContract>() =
Equinox.Codec.NewtonsoftJson.Json.Create<'Union>(serializationSettings)
Gardelloyd.NewtonsoftJson.Json.Create<'Union>(serializationSettings)

type EventWithId = { id : CartId } // where CartId uses FSharp.UMX

Expand Down
2 changes: 1 addition & 1 deletion samples/Store/Integration/EventStoreIntegration.fs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ open Equinox.EventStore
open System

let serializationSettings = Newtonsoft.Json.Converters.FSharp.Settings.CreateCorrect()
let genCodec<'Union when 'Union :> TypeShape.UnionContract.IUnionContract>() = Equinox.Codec.NewtonsoftJson.Json.Create<'Union>(serializationSettings)
let genCodec<'Union when 'Union :> TypeShape.UnionContract.IUnionContract>() = Gardelloyd.NewtonsoftJson.Json.Create<'Union>(serializationSettings)

/// Connect with Gossip based cluster discovery using the default Commercial edition Manager port config
/// Such a config can be simulated on a single node with zero config via the EventStore OSS package:-
Expand Down
2 changes: 1 addition & 1 deletion samples/Tutorial/Cosmos.fsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ module Store =
let cacheStrategy = CachingStrategy.SlidingWindow (cache, TimeSpan.FromMinutes 20.) // OR CachingStrategy.NoCaching

module FavoritesCategory =
let codec = Equinox.Codec.NewtonsoftJson.Json.Create<Favorites.Event>(Newtonsoft.Json.JsonSerializerSettings())
let codec = Gardelloyd.NewtonsoftJson.Json.Create<Favorites.Event>(Newtonsoft.Json.JsonSerializerSettings())
let resolve = Resolver(Store.context, codec, Favorites.fold, Favorites.initial, Store.cacheStrategy).Resolve

let service = Favorites.Service(Log.log, FavoritesCategory.resolve)
Expand Down
2 changes: 1 addition & 1 deletion samples/Tutorial/Todo.fsx
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ module Store =
let cacheStrategy = CachingStrategy.SlidingWindow (cache, TimeSpan.FromMinutes 20.)

module TodosCategory =
let codec = Equinox.Codec.NewtonsoftJson.Json.Create<Event>(Newtonsoft.Json.JsonSerializerSettings())
let codec = Gardelloyd.NewtonsoftJson.Json.Create<Event>(Newtonsoft.Json.JsonSerializerSettings())
let access = AccessStrategy.Snapshot (isOrigin,compact)
let resolve = Resolver(Store.store, codec, fold, initial, Store.cacheStrategy, access=access).Resolve

Expand Down
16 changes: 8 additions & 8 deletions src/Equinox.Cosmos/Cosmos.fs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ open Newtonsoft.Json
open Serilog
open System

type IIndexedEvent = Equinox.Codec.Core.IIndexedEvent<byte[]>
type IIndexedEvent = Gardelloyd.Core.IIndexedEvent<byte[]>

/// A single Domain Event from the array held in a Batch
type [<NoEquality; NoComparison; JsonObject(ItemRequired=Required.Always)>]
Expand Down Expand Up @@ -201,7 +201,7 @@ module Log =
| SyncResync of Measurement
| SyncConflict of Measurement
let prop name value (log : ILogger) = log.ForContext(name, value)
let propData name (events: #Equinox.Codec.IEvent<byte[]> seq) (log : ILogger) =
let propData name (events: #Gardelloyd.IEvent<byte[]> seq) (log : ILogger) =
let items = seq { for e in events do yield sprintf "{\"%s\": %s}" e.EventType (System.Text.Encoding.UTF8.GetString e.Data) }
log.ForContext(name, sprintf "[%s]" (String.concat ",\n\r" items))
let propEvents = propData "events"
Expand All @@ -224,7 +224,7 @@ module Log =
let enrich (e : LogEvent) = e.AddPropertyIfAbsent(LogEventProperty("cosmosEvt", ScalarValue(value)))
log.ForContext({ new Serilog.Core.ILogEventEnricher with member __.Enrich(evt,_) = enrich evt })
let (|BlobLen|) = function null -> 0 | (x : byte[]) -> x.Length
let (|EventLen|) (x: #Equinox.Codec.IEvent<_>) = let (BlobLen bytes), (BlobLen metaBytes) = x.Data, x.Meta in bytes+metaBytes
let (|EventLen|) (x: #Gardelloyd.IEvent<_>) = let (BlobLen bytes), (BlobLen metaBytes) = x.Data, x.Meta in bytes+metaBytes
let (|BatchLen|) = Seq.sumBy (|EventLen|)

/// NB Caveat emptor; this is subject to unlimited change without the major version changing - while the `dotnet-templates` repo will be kept in step, and
Expand Down Expand Up @@ -471,11 +471,11 @@ function sync(req, expIndex, expEtag) {
let batch (log : ILogger) retryPolicy containerStream batch: Async<Result> =
let call = logged containerStream batch
Log.withLoggedRetries retryPolicy "writeAttempt" call log
let mkBatch (stream: string) (events: Equinox.Codec.IEvent<_>[]) unfolds: Tip =
let mkBatch (stream: string) (events: Gardelloyd.IEvent<_>[]) unfolds: Tip =
{ p = stream; id = Tip.WellKnownDocumentId; n = -1L(*Server-managed*); i = -1L(*Server-managed*); _etag = null
e = [| for e in events -> { t = e.Timestamp; c = e.EventType; d = e.Data; m = e.Meta } |]
u = Array.ofSeq unfolds }
let mkUnfold baseIndex (unfolds: Equinox.Codec.IEvent<_> seq) : Unfold seq =
let mkUnfold baseIndex (unfolds: Gardelloyd.IEvent<_> seq) : Unfold seq =
unfolds |> Seq.mapi (fun offset x -> { i = baseIndex + int64 offset; c = x.EventType; d = x.Data; m = x.Meta } : Unfold)

module Initialization =
Expand Down Expand Up @@ -781,7 +781,7 @@ type BatchingPolicy
member __.MaxRequests = maxRequests

type Gateway(conn : Connection, batching : BatchingPolicy) =
let (|FromUnfold|_|) (tryDecode: #Equinox.Codec.IEvent<_> -> 'event option) (isOrigin: 'event -> bool) (xs:#Equinox.Codec.IEvent<_>[]) : Option<'event[]> =
let (|FromUnfold|_|) (tryDecode: #Gardelloyd.IEvent<_> -> 'event option) (isOrigin: 'event -> bool) (xs:#Gardelloyd.IEvent<_>[]) : Option<'event[]> =
match Array.tryFindIndexBack (tryDecode >> Option.exists isOrigin) xs with
| None -> None
| Some index -> xs |> Seq.skip index |> Seq.choose tryDecode |> Array.ofSeq |> Some
Expand Down Expand Up @@ -826,7 +826,7 @@ type Gateway(conn : Connection, batching : BatchingPolicy) =
| Sync.Result.ConflictUnknown pos' -> return InternalSyncResult.ConflictUnknown (Token.create containerStream pos')
| Sync.Result.Written pos' -> return InternalSyncResult.Written (Token.create containerStream pos') }

type private Category<'event, 'state>(gateway : Gateway, codec : Codec.IUnionEncoder<'event, byte[]>) =
type private Category<'event, 'state>(gateway : Gateway, codec : Gardelloyd.IUnionEncoder<'event, byte[]>) =
let (|TryDecodeFold|) (fold: 'state -> 'event seq -> 'state) initial (events: IIndexedEvent seq) : 'state = Seq.choose codec.TryDecode events |> fold initial
member __.Load includeUnfolds containerStream fold initial isOrigin (log : ILogger): Async<Store.StreamToken * 'state> = async {
let! token, events =
Expand Down Expand Up @@ -1135,7 +1135,7 @@ namespace Equinox.Cosmos.Core
open Equinox.Cosmos
open Equinox.Cosmos.Store
open FSharp.Control
open Equinox.Codec // must shadow Control.IEvent
open Gardelloyd // must shadow Control.IEvent
open System.Runtime.InteropServices

/// Outcome of appending events, specifying the new and/or conflicting events, together with the updated Target write position
Expand Down
16 changes: 8 additions & 8 deletions src/Equinox.EventStore/EventStore.fs
Original file line number Diff line number Diff line change
Expand Up @@ -269,16 +269,16 @@ module private Read =
return version, events }

module UnionEncoderAdapters =
let encodedEventOfResolvedEvent (x : ResolvedEvent) : Equinox.Codec.IEvent<byte[]> =
let encodedEventOfResolvedEvent (x : ResolvedEvent) : Gardelloyd.IEvent<byte[]> =
// Inspecting server code shows both Created and CreatedEpoch are set; taking this as it's less ambiguous than DateTime in the general case
let ts = DateTimeOffset.FromUnixTimeMilliseconds(x.Event.CreatedEpoch)
Equinox.Codec.Core.EventData.Create(x.Event.EventType, x.Event.Data, x.Event.Metadata, ts) :> _
let private eventDataOfEncodedEvent (x : Codec.IEvent<byte[]>) =
Gardelloyd.Core.EventData.Create(x.Event.EventType, x.Event.Data, x.Event.Metadata, ts) :> _
let private eventDataOfEncodedEvent (x : Gardelloyd.IEvent<byte[]>) =
EventData(Guid.NewGuid(), x.EventType, (*isJson*) true, x.Data, x.Meta)
let encode (xs : Codec.IEvent<byte[]> []) : EventData[] = Array.map eventDataOfEncodedEvent xs
let encodeEvents (codec : Codec.IUnionEncoder<'event,byte[]>) (xs : 'event seq) : EventData[] =
let encode (xs : Gardelloyd.IEvent<byte[]> []) : EventData[] = Array.map eventDataOfEncodedEvent xs
let encodeEvents (codec : Gardelloyd.IUnionEncoder<'event,byte[]>) (xs : 'event seq) : EventData[] =
xs |> Seq.map (codec.Encode >> eventDataOfEncodedEvent) |> Seq.toArray
let decodeKnownEvents (codec : Codec.IUnionEncoder<'event, byte[]>) (xs : ResolvedEvent[]) : 'event seq =
let decodeKnownEvents (codec : Gardelloyd.IUnionEncoder<'event, byte[]>) (xs : ResolvedEvent[]) : 'event seq =
xs |> Seq.map encodedEventOfResolvedEvent |> Seq.choose codec.TryDecode

type Stream = { name: string }
Expand Down Expand Up @@ -382,7 +382,7 @@ type Context(conn : Connection, batching : BatchingPolicy) =
| Some compactionEventIndex ->
Token.ofPreviousStreamVersionAndCompactionEventDataIndex streamToken compactionEventIndex encodedEvents.Length batching.BatchSize version'
return GatewaySyncResult.Written token }
member __.Sync(log, streamName, streamVersion, events: Codec.IEvent<byte[]>[]) : Async<GatewaySyncResult> = async {
member __.Sync(log, streamName, streamVersion, events: Gardelloyd.IEvent<byte[]>[]) : Async<GatewaySyncResult> = async {
let encodedEvents : EventData[] = UnionEncoderAdapters.encode events
let! wr = Write.writeEvents log conn.WriteRetryPolicy conn.WriteConnection streamName streamVersion encodedEvents
match wr with
Expand All @@ -402,7 +402,7 @@ type private CompactionContext(eventsLen : int, capacityBeforeCompaction : int)
/// Determines whether writing a Compaction event is warranted (based on the existing state and the current `Accumulated` changes)
member __.IsCompactionDue = eventsLen > capacityBeforeCompaction

type private Category<'event, 'state>(context : Context, codec : Codec.IUnionEncoder<'event,byte[]>, ?access : AccessStrategy<'event,'state>) =
type private Category<'event, 'state>(context : Context, codec : Gardelloyd.IUnionEncoder<'event,byte[]>, ?access : AccessStrategy<'event,'state>) =
let tryDecode (e: ResolvedEvent) = e |> UnionEncoderAdapters.encodedEventOfResolvedEvent |> codec.TryDecode
let compactionPredicate =
match access with
Expand Down
20 changes: 10 additions & 10 deletions src/Gardelloyd/Codec.fs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace Equinox.Codec
namespace Gardelloyd

/// Common form for either a Domain Event or an Unfolded Event
type IEvent<'Format> =
Expand All @@ -23,7 +23,7 @@ type IUnionEncoder<'Union, 'Format> =
abstract TryDecode : encoded:IEvent<'Format> -> 'Union option

/// Provides Codecs that render to a UTF-8 array suitable for storage in EventStore or CosmosDb based on explicit functions you supply
/// i.e., with using conventions / Type Shapes / Reflection or specific Json processing libraries - see Equinox.Codec.NewtonsoftJson.Json for batteries-included Coding/Decoding
/// i.e., with using conventions / Type Shapes / Reflection or specific Json processing libraries - see Gardelloyd.NewtonsoftJson.Json for batteries-included Coding/Decoding
type Custom =

/// <summary>
Expand Down Expand Up @@ -57,15 +57,15 @@ type Custom =
let tryDecode' (et,d,_md) = tryDecode (et, d)
Custom.Create(encode', tryDecode')

namespace Equinox.Codec.Core
namespace Gardelloyd.Core

open System

/// Represents a Domain Event or Unfold, together with it's Index in the event sequence
// Included here to enable extraction of this ancillary information (by downcasting IEvent in one's IUnionEncoder.TryDecode implementation)
// in the corner cases where this coupling is absolutely definitely better than all other approaches
type IIndexedEvent<'Format> =
inherit Equinox.Codec.IEvent<'Format>
inherit Gardelloyd.IEvent<'Format>
/// The index into the event sequence of this event
abstract member Index : int64
/// Indicates this is not a Domain Event, but actually an Unfolded Event based on the state inferred from the events up to `Index`
Expand All @@ -74,7 +74,7 @@ type IIndexedEvent<'Format> =
/// An Event about to be written, see IEvent for further information
type EventData<'Format> =
{ eventType : string; data : 'Format; meta : 'Format; timestamp: DateTimeOffset }
interface Equinox.Codec.IEvent<'Format> with
interface Gardelloyd.IEvent<'Format> with
member __.EventType = __.eventType
member __.Data = __.data
member __.Meta = __.meta
Expand All @@ -87,7 +87,7 @@ type EventData =
meta = defaultArg meta null
timestamp = match timestamp with Some ts -> ts | None -> DateTimeOffset.UtcNow }

namespace Equinox.Codec.NewtonsoftJson
namespace Gardelloyd.NewtonsoftJson

open Newtonsoft.Json
open System.IO
Expand Down Expand Up @@ -135,7 +135,7 @@ type BytesEncoder(settings : JsonSerializerSettings) =
serializer.Deserialize<'T>(jsonReader)

/// Provides Codecs that render to a UTF-8 array suitable for storage in EventStore or CosmosDb based on explicit functions you supply using `Newtonsoft.Json` and
/// `TypeShape.UnionContract.UnionContractEncoder` - if you need full control and/or have have your own codecs, see `Equinox.Codec.Custom.Create` instead
/// `TypeShape.UnionContract.UnionContractEncoder` - if you need full control and/or have have your own codecs, see `Gardelloyd.Custom.Create` instead
type Json =

/// <summary>
Expand All @@ -150,15 +150,15 @@ type Json =
static member Create<'Union when 'Union :> TypeShape.UnionContract.IUnionContract>
( settings,
[<Optional;DefaultParameterValue(null)>]?allowNullaryCases)
: Equinox.Codec.IUnionEncoder<'Union,byte[]> =
: Gardelloyd.IUnionEncoder<'Union,byte[]> =
let dataCodec =
TypeShape.UnionContract.UnionContractEncoder.Create<'Union,byte[]>(
new BytesEncoder(settings),
requireRecordFields=true, // See JsonConverterTests - roundtripping UTF-8 correctly with Json.net is painful so for now we lock up the dragons
?allowNullaryCases=allowNullaryCases)
{ new Equinox.Codec.IUnionEncoder<'Union,byte[]> with
{ new Gardelloyd.IUnionEncoder<'Union,byte[]> with
member __.Encode value =
let enc = dataCodec.Encode value
Equinox.Codec.Core.EventData.Create(enc.CaseName, enc.Payload) :> _
Gardelloyd.Core.EventData.Create(enc.CaseName, enc.Payload) :> _
member __.TryDecode encoded =
dataCodec.TryDecode { CaseName = encoded.EventType; Payload = encoded.Data } }
4 changes: 2 additions & 2 deletions tests/Equinox.Cosmos.Integration/CosmosCoreIntegration.fs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
open Equinox.Cosmos.Core
open Equinox.Cosmos.Integration.Infrastructure
open FSharp.Control
open Equinox.Codec // Shadow FSharp.Control.IEvent
open Equinox.Codec.Core
open Gardelloyd // Shadow FSharp.Control.IEvent
open Gardelloyd.Core
open Newtonsoft.Json.Linq
open Swensen.Unquote
open Serilog
Expand Down
2 changes: 1 addition & 1 deletion tests/Equinox.Cosmos.Integration/CosmosIntegration.fs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ open System

let serializationSettings = JsonSerializerSettings()
let genCodec<'Union when 'Union :> TypeShape.UnionContract.IUnionContract>() =
Equinox.Codec.NewtonsoftJson.Json.Create<'Union>(serializationSettings)
Gardelloyd.NewtonsoftJson.Json.Create<'Union>(serializationSettings)

module Cart =
let fold, initial = Domain.Cart.Folds.fold, Domain.Cart.Folds.initial
Expand Down
14 changes: 7 additions & 7 deletions tests/Equinox.Cosmos.Integration/JsonConverterTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ type Union =
interface TypeShape.UnionContract.IUnionContract

let defaultSettings = JsonSerializerSettings()
let mkUnionEncoder () = Equinox.Codec.NewtonsoftJson.Json.Create<Union>(defaultSettings)
let mkUnionEncoder () = Gardelloyd.NewtonsoftJson.Json.Create<Union>(defaultSettings)

type EmbeddedString = { embed : string }
type EmbeddedDate = { embed : DateTime }
Expand Down Expand Up @@ -47,7 +47,7 @@ type VerbatimUtf8Tests() =
let res = JsonConvert.SerializeObject(e)
test <@ res.Contains """"d":{"embed":"\""}""" @>

let uEncoder = Equinox.Codec.NewtonsoftJson.Json.Create<U>(defaultSettings)
let uEncoder = Gardelloyd.NewtonsoftJson.Json.Create<U>(defaultSettings)

let [<Property(MaxTest=100)>] ``roundtrips diverse bodies correctly`` (x: U) =
let encoded = uEncoder.Encode x
Expand All @@ -56,21 +56,21 @@ type VerbatimUtf8Tests() =
e = [| { t = DateTimeOffset.MinValue; c = encoded.EventType; d = encoded.Data; m = null } |] }
let ser = JsonConvert.SerializeObject(e, defaultSettings)
let des = JsonConvert.DeserializeObject<Store.Batch>(ser, defaultSettings)
let loaded = Equinox.Codec.Core.EventData.Create(des.e.[0].c,des.e.[0].d)
let loaded = Gardelloyd.Core.EventData.Create(des.e.[0].c,des.e.[0].d)
let decoded = uEncoder.TryDecode loaded |> Option.get
x =! decoded

// NB while this aspect works, we don't support it as it gets messy when you then use the VerbatimUtf8Converter
// https://github.com/JamesNK/Newtonsoft.Json/issues/862 // doesnt apply to this case
let [<Fact>] ``Equinox.Codec.NewtonsoftJson.Json does not fall prey to Date-strings being mutilated`` () =
let [<Fact>] ``Gardelloyd.NewtonsoftJson.Json does not fall prey to Date-strings being mutilated`` () =
let x = ES { embed = "2016-03-31T07:02:00+07:00" }
let encoded = uEncoder.Encode x
let decoded = uEncoder.TryDecode encoded |> Option.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 = Equinox.Codec.NewtonsoftJson.Json.Create<US>(defaultSettings)
//let [<Theory; InlineData ""; InlineData null>] ``Equinox.Codec.NewtonsoftJson.Json can roundtrip strings`` (value: string) =
//let sEncoder = Gardelloyd.NewtonsoftJson.Json.Create<US>(defaultSettings)
//let [<Theory; InlineData ""; InlineData null>] ``Gardelloyd.NewtonsoftJson.Json can roundtrip strings`` (value: string) =
// let x = SS value
// let encoded = sEncoder.Encode x
// let decoded = sEncoder.TryDecode encoded |> Option.get
Expand Down Expand Up @@ -131,6 +131,6 @@ type Base64ZipUtf8Tests() =
let ser = JsonConvert.SerializeObject(e)
test <@ ser.Contains("\"d\":\"") @>
let des = JsonConvert.DeserializeObject<Store.Unfold>(ser)
let d = Equinox.Codec.Core.EventData.Create(des.c, des.d)
let d = Gardelloyd.Core.EventData.Create(des.c, des.d)
let decoded = unionEncoder.TryDecode d |> Option.get
test <@ value = decoded @>
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ let createGesGateway connection batchSize = Context(connection, BatchingPolicy(m

let serializationSettings = JsonSerializerSettings()
let genCodec<'Union when 'Union :> TypeShape.UnionContract.IUnionContract>() =
Equinox.Codec.NewtonsoftJson.Json.Create<'Union>(serializationSettings)
Gardelloyd.NewtonsoftJson.Json.Create<'Union>(serializationSettings)

module Cart =
let fold, initial = Domain.Cart.Folds.fold, Domain.Cart.Folds.initial
Expand Down