rvf 0.1.0

Rust implementation of the ValueFlows vocabulary for distributed economic networks
Documentation
//! Processes - transformations of economic resources
//!
//! Processes represent economic activities that transform inputs into outputs.
//! They are the nodes in the production value flow graph.

use crate::error::{Error, Result};
use chrono::{DateTime, Utc};

#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};

/// A specification for a type of process
///
/// Process specifications define recipes or templates for processes.
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
pub struct ProcessSpecification {
    /// Unique identifier
    pub id: String,
    /// Display name
    pub name: String,
    /// Optional note/description
    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
    pub note: Option<String>,
    /// Classifications
    #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "Vec::is_empty"))]
    pub classified_as: Vec<String>,
    /// When this was created
    pub created_at: DateTime<Utc>,
}

impl ProcessSpecification {
    /// Create a new process specification builder
    pub fn builder() -> ProcessSpecificationBuilder {
        ProcessSpecificationBuilder::default()
    }
}

/// Builder for ProcessSpecification
#[derive(Debug, Default)]
pub struct ProcessSpecificationBuilder {
    id: Option<String>,
    name: Option<String>,
    note: Option<String>,
    classified_as: Vec<String>,
}

impl ProcessSpecificationBuilder {
    /// Set the ID
    pub fn id(mut self, id: impl Into<String>) -> Self {
        self.id = Some(id.into());
        self
    }

    /// Set the name
    pub fn name(mut self, name: impl Into<String>) -> Self {
        self.name = Some(name.into());
        self
    }

    /// Set a note
    pub fn note(mut self, note: impl Into<String>) -> Self {
        self.note = Some(note.into());
        self
    }

    /// Add a classification
    pub fn classified_as(mut self, classification: impl Into<String>) -> Self {
        self.classified_as.push(classification.into());
        self
    }

    /// Build the ProcessSpecification
    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(),
        })
    }
}

/// Status of a process
#[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 {
    /// The process is planned but not started
    Planned,
    /// The process is in progress
    InProgress,
    /// The process has been completed
    Completed,
    /// The process was cancelled
    Cancelled,
}

impl Default for ProcessStatus {
    fn default() -> Self {
        ProcessStatus::Planned
    }
}

/// A process - an activity that transforms inputs into outputs
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
pub struct Process {
    /// Unique identifier
    pub id: String,
    /// Display name
    pub name: String,
    /// The process specification this is based on
    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
    pub based_on: Option<String>,
    /// Plan this process is part of
    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
    pub planned_within: Option<String>,
    /// When the process is planned to start
    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
    pub has_beginning: Option<DateTime<Utc>>,
    /// When the process is planned to end
    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
    pub has_end: Option<DateTime<Utc>>,
    /// Whether the process is finished
    pub finished: bool,
    /// Scope (organization context)
    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
    pub in_scope_of: Option<String>,
    /// Optional note
    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
    pub note: Option<String>,
    /// Classifications
    #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "Vec::is_empty"))]
    pub classified_as: Vec<String>,
    /// Current status
    pub status: ProcessStatus,
    /// Nested within another process
    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
    pub nested_in: Option<String>,
    /// When this was created
    pub created_at: DateTime<Utc>,
    /// When this was last updated
    pub updated_at: DateTime<Utc>,
}

impl Process {
    /// Create a new process builder
    pub fn builder() -> ProcessBuilder {
        ProcessBuilder::default()
    }

    /// Check if the process is active
    pub fn is_active(&self) -> bool {
        matches!(self.status, ProcessStatus::InProgress)
    }

    /// Check if the process is complete
    pub fn is_complete(&self) -> bool {
        matches!(self.status, ProcessStatus::Completed)
    }

    /// Start the process
    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();
        }
    }

    /// Complete the process
    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();
        }
    }

    /// Cancel the process
    pub fn cancel(&mut self) {
        if !self.finished {
            self.status = ProcessStatus::Cancelled;
            self.finished = true;
            self.updated_at = Utc::now();
        }
    }

    /// Get the duration if both beginning and end are set
    pub fn duration(&self) -> Option<chrono::Duration> {
        match (self.has_beginning, self.has_end) {
            (Some(begin), Some(end)) => Some(end - begin),
            _ => None,
        }
    }
}

/// Builder for Process
#[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 {
    /// Set the ID
    pub fn id(mut self, id: impl Into<String>) -> Self {
        self.id = Some(id.into());
        self
    }

    /// Set the name
    pub fn name(mut self, name: impl Into<String>) -> Self {
        self.name = Some(name.into());
        self
    }

    /// Set the process specification
    pub fn based_on(mut self, spec_id: impl Into<String>) -> Self {
        self.based_on = Some(spec_id.into());
        self
    }

    /// Set the plan
    pub fn planned_within(mut self, plan_id: impl Into<String>) -> Self {
        self.planned_within = Some(plan_id.into());
        self
    }

    /// Set the beginning time
    pub fn has_beginning(mut self, time: DateTime<Utc>) -> Self {
        self.has_beginning = Some(time);
        self
    }

    /// Set the end time
    pub fn has_end(mut self, time: DateTime<Utc>) -> Self {
        self.has_end = Some(time);
        self
    }

    /// Set the scope
    pub fn in_scope_of(mut self, scope: impl Into<String>) -> Self {
        self.in_scope_of = Some(scope.into());
        self
    }

    /// Set a note
    pub fn note(mut self, note: impl Into<String>) -> Self {
        self.note = Some(note.into());
        self
    }

    /// Add a classification
    pub fn classified_as(mut self, classification: impl Into<String>) -> Self {
        self.classified_as.push(classification.into());
        self
    }

    /// Set the parent process
    pub fn nested_in(mut self, process_id: impl Into<String>) -> Self {
        self.nested_in = Some(process_id.into());
        self
    }

    /// Build the Process
    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);
    }
}