use crate::error::{Error, Result};
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 ProcessSpecification {
pub id: String,
pub name: String,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub note: Option<String>,
#[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "Vec::is_empty"))]
pub classified_as: Vec<String>,
pub created_at: DateTime<Utc>,
}
impl ProcessSpecification {
pub fn builder() -> ProcessSpecificationBuilder {
ProcessSpecificationBuilder::default()
}
}
#[derive(Debug, Default)]
pub struct ProcessSpecificationBuilder {
id: Option<String>,
name: Option<String>,
note: Option<String>,
classified_as: Vec<String>,
}
impl ProcessSpecificationBuilder {
pub fn id(mut self, id: impl Into<String>) -> Self {
self.id = Some(id.into());
self
}
pub fn name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
pub fn note(mut self, note: impl Into<String>) -> Self {
self.note = Some(note.into());
self
}
pub fn classified_as(mut self, classification: impl Into<String>) -> Self {
self.classified_as.push(classification.into());
self
}
pub fn build(self) -> Result<ProcessSpecification> {
let id = self.id.ok_or_else(|| Error::missing_field("id"))?;
let name = self.name.ok_or_else(|| Error::missing_field("name"))?;
Ok(ProcessSpecification {
id,
name,
note: self.note,
classified_as: self.classified_as,
created_at: Utc::now(),
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
pub enum ProcessStatus {
Planned,
InProgress,
Completed,
Cancelled,
}
impl Default for ProcessStatus {
fn default() -> Self {
ProcessStatus::Planned
}
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
pub struct Process {
pub id: String,
pub name: String,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub based_on: Option<String>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub planned_within: Option<String>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub has_beginning: Option<DateTime<Utc>>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub has_end: Option<DateTime<Utc>>,
pub finished: bool,
#[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>,
#[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "Vec::is_empty"))]
pub classified_as: Vec<String>,
pub status: ProcessStatus,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub nested_in: Option<String>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
impl Process {
pub fn builder() -> ProcessBuilder {
ProcessBuilder::default()
}
pub fn is_active(&self) -> bool {
matches!(self.status, ProcessStatus::InProgress)
}
pub fn is_complete(&self) -> bool {
matches!(self.status, ProcessStatus::Completed)
}
pub fn start(&mut self) {
if self.status == ProcessStatus::Planned {
self.status = ProcessStatus::InProgress;
if self.has_beginning.is_none() {
self.has_beginning = Some(Utc::now());
}
self.updated_at = Utc::now();
}
}
pub fn complete(&mut self) {
if self.status == ProcessStatus::InProgress {
self.status = ProcessStatus::Completed;
self.finished = true;
if self.has_end.is_none() {
self.has_end = Some(Utc::now());
}
self.updated_at = Utc::now();
}
}
pub fn cancel(&mut self) {
if !self.finished {
self.status = ProcessStatus::Cancelled;
self.finished = true;
self.updated_at = Utc::now();
}
}
pub fn duration(&self) -> Option<chrono::Duration> {
match (self.has_beginning, self.has_end) {
(Some(begin), Some(end)) => Some(end - begin),
_ => None,
}
}
}
#[derive(Debug, Default)]
pub struct ProcessBuilder {
id: Option<String>,
name: Option<String>,
based_on: Option<String>,
planned_within: Option<String>,
has_beginning: Option<DateTime<Utc>>,
has_end: Option<DateTime<Utc>>,
in_scope_of: Option<String>,
note: Option<String>,
classified_as: Vec<String>,
nested_in: Option<String>,
}
impl ProcessBuilder {
pub fn id(mut self, id: impl Into<String>) -> Self {
self.id = Some(id.into());
self
}
pub fn name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
pub fn based_on(mut self, spec_id: impl Into<String>) -> Self {
self.based_on = Some(spec_id.into());
self
}
pub fn planned_within(mut self, plan_id: impl Into<String>) -> Self {
self.planned_within = Some(plan_id.into());
self
}
pub fn has_beginning(mut self, time: DateTime<Utc>) -> Self {
self.has_beginning = Some(time);
self
}
pub fn has_end(mut self, time: DateTime<Utc>) -> Self {
self.has_end = Some(time);
self
}
pub fn in_scope_of(mut self, scope: impl Into<String>) -> Self {
self.in_scope_of = Some(scope.into());
self
}
pub fn note(mut self, note: impl Into<String>) -> Self {
self.note = Some(note.into());
self
}
pub fn classified_as(mut self, classification: impl Into<String>) -> Self {
self.classified_as.push(classification.into());
self
}
pub fn nested_in(mut self, process_id: impl Into<String>) -> Self {
self.nested_in = Some(process_id.into());
self
}
pub fn build(self) -> Result<Process> {
let id = self.id.ok_or_else(|| Error::missing_field("id"))?;
let name = self.name.ok_or_else(|| Error::missing_field("name"))?;
let now = Utc::now();
Ok(Process {
id,
name,
based_on: self.based_on,
planned_within: self.planned_within,
has_beginning: self.has_beginning,
has_end: self.has_end,
finished: false,
in_scope_of: self.in_scope_of,
note: self.note,
classified_as: self.classified_as,
status: ProcessStatus::Planned,
nested_in: self.nested_in,
created_at: now,
updated_at: now,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_process_specification_builder() {
let spec = ProcessSpecification::builder()
.id("spec-001")
.name("Baking")
.note("Process of baking bread")
.build()
.unwrap();
assert_eq!(spec.id, "spec-001");
assert_eq!(spec.name, "Baking");
}
#[test]
fn test_process_builder() {
let process = Process::builder()
.id("process-001")
.name("Bake Bread Batch #1")
.based_on("spec-001")
.build()
.unwrap();
assert_eq!(process.id, "process-001");
assert_eq!(process.status, ProcessStatus::Planned);
assert!(!process.finished);
}
#[test]
fn test_process_lifecycle() {
let mut process = Process::builder()
.id("process-001")
.name("Test Process")
.build()
.unwrap();
assert_eq!(process.status, ProcessStatus::Planned);
process.start();
assert_eq!(process.status, ProcessStatus::InProgress);
assert!(process.has_beginning.is_some());
process.complete();
assert_eq!(process.status, ProcessStatus::Completed);
assert!(process.finished);
assert!(process.has_end.is_some());
}
#[test]
fn test_process_cancel() {
let mut process = Process::builder()
.id("process-001")
.name("Test Process")
.build()
.unwrap();
process.start();
process.cancel();
assert_eq!(process.status, ProcessStatus::Cancelled);
assert!(process.finished);
}
#[cfg(feature = "serde")]
#[test]
fn test_process_serialization() {
let process = Process::builder()
.id("process-001")
.name("Test Process")
.based_on("spec-001")
.build()
.unwrap();
let json = serde_json::to_string(&process).unwrap();
let parsed: Process = serde_json::from_str(&json).unwrap();
assert_eq!(process.id, parsed.id);
assert_eq!(process.name, parsed.name);
}
}