Skip to content

Commit

Permalink
Replaced MonadicBits with ErrorOr
Browse files Browse the repository at this point in the history
  • Loading branch information
apfohl committed Jan 2, 2024
1 parent d79a629 commit 610ca91
Show file tree
Hide file tree
Showing 14 changed files with 103 additions and 78 deletions.
7 changes: 5 additions & 2 deletions HotwiredBooks.sln.DotSettings.user
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/Environment/AssemblyExplorer/XmlDocument/@EntryValue">&lt;AssemblyExplorer&gt;
&lt;Assembly Path="/home/andreas/.nuget/packages/erroror/1.3.0/lib/net6.0/ErrorOr.dll" /&gt;
&lt;/AssemblyExplorer&gt;</s:String>

<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=56f95705_002D69f0_002D4d78_002D9fec_002Db8596a7edfd4/@EntryIndexedValue">&lt;SessionState ContinuousTestingMode="0" Name="Create_inserts_value_into_repository" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"&gt;
<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=56f95705_002D69f0_002D4d78_002D9fec_002Db8596a7edfd4/@EntryIndexedValue">&lt;SessionState ContinuousTestingMode="0" IsActive="True" Name="Create_inserts_value_into_repository" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"&gt;
&lt;Project Location="/home/andreas/code/HotwiredBooks/HotwiredBooksTests" Presentation="&amp;lt;HotwiredBooksTests&amp;gt;" /&gt;
&lt;/SessionState&gt;</s:String>
<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=7a5365d0_002Db616_002D4d4b_002D83e3_002Df6bda9b641db/@EntryIndexedValue">&lt;SessionState ContinuousTestingMode="0" IsActive="True" Name="Create_inserts_value_into_repository #2" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"&gt;
<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=7a5365d0_002Db616_002D4d4b_002D83e3_002Df6bda9b641db/@EntryIndexedValue">&lt;SessionState ContinuousTestingMode="0" Name="Create_inserts_value_into_repository #2" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"&gt;
&lt;Project Location="/home/andreas/code/HotwiredBooks/HotwiredBooksTests" Presentation="&amp;lt;HotwiredBooksTests&amp;gt;" /&gt;
&lt;/SessionState&gt;</s:String></wpf:ResourceDictionary>
10 changes: 5 additions & 5 deletions HotwiredBooks/Components/IBooksRepository.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
using HotwiredBooks.Models;
using MonadicBits;
using ErrorOr;

namespace HotwiredBooks.Components;

public interface IBooksRepository
{
Task<Maybe<Book>> Lookup(Guid id);
Task<ErrorOr<Book>> Lookup(Guid id);
Task<IEnumerable<Book>> All();
Task<Maybe<Book>> Create(string title, string author);
Task<Maybe<Book>> Update(Book book);
Task<Maybe<Book>> Delete(Book book);
Task<ErrorOr<Book>> Create(string title, string author);
Task<ErrorOr<Book>> Update(Book book);
Task<ErrorOr<Book>> Delete(Book book);
}
41 changes: 21 additions & 20 deletions HotwiredBooks/Components/MemoryBasedBooksRepository.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
using System.Collections.Concurrent;
using bridgefield.FoundationalBits;
using HotwiredBooks.Models;
using MonadicBits;
using ErrorOr;
using ErrorOr.Extensions;
using HotwiredBooks.Extensions;

namespace HotwiredBooks.Components;

using static Functional;

internal interface IBooksCommand;

internal sealed record Create(string Title, string Author, DateTime CreatedAt) : IBooksCommand;
Expand All @@ -17,44 +17,45 @@ internal sealed record Delete(Book Book) : IBooksCommand;

