use crate::actions::ActionType;
use crate::error::{Error, Result};
use crate::measures::Measure;
use chrono::{DateTime, Utc};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
pub struct RecipeResource {
pub id: String,
pub name: String,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub description: Option<String>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub resource_conforms_to: Option<String>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub image: Option<String>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub unit_of_resource: Option<String>,
pub substitutable: bool,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub note: Option<String>,
pub created_at: DateTime<Utc>,
}
impl RecipeResource {
pub fn new(id: impl Into<String>, name: impl Into<String>) -> Self {
RecipeResource {
id: id.into(),
name: name.into(),
description: None,
resource_conforms_to: None,
image: None,
unit_of_resource: None,
substitutable: true,
note: None,
created_at: Utc::now(),
}
}
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
pub fn with_resource_conforms_to(mut self, spec_id: impl Into<String>) -> Self {
self.resource_conforms_to = Some(spec_id.into());
self
}
pub fn with_substitutable(mut self, substitutable: bool) -> Self {
self.substitutable = substitutable;
self
}
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
pub struct RecipeFlow {
pub id: String,
pub action: ActionType,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub recipe_input_of: Option<String>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub recipe_output_of: Option<String>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub recipe_flow_resource: Option<String>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub resource_conforms_to: Option<String>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub resource_quantity: Option<Measure>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub effort_quantity: Option<Measure>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub stage: Option<String>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub state: Option<String>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub note: Option<String>,
pub created_at: DateTime<Utc>,
}
impl RecipeFlow {
pub fn builder() -> RecipeFlowBuilder {
RecipeFlowBuilder::default()
}
}
#[derive(Debug, Default)]
pub struct RecipeFlowBuilder {
id: Option<String>,
action: Option<ActionType>,
recipe_input_of: Option<String>,
recipe_output_of: Option<String>,
recipe_flow_resource: Option<String>,
resource_conforms_to: Option<String>,
resource_quantity: Option<Measure>,
effort_quantity: Option<Measure>,
stage: Option<String>,
state: Option<String>,
note: Option<String>,
}
impl RecipeFlowBuilder {
pub fn id(mut self, id: impl Into<String>) -> Self {
self.id = Some(id.into());
self
}
pub fn action(mut self, action: ActionType) -> Self {
self.action = Some(action);
self
}
pub fn input_of(mut self, process_id: impl Into<String>) -> Self {
self.recipe_input_of = Some(process_id.into());
self
}
pub fn output_of(mut self, process_id: impl Into<String>) -> Self {
self.recipe_output_of = Some(process_id.into());
self
}
pub fn recipe_flow_resource(mut self, resource_id: impl Into<String>) -> Self {
self.recipe_flow_resource = Some(resource_id.into());
self
}
pub fn resource_conforms_to(mut self, spec_id: impl Into<String>) -> Self {
self.resource_conforms_to = Some(spec_id.into());
self
}
pub fn resource_quantity(mut self, quantity: Measure) -> Self {
self.resource_quantity = Some(quantity);
self
}
pub fn effort_quantity(mut self, quantity: Measure) -> Self {
self.effort_quantity = Some(quantity);
self
}
pub fn stage(mut self, stage: impl Into<String>) -> Self {
self.stage = Some(stage.into());
self
}
pub fn state(mut self, state: impl Into<String>) -> Self {
self.state = Some(state.into());
self
}
pub fn note(mut self, note: impl Into<String>) -> Self {
self.note = Some(note.into());
self
}
pub fn build(self) -> Result<RecipeFlow> {
let id = self.id.ok_or_else(|| Error::missing_field("id"))?;
let action = self.action.ok_or_else(|| Error::missing_field("action"))?;
Ok(RecipeFlow {
id,
action,
recipe_input_of: self.recipe_input_of,
recipe_output_of: self.recipe_output_of,
recipe_flow_resource: self.recipe_flow_resource,
resource_conforms_to: self.resource_conforms_to,
resource_quantity: self.resource_quantity,
effort_quantity: self.effort_quantity,
stage: self.stage,
state: self.state,
note: self.note,
created_at: Utc::now(),
})
}
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
pub struct RecipeProcess {
pub id: String,
pub name: String,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub description: Option<String>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub has_duration: Option<i64>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub duration_unit: Option<String>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub process_conforms_to: Option<String>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub in_scope_of: Option<String>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub note: Option<String>,
pub created_at: DateTime<Utc>,
}
impl RecipeProcess {
pub fn new(id: impl Into<String>, name: impl Into<String>) -> Self {
RecipeProcess {
id: id.into(),
name: name.into(),
description: None,
has_duration: None,
duration_unit: None,
process_conforms_to: None,
in_scope_of: None,
note: None,
created_at: Utc::now(),
}
}
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
pub fn with_duration(mut self, duration: i64, unit: impl Into<String>) -> Self {
self.has_duration = Some(duration);
self.duration_unit = Some(unit.into());
self
}
pub fn with_process_conforms_to(mut self, spec_id: impl Into<String>) -> Self {
self.process_conforms_to = Some(spec_id.into());
self
}
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
pub struct RecipeExchange {
pub id: String,
pub name: String,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub description: Option<String>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub note: Option<String>,
pub created_at: DateTime<Utc>,
}
impl RecipeExchange {
pub fn new(id: impl Into<String>, name: impl Into<String>) -> Self {
RecipeExchange {
id: id.into(),
name: name.into(),
description: None,
note: None,
created_at: Utc::now(),
}
}
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_recipe_resource() {
let resource = RecipeResource::new("rr-001", "Flour")
.with_description("All-purpose flour")
.with_substitutable(false);
assert_eq!(resource.id, "rr-001");
assert_eq!(resource.name, "Flour");
assert!(!resource.substitutable);
}
#[test]
fn test_recipe_flow_builder() {
let flow = RecipeFlow::builder()
.id("rf-001")
.action(ActionType::Consume)
.input_of("rp-001")
.resource_conforms_to("flour-spec")
.build()
.unwrap();
assert_eq!(flow.id, "rf-001");
assert_eq!(flow.action, ActionType::Consume);
assert_eq!(flow.recipe_input_of, Some("rp-001".to_string()));
}
#[test]
fn test_recipe_process() {
let process = RecipeProcess::new("rp-001", "Baking")
.with_description("Bake bread in oven")
.with_duration(45, "minutes");
assert_eq!(process.id, "rp-001");
assert_eq!(process.name, "Baking");
assert_eq!(process.has_duration, Some(45));
assert_eq!(process.duration_unit, Some("minutes".to_string()));
}
#[test]
fn test_recipe_exchange() {
let exchange = RecipeExchange::new("re-001", "Standard Sale")
.with_description("Exchange goods for currency");
assert_eq!(exchange.id, "re-001");
assert_eq!(exchange.name, "Standard Sale");
}
#[cfg(feature = "serde")]
#[test]
fn test_recipe_flow_serialization() {
let flow = RecipeFlow::builder()
.id("rf-001")
.action(ActionType::Produce)
.output_of("rp-001")
.build()
.unwrap();
let json = serde_json::to_string(&flow).unwrap();
let parsed: RecipeFlow = serde_json::from_str(&json).unwrap();
assert_eq!(flow.id, parsed.id);
assert_eq!(flow.action, parsed.action);
}
}