Skip to content

Commit

Permalink
Implement Todo Handler and related wiring
Browse files Browse the repository at this point in the history
  • Loading branch information
bartelink committed Jan 4, 2019
1 parent 0c9e66a commit 310f565
Show file tree
Hide file tree
Showing 2 changed files with 34 additions and 54 deletions.
28 changes: 21 additions & 7 deletions equinox-web-csharp/Domain/Infrastructure.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,31 @@ public static void Execute<TEvent, TState>(this Context<TEvent, TState> that,

public class EquinoxHandler<TEvent, TState> : Handler<TEvent, TState>
{
public EquinoxHandler(Func<TState, IEnumerable<TEvent>, TState> fold, ILogger log,
IStream<TEvent, TState> stream)
: base(FuncConvert.FromFunc<TState, FSharpList<TEvent>, TState>(fold), log, stream, 3, null, null)
public EquinoxHandler
( Func<TState, IEnumerable<TEvent>, TState> fold,
ILogger log,
IStream<TEvent, TState> stream,
int maxAttempts = 3)
: base(FuncConvert.FromFunc<TState, FSharpList<TEvent>, TState>(fold),
log,
stream,
maxAttempts,
null,
null)
{
}

public async Task<Unit> Decide(Action<Context<TEvent, TState>> f) =>
await FSharpAsync.StartAsTask(Decide(FuncConvert.ToFSharpFunc(f)), null, null);
// Run the decision method, letting it decide whether or not the Command's intent should manifest as Events
public async Task<Unit> Decide(Action<Context<TEvent, TState>> decide) =>
await FSharpAsync.StartAsTask(Decide(FuncConvert.ToFSharpFunc(decide)), null, null);

public async Task<T> Query<T>(Func<TState, T> projection) =>
await FSharpAsync.StartAsTask(Query(FuncConvert.FromFunc(projection)), null, null);
// Execute a command, as Decide(Action) does, but also yield an outcome from the decision
public async Task<T> Decide<T>(Func<Context<TEvent, TState>,T> interpret) =>
await FSharpAsync.StartAsTask<T>(Decide(FuncConvert.FromFunc(interpret)), null, null);

// Project from the synchronized state, without the possibility of adding events that Decide(Func) admits
public async Task<T> Query<T>(Func<TState, T> project) =>
await FSharpAsync.StartAsTask(Query(FuncConvert.FromFunc(project)), null, null);
}

/// Newtonsoft.Json implementation of IEncoder that encodes direct to a UTF-8 Buffer
Expand Down
60 changes: 13 additions & 47 deletions equinox-web-csharp/Domain/Todo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -189,10 +189,9 @@ public static IEnumerable<IEvent> Interpret(State s, ICommand x)
break;
case Update c:
var proposed = Tuple.Create(c.Props.Order, c.Props.Title, c.Props.Completed);

bool IsEquivalent(Events.ItemData i) =>
i.Id == c.Id && Tuple.Create(i.Order, i.Title, i.Completed).Equals(proposed);

i.Id == c.Id
&& Tuple.Create(i.Order, i.Title, i.Completed).Equals(proposed);
if (!s.Items.Any(IsEquivalent))
yield return Make<Events.Updated>(c.Id, c.Props);
break;
Expand All @@ -213,6 +212,7 @@ bool IsEquivalent(Events.ItemData i) =>
}
}

/// Defines low level stream operations relevant to the Todo Stream in terms of Command and Events
private class Handler
{
readonly EquinoxHandler<IEvent, State> _inner;
Expand All @@ -222,11 +222,19 @@ public Handler(ILogger log, IStream<IEvent, State> stream)
_inner = new EquinoxHandler<IEvent, State>(Folds.Fold, log, stream);
}

/// Execute `command`, syncing any events decided upon
/// Execute `command`; does not emit the post state
public Task<Unit> Execute(ICommand c) =>
_inner.Decide(ctx =>
ctx.Execute(s => Commands.Interpret(s, c)));

/// Handle `command`, return the items after the command's intent has been applied to the stream
public Task<Events.ItemData[]> Decide(ICommand c) =>
_inner.Decide(ctx =>
{
ctx.Execute(s => Commands.Interpret(s, c));
return ctx.State.Items;
});

/// Establish the present state of the Stream, project from that as specified by `projection`
public Task<T> Query<T>(Func<State, T> projection) =>
_inner.Query(projection);
Expand Down Expand Up @@ -261,49 +269,7 @@ public Task<Unit> Execute(string id, ICommand command) =>
}


/// Defines the decion process which maps from the intent of the `Command` to the `Event`s that represent that decision in the Stream
module Commands =

/// Defines the operations a caller can perform on a Todo List
type Command =
/// Create a single item
| Add of Props
/// Update a single item
| Update of id: int * Props
/// Delete a single item from the list
| Delete of id: int
/// Complete clear the todo list
| Clear

let interpret c (state : Folds.State) =
let mkItem id (value: Props): Events.ItemData = { id = id; order=value.order; title=value.title; completed=value.completed }
match c with
| Add value -> [Events.Added (mkItem state.nextId value)]
| Update (itemId,value) ->
let proposed = mkItem itemId value
match state.items |> List.tryFind (function { id = id } -> id = itemId) with
| Some current when current <> proposed -> [Events.Updated proposed]
| _ -> []
| Delete id -> if state.items |> List.exists (fun x -> x.id = id) then [Events.Deleted id] else []
| Clear -> if state.items |> List.isEmpty then [] else [Events.Cleared]

/// Defines low level stream operations relevant to the Todo Stream in terms of Command and Events
type Handler(log, stream, ?maxAttempts) =

let inner = Equinox.Handler(Folds.fold, log, stream, maxAttempts = defaultArg maxAttempts 2)

/// Execute `command`; does not emit the post state
member __.Execute command : Async<unit> =
inner.Decide <| fun ctx ->
ctx.Execute (Commands.interpret command)
/// Handle `command`, return the items after the command's intent has been applied to the stream
member __.Handle command : Async<Events.ItemData list> =
inner.Decide <| fun ctx ->
ctx.Execute (Commands.interpret command)
ctx.State.items
/// Establish the present state of the Stream, project from that as specified by `projection`
member __.Query(projection : Folds.State -> 't) : Async<'t> =
inner.Query projection


/// A single Item in the Todo List
type View = { id: int; order: int; title: string; completed: bool }
Expand Down

0 comments on commit 310f565

Please sign in to comment.