public sealed class MemoryBasedBooksRepository : IBooksRepository
{
private readonly IAgent<IBooksCommand, Maybe<Book>> agent;
private readonly IAgent<IBooksCommand, ErrorOr<Book>> agent;
private readonly ConcurrentDictionary<Guid, Book> books = new(InitialBooks());

public MemoryBasedBooksRepository() =>
agent = Agent.Start<ConcurrentDictionary<Guid, Book>, IBooksCommand, Maybe<Book>>(
agent = Agent.Start<ConcurrentDictionary<Guid, Book>, IBooksCommand, ErrorOr<Book>>(
books,
(current, command) => command switch
{
Create create => Task.FromResult((current,
from book in new Book(Guid.NewGuid(), create.Title, create.Author, create.CreatedAt).Just()
from createdBook in current.TryAdd(book.Id, book) ? book.Just() : Nothing
select createdBook)),
Delete delete => Task.FromResult((current,
current.TryRemove(delete.Book.Id, out var deletedBook) ? deletedBook.Just() : Nothing)),
Create create => (current,
from book in new Book(Guid.NewGuid(), create.Title, create.Author, create.CreatedAt).Success()
from createdBook in current.TryAdd(book.Id, book) ? book.Success() : ErrorOr<Book>.From([Error.Failure()])
select createdBook).AsTask(),
Delete delete => (current,
current.TryRemove(delete.Book.Id, out var deletedBook) ? deletedBook.Success() : ErrorOr<Book>.From(
[Error.Failure()])).AsTask(),
Update update => Task.FromResult((current,
from currentBook in current.TryGetValue(update.Book.Id, out var currentValue)
? currentValue.Just()
: Nothing
? currentValue.Success()
: ErrorOr<Book>.From([Error.Failure()])
from updatedBook in current.TryUpdate(update.Book.Id, update.Book, currentBook)
? update.Book.Just()
: Nothing
? update.Book.Success()
: ErrorOr<Book>.From([Error.Failure()])
select updatedBook)),
_ => throw new ArgumentOutOfRangeException(nameof(command))
});

public Task<Maybe<Book>> Lookup(Guid id) =>
Task.FromResult(books.TryGetValue(id, out var book) ? book.Just() : Nothing);
public Task<ErrorOr<Book>> Lookup(Guid id) =>
Task.FromResult(books.TryGetValue(id, out var book) ? ErrorOrFactory.From(book) : Error.NotFound());

public Task<IEnumerable<Book>> All() =>
Task.FromResult<IEnumerable<Book>>(books.Values);

public Task<Maybe<Book>> Create(string title, string author) =>
public Task<ErrorOr<Book>> Create(string title, string author) =>
agent.Tell(new Create(title, author, DateTime.Now));

public Task<Maybe<Book>> Update(Book book) =>
public Task<ErrorOr<Book>> Update(Book book) =>
agent.Tell(new Update(book));

public Task<Maybe<Book>> Delete(Book book) =>
public Task<ErrorOr<Book>> Delete(Book book) =>
agent.Tell(new Delete(book));

private static IEnumerable<KeyValuePair<Guid, Book>> InitialBooks()
Expand Down
25 changes: 13 additions & 12 deletions HotwiredBooks/Controllers/BooksController.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
using ErrorOr;
using ErrorOr.Extensions;
using HotwiredBooks.Attributes;
using HotwiredBooks.Components;
using HotwiredBooks.Extensions;
using HotwiredBooks.Models;
using HotwiredBooks.ViewModels;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using MonadicBits;

namespace HotwiredBooks.Controllers;

Expand All @@ -32,15 +33,15 @@ from formData in ParseFormData(collection)
from book in booksRepository.Create(formData.Title, formData.Author)
select book
)
.MapAsync<Book, IActionResult>(async book =>
.Select<Book, IActionResult>(async book =>
View(new BooksCreateViewModel(book, (await booksRepository.All()).Count())))
.OrElse(StatusCode(500, "An unexpected error occurred on the server."));

[HttpGet]
public Task<IActionResult> Edit(Guid id) =>
booksRepository
.Lookup(id)
.MapAsync<Book, IActionResult>(book => View(new BooksEditViewModel(book)))
.Select<Book, IActionResult>(book => View(new BooksEditViewModel(book)))
.OrElse(StatusCode(500, "An unexpected error occurred on the server."));

[HttpPatch, HttpPut]
Expand All @@ -58,7 +59,7 @@ book with
)
select updatedBook
)
.MapAsync(book => ViewComponentRenderer.RenderAsync("Book", new BooksEditViewModel(book)))
.Select(book => ViewComponentRenderer.RenderAsync("Book", new BooksEditViewModel(book)))
.OrElse(StatusCode(500, "An unexpected error occurred on the server."));

