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

States and projections #1

Merged
merged 2 commits into from
Jan 5, 2024
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
4 changes: 2 additions & 2 deletions Justfile
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
axium:
axum:
cd clients/axum && cargo watch -x run

yew:
cd clients/yew && trunk serve

test:
cargo test
cargo test -- --nocapture

deps:
cargo +nightly udeps
Expand Down
55 changes: 55 additions & 0 deletions application/src/command.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
use crate::port::{TodoListRepository, TodoListStore};
use domain::todolist_message::TodoListMessage;
use framework::*;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Consider using a more specific import for the framework::* module instead of importing all its contents. [best practice]

Suggested change
use framework::*;
use framework::{AnyResult, Error};

use serde::Deserialize;

#[derive(Deserialize)]
pub enum Command {
AddTask { name: String },
RemoveTask { index: usize },
CompleteTask { index: usize },
}
Comment on lines +7 to +11

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Consider adding a docstring to the Command enum. [best practice]

Suggested change
pub enum Command {
AddTask { name: String },
RemoveTask { index: usize },
CompleteTask { index: usize },
}
#[derive(Deserialize)]
/// Represents a command to be executed on the todo list.
pub enum Command {


#[derive(Debug, Error)]
pub enum CommandError {
#[error("Task name cannot be empty")]
TaskNameCannotBeEmpty,
}
Comment on lines +13 to +17

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Consider adding a docstring to the CommandError enum. [best practice]

Suggested change
#[derive(Debug, Error)]
pub enum CommandError {
#[error("Task name cannot be empty")]
TaskNameCannotBeEmpty,
}
#[derive(Debug, Error)]
/// Represents an error that can occur while executing a command on the todo list.
pub enum CommandError {


impl TryInto<TodoListMessage> for &Command {
type Error = CommandError;

fn try_into(self) -> Result<TodoListMessage, Self::Error> {
Ok(match self {
Command::AddTask { name } => TodoListMessage::AddTask(
name.clone()
.try_into()
.map_err(|_| CommandError::TaskNameCannotBeEmpty)?,
),
Command::RemoveTask { index } => TodoListMessage::RemoveTask((*index).into()),
Command::CompleteTask { index } => TodoListMessage::CompleteTask((*index).into()),
})
}
}
Comment on lines +19 to +33

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Consider adding a docstring to the try_into function. [best practice]

Suggested change
impl TryInto<TodoListMessage> for &Command {
type Error = CommandError;
fn try_into(self) -> Result<TodoListMessage, Self::Error> {
Ok(match self {
Command::AddTask { name } => TodoListMessage::AddTask(
name.clone()
.try_into()
.map_err(|_| CommandError::TaskNameCannotBeEmpty)?,
),
Command::RemoveTask { index } => TodoListMessage::RemoveTask((*index).into()),
Command::CompleteTask { index } => TodoListMessage::CompleteTask((*index).into()),
})
}
}
impl TryInto<TodoListMessage> for &Command {
/// Tries to convert the command into a `TodoListMessage`.
/// Returns an error if the conversion fails.
type Error = CommandError;
fn try_into(self) -> Result<TodoListMessage, Self::Error> {
...


#[async_trait]
impl<R> Execute<R> for Command
where
R: TodoListRepository + TodoListStore + Send + Sync,
{
async fn execute(&self, runtime: &R) -> AnyResult<()> {
let message = self.try_into()?;

// Pull the current state and apply the message
let todolist = TodoListStore::pull(runtime).await?;
let new_events = todolist.send(&message)?;
TodoListStore::push(runtime, &new_events).await?;

// Save the projection
let mut projection = TodoListRepository::fetch(runtime).await?;
projection.apply(&new_events);
TodoListRepository::save(runtime, &projection).await?;

Ok(())
}
Comment on lines +36 to +54

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Consider adding a docstring to the execute function. [best practice]

Suggested change
impl<R> Execute<R> for Command
where
R: TodoListRepository + TodoListStore + Send + Sync,
{
async fn execute(&self, runtime: &R) -> AnyResult<()> {
let message = self.try_into()?;
// Pull the current state and apply the message
let todolist = TodoListStore::pull(runtime).await?;
let new_events = todolist.send(&message)?;
TodoListStore::push(runtime, &new_events).await?;
// Save the projection
let mut projection = TodoListRepository::fetch(runtime).await?;
projection.apply(&new_events);
TodoListRepository::save(runtime, &projection).await?;
Ok(())
}
#[async_trait]
impl<R> Execute<R> for Command
where
R: TodoListRepository + TodoListStore + Send + Sync,
{
/// Executes the command on the todo list.
/// Returns an error if the execution fails.
async fn execute(&self, runtime: &R) -> AnyResult<()> {
...

}
71 changes: 0 additions & 71 deletions application/src/commands.rs

This file was deleted.

8 changes: 4 additions & 4 deletions application/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
pub mod commands;
pub mod ports;
pub mod projections;
pub mod queries;
pub mod command;
pub mod port;
pub mod projection;
pub mod query;
4 changes: 2 additions & 2 deletions application/src/ports.rs → application/src/port.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
pub use crate::projections::TodoListProjection;
pub use domain::{todolist::TodoList, todolist_event::TodoListEvent};
pub use crate::projection::TodoListProjection;
pub use domain::{todolist_event::TodoListEvent, todolist_state::TodoList};
use framework::*;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Consider using a more specific import for the framework::* module instead of importing all its contents. [best practice]

Suggested change
use framework::*;
use framework::{AnyResult, Error};

#[async_trait]
Expand Down
34 changes: 34 additions & 0 deletions application/src/projection.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
use domain::todolist_event::TodoListEvent;
use framework::*;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;

#[derive(Clone, Default, Debug, Serialize, Deserialize)]
pub struct TodoListProjection {
pub in_progress: HashMap<usize, String>,
pub completed: HashMap<usize, String>,
}
Comment on lines +6 to +10

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Consider adding a docstring to the TodoListProjection struct. [best practice]

Suggested change
#[derive(Clone, Default, Debug, Serialize, Deserialize)]
pub struct TodoListProjection {
pub in_progress: HashMap<usize, String>,
pub completed: HashMap<usize, String>,
}
#[derive(Clone, Default, Debug, Serialize, Deserialize)]
/// Represents the projection of the todo list.
pub struct TodoListProjection {
pub in_progress: HashMap<usize, String>,
pub completed: HashMap<usize, String>,
}


impl Projection for TodoListProjection {
type Event = TodoListEvent;

fn apply(&mut self, events: &[Self::Event]) {
for event in events {
match event {
TodoListEvent::TaskAdded(index, name) => {
self.in_progress
.insert((*index).into(), name.clone().into());
}
TodoListEvent::TaskCompleted(index) => {
self.completed.insert(
(*index).into(),
self.in_progress.remove(&(*index).into()).unwrap(),
);
}
TodoListEvent::TaskRemoved(index) => {
self.in_progress.remove(&(*index).into());
}
}
}
}
}
40 changes: 0 additions & 40 deletions application/src/projections.rs

This file was deleted.

4 changes: 2 additions & 2 deletions application/src/queries.rs → application/src/query.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
use crate::{ports::TodoListRepository, projections::TodoListProjection};
use crate::{port::TodoListRepository, projection::TodoListProjection};
use framework::*;
use serde::Deserialize;

#[derive(Deserialize)]
pub struct GetTodoListQuery {}

#[async_trait]
impl<R> Query<R> for GetTodoListQuery
impl<R> Execute<R> for GetTodoListQuery
where
R: TodoListRepository + Send + Sync,
{
Expand Down
32 changes: 32 additions & 0 deletions clients/axum/examples.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
### Retrieve all todolist
GET http://localhost:3000/todolist HTTP/1.1

### Create a new task
POST http://localhost:3000/todolist HTTP/1.1
content-type: application/json

{
"AddTask": {
"name": "Some task"
}
}

### Complete a task
POST http://localhost:3000/todolist HTTP/1.1
content-type: application/json

{
"CompleteTask": {
"index": 0
}
}

### Remove a task
POST http://localhost:3000/todolist HTTP/1.1
content-type: application/json

{
"RemoveTask": {
"index": 0
}
}
46 changes: 24 additions & 22 deletions clients/axum/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
mod runtime;

use application::{
commands::{AddTaskCommand, CompleteTaskCommand, RemoveTaskCommand},
ports::*,
queries::GetTodoListQuery,
};
use application::{command::Command, port::*, query::GetTodoListQuery};
use axum::{
extract::State,
http::StatusCode,
response::{IntoResponse, Response},
routing::{get, post},
Json, Router,
};
Expand All @@ -22,15 +20,7 @@ async fn main() {

let app = Router::new()
.route("/todolist", get(get_todolist))
.route("/todolist/add", post(handle_command::<AddTaskCommand>))
.route(
"/todolist/remove",
post(handle_command::<RemoveTaskCommand>),
)
.route(
"/todolist/complete",
post(handle_command::<CompleteTaskCommand>),
)
.route("/todolist", post(handle_command))
.with_state(runtime);

let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
Expand All @@ -48,15 +38,27 @@ async fn get_todolist(
.map_err(|err| err.to_string())
}

async fn handle_command<T>(
async fn handle_command(
State(state): State<Arc<Runtime>>,
Json(command): Json<T>,
) -> Result<(), String>
Json(command): Json<Command>,
) -> Result<(), AppError> {
command.execute(state.as_ref()).await?;
Ok(())
}

struct AppError(AnyError);

impl IntoResponse for AppError {
fn into_response(self) -> Response {
(StatusCode::BAD_REQUEST, self.0.to_string()).into_response()
}
}

impl<E> From<E> for AppError
where
T: Command<Runtime>,
E: Into<AnyError>,
{
command
.execute(state.as_ref())
.await
.map_err(|err| err.to_string())
fn from(err: E) -> Self {
Self(err.into())
}
}
Loading
Loading