Skip to content

Commit

Permalink
Tests + guard re non record unions fixes #103
Browse files Browse the repository at this point in the history
  • Loading branch information
bartelink committed Mar 6, 2019
1 parent 35879a9 commit 606399b
Show file tree
Hide file tree
Showing 11 changed files with 156 additions and 63 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ _NB at the present time, this project does not adhere strictly to Semantic Versi
### Fixed

- Add Writing empty event list guard for `Equinox.Cosmos` [#105](https://github.com/jet/equinox/issues/105)
- Disabled support for non-record Event payloads in `Equinox.Codec` [#103](https://github.com/jet/equinox/issues/103)

<a name="1.1.0-preview2"></a>
## [1.1.0-preview2] - 2019-02-20
Expand Down
9 changes: 5 additions & 4 deletions samples/Store/Integration/CodecIntegration.fs
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,9 @@ type SimpleDu =
| EventB of EventWithOption
| EventC of EventWithUnion
| EventD
| EventE of int
| EventF of string
// See JsonConverterTests for why these are ruled out atm
//| EventE of int // works but disabled due to Strings and DateTimes not working
//| EventF of string // has wierd semantics, particularly when used with a VerbatimJsonConverter in Equinox.Cosmos
interface IUnionContract

let render = function
Expand All @@ -43,8 +44,8 @@ let render = function
| EventC { value = S { maybeI = None } } -> sprintf """{"value":{"case":"S"}}"""
| EventC { value = S { maybeI = Some i } } -> sprintf """{"value":{"case":"S","maybeI":%d}}""" i
| EventD -> null
| EventE i -> string i
| EventF s -> Newtonsoft.Json.JsonConvert.SerializeObject s
//| EventE i -> string i
//| EventF s -> Newtonsoft.Json.JsonConvert.SerializeObject s

let codec = genCodec<SimpleDu>()

Expand Down
14 changes: 8 additions & 6 deletions samples/TodoBackend/Todo.fs
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ open Domain
[<AutoOpen>]
module Events =
type Todo = { id: int; order: int; title: string; completed: bool }
type DeletedInfo = { id: int }
type CompactedInfo = { items: Todo[] }
type Event =
| Added of Todo
| Updated of Todo
| Deleted of int
| Deleted of DeletedInfo
| Cleared
| Compacted of Todo[]
| Compacted of CompactedInfo
interface TypeShape.UnionContract.IUnionContract

module Folds =
Expand All @@ -21,12 +23,12 @@ module Folds =
match e with
| Added item -> { s with items = item :: s.items; nextId = s.nextId + 1 }
| Updated value -> { s with items = s.items |> List.map (function { id = id } when id = value.id -> value | item -> item) }
| Deleted id -> { s with items = s.items |> List.filter (fun x -> x.id <> id) }
| Deleted { id=id } -> { s with items = s.items |> List.filter (fun x -> x.id <> id) }
| Cleared -> { s with items = [] }
| Compacted items -> { s with items = List.ofArray items }
| Compacted { items = items } -> { s with items = List.ofArray items }
let fold state = Seq.fold evolve state
let isOrigin = function Events.Cleared | Events.Compacted _ -> true | _ -> false
let compact state = Compacted (Array.ofList state.items)
let compact state = Compacted { items = Array.ofList state.items }

type Command = Add of Todo | Update of Todo | Delete of id: int | Clear

Expand All @@ -38,7 +40,7 @@ module Commands =
match state.items |> List.tryFind (function { id = id } -> id = value.id) with
| Some current when current <> value -> [Updated value]
| _ -> []
| Delete id -> if state.items |> List.exists (fun x -> x.id = id) then [Deleted id] else []
| Delete id -> if state.items |> List.exists (fun x -> x.id = id) then [Deleted {id=id}] else []
| Clear -> if state.items |> List.isEmpty then [] else [Cleared]

type Service(log, resolveStream, ?maxAttempts) =
Expand Down
62 changes: 47 additions & 15 deletions samples/Tutorial/Cosmos.fsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
// Compile Tutorial.fsproj by either a) right-clicking or b) typing
// dotnet build samples/Tutorial before attempting to send this to FSI with Alt-Enter
#r "netstandard"
#r "bin/Debug/netstandard2.0/Serilog.dll"
#r "bin/Debug/netstandard2.0/Serilog.Sinks.Console.dll"
#r "bin/Debug/netstandard2.0/Newtonsoft.Json.dll"
#r "bin/Debug/netstandard2.0/TypeShape.dll"
#r "bin/Debug/netstandard2.0/Equinox.dll"
#r "bin/Debug/netstandard2.0/Equinox.Codec.dll"
#r "bin/Debug/netstandard2.0/FSharp.Control.AsyncSeq.dll"
#r "bin/Debug/netstandard2.0/Microsoft.Azure.DocumentDb.Core.dll"
#r "bin/Debug/netstandard2.0/Equinox.Cosmos.dll"
#I "bin/Debug/netstandard2.0/"
#r "Serilog.dll"
#r "Serilog.Sinks.Console.dll"
#r "Newtonsoft.Json.dll"
#r "TypeShape.dll"
#r "Equinox.dll"
#r "Equinox.Codec.dll"
#r "FSharp.Control.AsyncSeq.dll"
#r "Microsoft.Azure.DocumentDb.Core.dll"
#r "System.Net.Http"
#r "Serilog.Sinks.Seq.dll"
#r "Equinox.Cosmos.dll"
#load "../Infrastructure/Log.fs"

open Equinox.Cosmos
open System
Expand All @@ -31,7 +35,7 @@ module Favorites =
let interpret command state =
match command with
| Add sku -> if state |> List.contains sku then [] else [Added sku]
| Remove sku -> if state |> List.contains sku |> not then [] else [Removed sku]
| Remove sku -> if state |> List.contains sku then [Removed sku] else []

type Service(log, resolveStream, ?maxAttempts) =
let (|AggregateId|) clientId = Equinox.AggregateId("Favorites", clientId)
Expand All @@ -46,7 +50,33 @@ module Favorites =

module Log =
open Serilog
let log = LoggerConfiguration().WriteTo.Console().CreateLogger()
open Serilog.Events
open Samples.Infrastructure.Log
let verbose = true // false will remove lots of noise
let log =
let c = LoggerConfiguration()
let c = if verbose then c.MinimumLevel.Debug() else c
let c = c.WriteTo.Sink(RuCounterSink())
let c = c.WriteTo.Seq("http://localhost:5341") // https://getseq.net
let c = c.WriteTo.Console(if verbose then LogEventLevel.Debug else LogEventLevel.Information)
c.CreateLogger()

let dumpStats () =
let stats =
[ "Read", RuCounterSink.Read
"Write", RuCounterSink.Write
"Resync", RuCounterSink.Resync ]
let mutable totalCount, totalRc, totalMs = 0L, 0., 0L
let logActivity name count rc lat =
log.Information("{name}: {count:n0} requests costing {ru:n0} RU (average: {avg:n2}); Average latency: {lat:n0}ms",
name, count, rc, (if count = 0L then Double.NaN else rc/float count), (if count = 0L then Double.NaN else float lat/float count))
for name, stat in stats do
let ru = float stat.rux100 / 100.
totalCount <- totalCount + stat.count
totalRc <- totalRc + ru
totalMs <- totalMs + stat.ms
logActivity name stat.count ru stat.ms
logActivity "TOTAL" totalCount totalRc totalMs

module Store =
let read key = System.Environment.GetEnvironmentVariable key |> Option.ofObj |> Option.get
Expand All @@ -57,18 +87,20 @@ module Store =

let store = CosmosStore(gateway, read "EQUINOX_COSMOS_DATABASE", read "EQUINOX_COSMOS_COLLECTION")
let cache = Caching.Cache("equinox-tutorial", 20)
let cacheStrategy = CachingStrategy.SlidingWindow (cache, TimeSpan.FromMinutes 20.)
let cacheStrategy = CachingStrategy.SlidingWindow (cache, TimeSpan.FromMinutes 20.) // OR CachingStrategy.NoCaching

module FavoritesCategory =
let codec = Equinox.UnionCodec.JsonUtf8.Create<Favorites.Event>(Newtonsoft.Json.JsonSerializerSettings())
let codec = Equinox.Codec.JsonNet.JsonUtf8.Create<Favorites.Event>(Newtonsoft.Json.JsonSerializerSettings())
let resolve = CosmosResolver(Store.store, codec, Favorites.fold, Favorites.initial, Store.cacheStrategy).Resolve

let service = Favorites.Service(Log.log, FavoritesCategory.resolve)

let client = "ClientE"
let client = "ClientJ"
service.Favorite(client, "a") |> Async.RunSynchronously
service.Favorite(client, "b") |> Async.RunSynchronously
service.List(client) |> Async.RunSynchronously

service.Unfavorite(client, "b") |> Async.RunSynchronously
service.List(client) |> Async.RunSynchronously
service.List(client) |> Async.RunSynchronously

Log.dumpStats()
11 changes: 6 additions & 5 deletions samples/Tutorial/Counter.fsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
// Compile Tutorial.fsproj by either a) right-clicking or b) typing
// dotnet build samples/Tutorial before attempting to send this to FSI with Alt-Enter
#r "bin/Debug/netstandard2.0/Serilog.dll"
#r "bin/Debug/netstandard2.0/Serilog.Sinks.Console.dll"
#r "bin/Debug/netstandard2.0/Equinox.dll"
#r "bin/Debug/netstandard2.0/Equinox.MemoryStore.dll"
#I "bin/Debug/netstandard2.0/"
#r "Serilog.dll"
#r "Serilog.Sinks.Console.dll"
#r "Equinox.dll"
#r "Equinox.MemoryStore.dll"

// Contributed by @voronoipotato

Expand All @@ -13,7 +14,7 @@
type Event =
| Incremented
| Decremented
| Cleared of int
| Cleared of int // NOTE int payload will need to be wrapped in a record if using .Cosmos and/or .EventSore
(* A counter going up might clear to 0,
but a counter going down might clear to 100. *)

Expand Down
14 changes: 9 additions & 5 deletions samples/Tutorial/Favorites.fsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
// Compile Tutorial.fsproj by either a) right-clicking or b) typing
// dotnet build samples/Tutorial before attempting to send this to FSI with Alt-Enter
#r "bin/Debug/netstandard2.0/Serilog.dll"
#r "bin/Debug/netstandard2.0/Serilog.Sinks.Console.dll"
#r "bin/Debug/netstandard2.0/Equinox.dll"
#r "bin/Debug/netstandard2.0/Equinox.MemoryStore.dll"
#I "bin/Debug/netstandard2.0/"
#r "Serilog.dll"
#r "Serilog.Sinks.Console.dll"
#r "Equinox.dll"
#r "Equinox.MemoryStore.dll"

(*
* EVENTS
*)

(* Define the events that will be saved in the Stream *)

// Note using strings and DateTimes etc as Event payloads is not supported for .Cosmos or .EventStore using the UnionCodec support
// i.e. typically records are used for the Event Payloads even in cases where you feel you'll only ever have a single primitive value

type Event =
| Added of string
| Added of string
| Removed of string

let initial : string list = []
Expand Down
36 changes: 19 additions & 17 deletions samples/Tutorial/Todo.fsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
// Compile Tutorial.fsproj by either a) right-clicking or b) typing
// dotnet build samples/Tutorial before attempting to send this to FSI with Alt-Enter
#r "netstandard"
#r "bin/Debug/netstandard2.0/Serilog.dll"
#r "bin/Debug/netstandard2.0/Serilog.Sinks.Console.dll"
#r "bin/Debug/netstandard2.0/Newtonsoft.Json.dll"
#r "bin/Debug/netstandard2.0/TypeShape.dll"
#r "bin/Debug/netstandard2.0/Equinox.dll"
#r "bin/Debug/netstandard2.0/Equinox.Codec.dll"
#r "bin/Debug/netstandard2.0/FSharp.Control.AsyncSeq.dll"
#r "bin/Debug/netstandard2.0/Microsoft.Azure.DocumentDb.Core.dll"
#r "bin/Debug/netstandard2.0/Equinox.Cosmos.dll"
#I "bin/Debug/netstandard2.0/"
#r "Serilog.dll"
#r "Serilog.Sinks.Console.dll"
#r "Newtonsoft.Json.dll"
#r "TypeShape.dll"
#r "Equinox.dll"
#r "Equinox.Codec.dll"
#r "FSharp.Control.AsyncSeq.dll"
#r "Microsoft.Azure.DocumentDb.Core.dll"
#r "Equinox.Cosmos.dll"

open Equinox.Cosmos
open System
Expand All @@ -18,27 +19,28 @@ open System
This tutorial stresses different aspects *)

type Todo = { id: int; order: int; title: string; completed: bool }
type DeletedInfo = { id: int }
type CompactedInfo = { items: Todo[] }
type Event =
| Added of Todo
| Updated of Todo
| Deleted of int
| Deleted of DeletedInfo
| Cleared
| Compacted of Todo[]
| Compacted of CompactedInfo
interface TypeShape.UnionContract.IUnionContract

type State = { items : Todo list; nextId : int }
let initial = { items = []; nextId = 0 }
let evolve s (e : Event) =
printfn "%A" e
match e with
| Added item -> { s with items = item :: s.items; nextId = s.nextId + 1 }
| Updated value -> { s with items = s.items |> List.map (function { id = id } when id = value.id -> value | item -> item) }
| Deleted id -> { s with items = s.items |> List.filter (fun x -> x.id <> id) }
| Deleted { id=id } -> { s with items = s.items |> List.filter (fun x -> x.id <> id) }
| Cleared -> { s with items = [] }
| Compacted items -> { s with items = List.ofArray items }
| Compacted { items=items } -> { s with items = List.ofArray items }
let fold state = Seq.fold evolve state
let isOrigin = function Cleared | Compacted _ -> true | _ -> false
let compact state = Compacted (Array.ofList state.items)
let compact state = Compacted { items = Array.ofList state.items }

type Command = Add of Todo | Update of Todo | Delete of id: int | Clear
let interpret c (state : State) =
Expand All @@ -48,7 +50,7 @@ let interpret c (state : State) =
match state.items |> List.tryFind (function { id = id } -> id = value.id) with
| Some current when current <> value -> [Updated value]
| _ -> []
| Delete id -> if state.items |> List.exists (fun x -> x.id = id) then [Deleted id] else []
| Delete id -> if state.items |> List.exists (fun x -> x.id = id) then [Deleted { id=id }] else []
| Clear -> if state.items |> List.isEmpty then [] else [Cleared]

type Service(log, resolveStream, ?maxAttempts) =
Expand Down Expand Up @@ -112,7 +114,7 @@ module Store =
let cacheStrategy = CachingStrategy.SlidingWindow (cache, TimeSpan.FromMinutes 20.)

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

Expand Down
1 change: 1 addition & 0 deletions samples/Tutorial/Tutorial.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

<ItemGroup>
<PackageReference Include="Serilog.Sinks.Console" Version="3.1.1" />
<PackageReference Include="Serilog.Sinks.Seq" Version="4.0.0" />
</ItemGroup>

</Project>
4 changes: 1 addition & 3 deletions src/Equinox.Codec/Codec.fs
Original file line number Diff line number Diff line change
Expand Up @@ -103,17 +103,15 @@ type JsonUtf8 =
/// The Union must be tagged with `interface TypeShape.UnionContract.IUnionContract` to signify this scheme applies.
/// See https://github.com/eiriktsarpalis/TypeShape/blob/master/tests/TypeShape.Tests/UnionContractTests.fs for example usage.</summary>
/// <param name="settings">Configuration to be used by the underlying <c>Newtonsoft.Json</c> Serializer when encoding/decoding.</param>
/// <param name="requireRecordFields">Fail encoder generation if union cases contain fields that are not F# records. Defaults to <c>false</c>.</param>
/// <param name="allowNullaryCases">Fail encoder generation if union contains nullary cases. Defaults to <c>true</c>.</param>
static member Create<'Union when 'Union :> TypeShape.UnionContract.IUnionContract>
( settings,
[<Optional;DefaultParameterValue(null)>]?requireRecordFields,
[<Optional;DefaultParameterValue(null)>]?allowNullaryCases)
: Equinox.Codec.IUnionEncoder<'Union,byte[]> =
let dataCodec =
TypeShape.UnionContract.UnionContractEncoder.Create<'Union,byte[]>(
new Utf8BytesUnionEncoder(settings),
?requireRecordFields=requireRecordFields,
requireRecordFields=true, // See JsonConverterTests - roundtripping correctly to UTF-8 with Json.net is painful so for now we lock up the dragons
?allowNullaryCases=allowNullaryCases)
{ new Equinox.Codec.IUnionEncoder<'Union,byte[]> with
member __.Encode value =
Expand Down
16 changes: 9 additions & 7 deletions src/Equinox.Cosmos/CosmosInternalJson.fs
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,21 @@ open Newtonsoft.Json
/// Manages injecting prepared json into the data being submitted to DocDb as-is, on the basis we can trust it to be valid json as DocDb will need it to be
type VerbatimUtf8JsonConverter() =
inherit JsonConverter()


static let enc = System.Text.Encoding.UTF8

override __.ReadJson(reader, _, _, _) =
let token = JToken.Load(reader)
if token.Type = JTokenType.Object then token.ToString() |> System.Text.Encoding.UTF8.GetBytes |> box
else Array.empty<byte> |> box
let token = JToken.Load reader
if token = null then null
else token |> string |> enc.GetBytes |> box

override __.CanConvert(objectType) =
typeof<byte[]>.Equals(objectType)

override __.WriteJson(writer, value, serializer) =
let array = value :?> byte[]
if array = null || Array.length array = 0 then serializer.Serialize(writer, null)
else writer.WriteRawValue(System.Text.Encoding.UTF8.GetString(array))
if array = null then serializer.Serialize(writer, null)
else writer.WriteRawValue(enc.GetString(array))

open System.IO
open System.IO.Compression
Expand Down Expand Up @@ -52,4 +54,4 @@ type Base64ZipUtf8JsonConverter() =
serializer.Deserialize(reader, typedefof<string>) :?> string |> unpickle |> box
override __.WriteJson(writer, value, serializer) =
let pickled = value |> unbox |> pickle
serializer.Serialize(writer, pickled)
serializer.Serialize(writer, pickled)
Loading

0 comments on commit 606399b

Please sign in to comment.