[HttpPost]
Expand All @@ -67,15 +68,15 @@ select updatedBook
public Task<IActionResult> Delete(Guid id) =>
booksRepository
.Lookup(id)
.BindAsync(booksRepository.Delete)
.MapAsync<Book, IActionResult>(async book =>
.SelectMany(booksRepository.Delete)
.Select<Book, IActionResult>(async book =>
View(new BooksDeleteViewModel(book, (await booksRepository.All()).Count())))
.OrElse(StatusCode(500, "An unexpected error occurred on the server."));

private static Task<Maybe<FormData>> ParseFormData(IFormCollection collection) =>
Task.FromResult(
from title in collection.JustGetValue("title")
from author in collection.JustGetValue("author")
select new FormData(title, author)
);
private static Task<ErrorOr<FormData>> ParseFormData(IFormCollection collection) =>
(
from title in collection.JustGetValue("title")
from author in collection.JustGetValue("author")
select new FormData(title, author)
).AsTask();
}
7 changes: 7 additions & 0 deletions HotwiredBooks/Extensions/AsyncExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace HotwiredBooks.Extensions;

public static class AsyncExtensions
{
public static Task<T> AsTask<T>(this T value) =>
Task.FromResult(value);
}
9 changes: 9 additions & 0 deletions HotwiredBooks/Extensions/ErrorOrExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using ErrorOr;

namespace HotwiredBooks.Extensions;

public static class ErrorOrExtensions
{
public static ErrorOr<T> Success<T>(this T value) =>
ErrorOrFactory.From(value);
}
29 changes: 15 additions & 14 deletions HotwiredBooks/Extensions/FunctionalExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,26 +1,27 @@
using Microsoft.Extensions.Primitives;
using MonadicBits;
using ErrorOr;

namespace HotwiredBooks.Extensions;

using static Functional;

public static class FunctionalExtensions
{
public static Maybe<T> ToMaybe<T>(this T value) => value?.Just() ?? Nothing;
public static ErrorOr<T> ToErrorOr<T>(this T value) =>
value is not null ? ErrorOrFactory.From(value) : ErrorOr<T>.From([Error.Failure()]);

public static Maybe<StringValues> JustGetValue(this IFormCollection collection, string key) =>
collection.TryGetValue(key, out var value) ? value.Just() : Nothing;
public static ErrorOr<StringValues> JustGetValue(this IFormCollection collection, string key) =>
collection.TryGetValue(key, out var value)
? ErrorOrFactory.From(value)
: ErrorOr<StringValues>.From([Error.NotFound()]);

public static T OrElse<T>(this Maybe<T> maybe, T orElse) =>
maybe.Match(value => value, () => orElse);
public static T OrElse<T>(this ErrorOr<T> maybe, T orElse) =>
maybe.Match(value => value, _ => orElse);

public static T OrElse<T>(this Maybe<T> maybe, Func<T> orElse) =>
maybe.Match(value => value, orElse);
public static T OrElse<T>(this ErrorOr<T> maybe, Func<T> orElse) =>
maybe.Match(value => value, _ => orElse());

public static async Task<T> OrElse<T>(this Task<Maybe<T>> maybe, Func<T> orElse) =>
(await maybe).Match(value => value, orElse);
public static async Task<T> OrElse<T>(this Task<ErrorOr<T>> maybe, Func<T> orElse) =>
(await maybe).Match(value => value, _ => orElse());

public static async Task<T> OrElse<T>(this Task<Maybe<T>> maybe, T orElse) =>
(await maybe).Match(value => value, () => orElse);
public static async Task<T> OrElse<T>(this Task<ErrorOr<T>> maybe, T orElse) =>
(await maybe).Match(value => value, _ => orElse);
}
8 changes: 4 additions & 4 deletions HotwiredBooks/Extensions/HtmlHelperExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
using System.Reflection;
using ErrorOr.Extensions;
using Humanizer;
using Microsoft.AspNetCore.Mvc.Rendering;
using MonadicBits;

namespace HotwiredBooks.Extensions;

Expand All @@ -12,12 +12,12 @@ public static class HtmlHelperExtensions

public static string DomId(this IHtmlHelper htmlHelper, object @object, string prefix = null) =>
(
from property in @object.GetType().GetProperty("Id").ToMaybe()
from value in property.GetValue(@object).ToMaybe()
from property in @object.GetType().GetProperty("Id").ToErrorOr()
from value in property.GetValue(@object).ToErrorOr()
select value.ToString()
).Match(
id => $"{DomClass(htmlHelper, @object, prefix)}{Join}{id}",
() => DomClass(htmlHelper, @object, prefix ?? New)
_ => DomClass(htmlHelper, @object, prefix ?? New)
);

public static string DomClass(this IHtmlHelper _, object objectOrType, string prefix = null)
Expand Down
3 changes: 2 additions & 1 deletion HotwiredBooks/HotwiredBooks.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@

