Skip to content

Commit

Permalink
refactor(state): accepting arbitrary N-state and N-context definitons
Browse files Browse the repository at this point in the history
  • Loading branch information
willyrgf committed Nov 8, 2023
1 parent ffa2753 commit ed0edef
Show file tree
Hide file tree
Showing 4 changed files with 158 additions and 152 deletions.
52 changes: 37 additions & 15 deletions mfm_machine/src/state/context.rs
Original file line number Diff line number Diff line change
@@ -1,31 +1,53 @@
use anyhow::{anyhow, Error};
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};

pub trait Context {
fn read<T: for<'de> Deserialize<'de>>(&self) -> Result<T, Error>;
fn write<T: Serialize>(&mut self, data: &T) -> Result<(), Error>;
fn read_input(&self) -> Result<Value, Error>;
fn write_output(&mut self, data: &Value) -> Result<(), Error>;
}

#[derive(Debug)]
pub struct RawContext {
data: String,
// Your custom context that implements the library's Context trait.
pub struct MyContext {
data: Value,
}

impl RawContext {
impl MyContext {
pub fn new() -> Self {
Self {
data: "{}".to_string(),
}
Self { data: json!({}) }
}
}

impl Context for RawContext {
fn read<T: for<'de> Deserialize<'de>>(&self) -> Result<T, Error> {
serde_json::from_str(&self.data).map_err(|e| anyhow!("error on deserialize: {}", e))
impl Context for MyContext {
fn read_input(&self) -> Result<Value, Error> {
Ok(self.data.clone())
}

fn write<T: Serialize>(&mut self, data: &T) -> Result<(), Error> {
self.data = serde_json::to_string(data)?;
fn write_output(&mut self, data: &Value) -> Result<(), Error> {
self.data = data.clone();
Ok(())
}
}

// #[derive(Debug)]
// pub struct RawContext {
// data: String,
// }
//
// impl RawContext {
// pub fn new() -> Self {
// Self {
// data: "{}".to_string(),
// }
// }
// }
//
// impl Context for RawContext {
// fn read<T: for<'de> Deserialize<'de>>(&self) -> Result<T, Error> {
// serde_json::from_str(&self.data).map_err(|e| anyhow!("error on deserialize: {}", e))
// }
//
// fn write<T: Serialize>(&mut self, data: &T) -> Result<(), Error> {
// self.data = serde_json::to_string(data)?;
// Ok(())
// }
// }
37 changes: 1 addition & 36 deletions mfm_machine/src/state/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,42 +58,7 @@ pub trait StateConfig {
}

pub trait StateHandler: StateConfig {
fn handler<C: Context>(&self, context: &mut C) -> Result<(), Error>;
}

#[derive(Debug, Clone, PartialEq)]
pub struct StateWrapper<T: StateHandler>(T);
impl<T: StateHandler> StateWrapper<T> {
fn new(state: T) -> Self {
Self(state)
}

fn handler<C: Context>(&self, context: &mut C) -> Result<(), Error> {
self.0.handler(context)
}

fn label(&self) -> &Label {
self.0.label()
}

fn tags(&self) -> &[Tag] {
self.0.tags()
}

fn depends_on(&self) -> &[Tag] {
self.0.depends_on()
}

fn depends_on_strategy(&self) -> &DependencyStrategy {
self.0.depends_on_strategy()
}
}

// Those states are mfm-specific states, and should be moved to the app side
#[derive(Debug, Clone, PartialEq)]
enum State {
Setup(StateWrapper<states::Setup>),
Report(StateWrapper<states::Report>),
fn handler(&self, context: &mut dyn Context) -> Result<(), Error>;
}

#[derive(Debug)]
Expand Down
137 changes: 120 additions & 17 deletions mfm_machine/src/state/state_machine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@ use std::usize;

use anyhow::{anyhow, Error};

use super::{context::Context, State, StateError};
use super::{context::Context, StateError, StateHandler};

struct StateMachine {
states: Vec<State>,
states: Vec<Box<dyn StateHandler>>,
}

type StateResult = Result<(), Error>;

impl StateMachine {
pub fn new(initial_states: Vec<State>) -> Self {
pub fn new(initial_states: Vec<Box<dyn StateHandler>>) -> Self {
Self {
states: initial_states,
}
Expand Down Expand Up @@ -71,10 +71,12 @@ impl StateMachine {

let current_state = &self.states[next_state_index];

let result = match current_state {
State::Setup(h) => h.handler(context),
State::Report(h) => h.handler(context),
};
let result = current_state.handler(context);

// let result = match current_state {
// State::Setup(h) => h.handler(context),
// State::Report(h) => h.handler(context),
// };

self.execute_rec(context, next_state_index, Option::Some(result))
}
Expand All @@ -85,24 +87,125 @@ impl StateMachine {
}

mod test {
use crate::state::{
context::Context, DependencyStrategy, Label, StateConfig, StateHandler, Tag,
};
use anyhow::Error;
use mfm_machine_macros::StateConfigReqs;
use serde_json::json;

#[derive(Debug, Clone, PartialEq, StateConfigReqs)]
pub struct Setup {
label: Label,
tags: Vec<Tag>,
depends_on: Vec<Tag>,
depends_on_strategy: DependencyStrategy,
}

impl Setup {
pub fn new() -> Self {
Self {
label: Label::new("setup_state").unwrap(),
tags: vec![Tag::new("setup").unwrap()],
depends_on: vec![Tag::new("setup").unwrap()],
depends_on_strategy: DependencyStrategy::Latest,
}
}
}

impl StateHandler for Setup {
fn handler(&self, context: &mut dyn Context) -> Result<(), Error> {
let _data = context.read_input().unwrap();
let data = json!({ "data": "some new data".to_string() });
context.write_output(&data)
}
}

#[derive(Debug, Clone, PartialEq, StateConfigReqs)]
pub struct Report {
label: Label,
tags: Vec<Tag>,
depends_on: Vec<Tag>,
depends_on_strategy: DependencyStrategy,
}

impl Report {
pub fn new() -> Self {
Self {
label: Label::new("report_state").unwrap(),
tags: vec![Tag::new("report").unwrap()],
depends_on: vec![Tag::new("setup").unwrap()],
depends_on_strategy: DependencyStrategy::Latest,
}
}
}

impl StateHandler for Report {
fn handler(&self, context: &mut dyn Context) -> Result<(), Error> {
let _data = context.read_input().unwrap();
let data = json!({ "data": "some new data reported".to_string() });
context.write_output(&data)
}
}

#[test]
fn test_setup_state_initialization() {
use crate::state::context::MyContext;

let label = Label::new("setup_state").unwrap();
let tags = vec![Tag::new("setup").unwrap()];
let state = Setup::new();
let mut ctx_input = MyContext::new();

let result = state.handler(&mut ctx_input);
assert!(result.is_ok());
assert_eq!(state.label(), &label);
assert_eq!(state.tags(), &tags);
}

#[test]
fn test_state_machine_execute() {
use super::*;
use crate::state::{context::RawContext, states, StateWrapper};
use crate::state::context::MyContext;

let setup_state = Box::new(Setup::new());
let report_state = Box::new(Report::new());

let initial_states: Vec<Box<dyn StateHandler>> =
vec![setup_state.clone(), report_state.clone()];

let initial_states = vec![
State::Setup(StateWrapper::new(states::Setup::new())),
State::Report(StateWrapper::new(states::Report::new())),
];
let states: Vec<Box<dyn StateHandler>> = vec![setup_state.clone(), report_state.clone()];

let state_machine = StateMachine::new(initial_states.clone());
let iss: Vec<(Label, &[Tag], &[Tag], DependencyStrategy)> = states
.iter()
.map(|is| {
(
is.label().clone(),
is.tags().clone(),
is.depends_on().clone(),
is.depends_on_strategy().clone(),
)
})
.collect();

let mut context = RawContext::new();
let state_machine = StateMachine::new(initial_states);

let mut context = MyContext::new();
let result = state_machine.execute(&mut context);
let last_ctx_message: String = context.read().unwrap();
let last_ctx_message = context.read_input().unwrap();

assert_eq!(state_machine.states.len(), iss.len());

state_machine.states.iter().zip(iss.iter()).for_each(
|(s, (label, tags, depends_on, depends_on_strategy))| {
assert_eq!(s.label(), label);
assert_eq!(s.tags(), *tags);
assert_eq!(s.depends_on(), *depends_on);
assert_eq!(s.depends_on_strategy(), depends_on_strategy);
},
);

assert_eq!(state_machine.states, initial_states);
assert!(result.is_ok());
assert_eq!(last_ctx_message, "some new data reported".to_string());
assert_eq!(last_ctx_message, json!({"data": "some new data reported"}));
}
}
84 changes: 0 additions & 84 deletions mfm_machine/src/state/states.rs
Original file line number Diff line number Diff line change
@@ -1,86 +1,2 @@
use anyhow::Error;
use mfm_machine_macros::StateConfigReqs;
use serde_derive::{Deserialize, Serialize};

use crate::state::{context::Context, DependencyStrategy, Label, StateConfig, StateHandler, Tag};

#[derive(Debug, Clone, PartialEq, StateConfigReqs)]
pub struct Setup {
label: Label,
tags: Vec<Tag>,
depends_on: Vec<Tag>,
depends_on_strategy: DependencyStrategy,
}

impl Setup {
pub fn new() -> Self {
Self {
label: Label::new("setup_state").unwrap(),
tags: vec![Tag::new("setup").unwrap()],
depends_on: vec![Tag::new("setup").unwrap()],
depends_on_strategy: DependencyStrategy::Latest,
}
}
}

#[derive(Debug, Deserialize, Serialize)]
struct SetupStateData {}

impl StateHandler for Setup {
fn handler<C: Context>(&self, context: &mut C) -> Result<(), Error> {
let _data: SetupStateData = context.read().unwrap();
let data = "some new data".to_string();
context.write(&data)
}
}

#[derive(Debug, Clone, PartialEq, StateConfigReqs)]
pub struct Report {
label: Label,
tags: Vec<Tag>,
depends_on: Vec<Tag>,
depends_on_strategy: DependencyStrategy,
}

impl Report {
pub fn new() -> Self {
Self {
label: Label::new("report_state").unwrap(),
tags: vec![Tag::new("report").unwrap()],
depends_on: vec![Tag::new("setup").unwrap()],
depends_on_strategy: DependencyStrategy::Latest,
}
}
}

impl StateHandler for Report {
fn handler<C: Context>(&self, context: &mut C) -> Result<(), Error> {
let _data: String = context.read().unwrap();
let data = "some new data reported".to_string();
context.write(&data)
}
}

#[cfg(test)]
mod test {
use crate::state::{context::RawContext, State, StateWrapper};

use super::*;

#[test]
fn test_setup_state_initialization() {
let label = Label::new("setup_state").unwrap();
let tags = vec![Tag::new("setup").unwrap()];
let state = State::Setup(StateWrapper::new(Setup::new()));
let mut ctx_input = RawContext::new();
match state {
State::Setup(t) => {
let result = t.handler(&mut ctx_input);
assert!(result.is_ok());
assert_eq!(t.label(), &label);
assert_eq!(t.tags(), &tags);
}
_ => panic!("expected Setup state"),
}
}
}

0 comments on commit ed0edef

Please sign in to comment.