<ItemGroup>
<PackageReference Include="bridgefield.FoundationalBits" Version="0.1.2" />
<PackageReference Include="bridgefield.MonadicBits" Version="0.5.1" />
<PackageReference Include="ErrorOr" Version="1.3.0" />
<PackageReference Include="ErrorOr.Extensions" Version="1.0.0" />
<PackageReference Include="Humanizer" Version="2.14.1" />
<PackageReference Include="System.Reactive" Version="6.0.0" />
</ItemGroup>
Expand Down
4 changes: 2 additions & 2 deletions HotwiredBooks/ViewComponents/BookFormViewComponent.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
using HotwiredBooks.Models;
using HotwiredBooks.ViewModels;
using Microsoft.AspNetCore.Mvc;
using MonadicBits;
using ErrorOr;

namespace HotwiredBooks.ViewComponents;

public sealed class BookFormViewComponent : ViewComponent
{
public IViewComponentResult Invoke(Maybe<Book> book) => View(new BookFormViewModel(book));
public IViewComponentResult Invoke(ErrorOr<Book> book) => View(new BookFormViewModel(book));
}
4 changes: 2 additions & 2 deletions HotwiredBooks/ViewModels/BookFormViewModel.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using HotwiredBooks.Models;
using MonadicBits;
using ErrorOr;

namespace HotwiredBooks.ViewModels;

public sealed record BookFormViewModel(Maybe<Book> Book);
public sealed record BookFormViewModel(ErrorOr<Book> Book);
5 changes: 3 additions & 2 deletions HotwiredBooks/Views/Books/Edit.cshtml
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
@using HotwiredBooks.Extensions
@using MonadicBits
@using HotwiredBooks.TagHelpers
@using HotwiredBooks.ViewComponents
@model HotwiredBooks.ViewModels.BooksEditViewModel
@{
Layout = null;
}

<turbo-frame id="@Html.DomId(Model.Book)">
<vc:book-form book="@Model.Book.Just()"/>
<vc:book-form book="@Model.Book.Success()"/>
</turbo-frame>
11 changes: 6 additions & 5 deletions HotwiredBooks/Views/Shared/Components/BookForm/Default.cshtml
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
@using ErrorOr.Extensions
@model HotwiredBooks.ViewModels.BookFormViewModel

@{
var id = Model.Book.Map(book => book.Id).Match(id => id.ToString(), () => string.Empty);
var action = Model.Book.Match(_ => "Update", () => "Create");
var method = Model.Book.Match(_ => "put", () => "post");
var title = Model.Book.Map(book => book.Title).Match(title => title, () => string.Empty);
var author = Model.Book.Map(book => book.Author).Match(author => author, () => string.Empty);
var id = Model.Book.Select(book => book.Id).Match(id => id.ToString(), _ => string.Empty);
var action = Model.Book.Match(_ => "Update", _ => "Create");
var method = Model.Book.Match(_ => "put", _ => "post");
var title = Model.Book.Select(book => book.Title).Match(title => title, _ => string.Empty);
var author = Model.Book.Select(book => book.Author).Match(author => author, _ => string.Empty);
}

<form asp-controller="Books"
Expand Down
18 changes: 9 additions & 9 deletions HotwiredBooksTests/MemoryBasedBookRepositoryTests.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
using ErrorOr.Extensions;
using HotwiredBooks.Components;
using MonadicBits;

namespace HotwiredBooksTests;

Expand All @@ -13,21 +13,21 @@ public static async Task Create_inserts_value_into_repository()
const string title = "ABC";
const string author = "DEF";

var maybe = await repository.Create(title, author);
maybe.Match(book => Assert.Multiple(() =>
var book = await repository.Create(title, author);
book.Switch(b => Assert.Multiple(() =>
{
Assert.That(book.Title, Is.EqualTo(title));
Assert.That(book.Author, Is.EqualTo(author));
Assert.That(book.Id, Is.Not.EqualTo(new Guid()));
Assert.That(b.Title, Is.EqualTo(title));
Assert.That(b.Author, Is.EqualTo(author));
Assert.That(b.Id, Is.Not.EqualTo(new Guid()));
}),
Assert.Fail
_ => Assert.Fail()
);

var areEqual =
from created in Task.FromResult(maybe)
from created in Task.FromResult(book)
from read in repository.Lookup(created.Id)
select created == read;

(await areEqual).Match(Assert.True, Assert.Fail);
(await areEqual).Switch(Assert.True, _ => Assert.Fail());
}
}

0 comments on commit 610ca91

Please sign in to comment.