rvf 0.1.0

Rust implementation of the ValueFlows vocabulary for distributed economic networks
Documentation
//! Agents: people, organizations, and ecological agents
//!
//! Agents are the actors in the ValueFlows economic network. They can be
//! individuals, organizations, or even ecological systems that participate
//! in economic activity.

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

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

/// Type of agent
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
pub enum AgentType {
    /// An individual person
    Person,
    /// A formal or informal organization
    Organization,
    /// An ecological agent (ecosystem, forest, etc.)
    EcologicalAgent,
}

impl Default for AgentType {
    fn default() -> Self {
        AgentType::Person
    }
}

/// An economic agent
///
/// Agents are people, organizations, or ecological agents that participate
/// in economic activity. They can perform economic events, make commitments,
/// and have relationships with resources.
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
pub struct Agent {
    /// Unique identifier for this agent
    pub id: String,
    /// The type of agent
    pub agent_type: AgentType,
    /// Display name
    pub name: String,
    /// Optional image URL
    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
    pub image: Option<String>,
    /// Optional note/description
    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
    pub note: Option<String>,
    /// Primary location identifier
    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
    pub primary_location: Option<String>,
    /// Classifications/categories for this agent
    #[cfg_attr(
        feature = "serde",
        serde(default, skip_serializing_if = "Vec::is_empty")
    )]
    pub classified_as: Vec<String>,
    /// When this agent record was created
    pub created_at: DateTime<Utc>,
    /// When this agent record was last updated
    pub updated_at: DateTime<Utc>,
}

impl Agent {
    /// Create a new agent builder
    pub fn builder() -> AgentBuilder {
        AgentBuilder::default()
    }

    /// Check if this agent is a person
    pub fn is_person(&self) -> bool {
        matches!(self.agent_type, AgentType::Person)
    }

    /// Check if this agent is an organization
    pub fn is_organization(&self) -> bool {
        matches!(self.agent_type, AgentType::Organization)
    }

    /// Check if this agent is an ecological agent
    pub fn is_ecological(&self) -> bool {
        matches!(self.agent_type, AgentType::EcologicalAgent)
    }
}

/// Builder for creating Agent instances
#[derive(Debug, Default)]
pub struct AgentBuilder {
    id: Option<String>,
    agent_type: Option<AgentType>,
    name: Option<String>,
    image: Option<String>,
    note: Option<String>,
    primary_location: Option<String>,
    classified_as: Vec<String>,
}

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

    /// Set the agent type
    pub fn agent_type(mut self, agent_type: AgentType) -> Self {
        self.agent_type = Some(agent_type);
        self
    }

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

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

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

    /// Set the primary location
    pub fn primary_location(mut self, location: impl Into<String>) -> Self {
        self.primary_location = Some(location.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 Agent
    pub fn build(self) -> Result<Agent> {
        let id = self.id.ok_or_else(|| Error::missing_field("id"))?;
        let name = self.name.ok_or_else(|| Error::missing_field("name"))?;
        let agent_type = self.agent_type.unwrap_or_default();
        let now = Utc::now();

        Ok(Agent {
            id,
            agent_type,
            name,
            image: self.image,
            note: self.note,
            primary_location: self.primary_location,
            classified_as: self.classified_as,
            created_at: now,
            updated_at: now,
        })
    }
}

/// The type of relationship between agents
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
pub enum AgentRelationshipType {
    /// Subject is a member of the object organization
    MemberOf,
    /// Subject is an employee of the object organization  
    EmployedBy,
    /// Subject is a supplier to the object
    SupplierOf,
    /// Subject is a customer of the object
    CustomerOf,
    /// Subject is a partner of the object
    PartnerOf,
    /// Subject is a subsidiary of the object
    SubsidiaryOf,
    /// Subject is an affiliate of the object
    AffiliateOf,
    /// Custom relationship type
    Custom(String),
}

/// A relationship between two agents
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
pub struct AgentRelationship {
    /// Unique identifier
    pub id: String,
    /// The subject agent (the one that has the relationship)
    pub subject: String,
    /// The object agent (the one the relationship is to)
    pub object: String,
    /// The type of relationship
    pub relationship_type: AgentRelationshipType,
    /// Optional note
    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
    pub note: Option<String>,
    /// When the relationship started
    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
    pub in_scope_of: Option<String>,
    /// When this was created
    pub created_at: DateTime<Utc>,
}

impl AgentRelationship {
    /// Create a new agent relationship builder
    pub fn builder() -> AgentRelationshipBuilder {
        AgentRelationshipBuilder::default()
    }
}

/// Builder for AgentRelationship
#[derive(Debug, Default)]
pub struct AgentRelationshipBuilder {
    id: Option<String>,
    subject: Option<String>,
    object: Option<String>,
    relationship_type: Option<AgentRelationshipType>,
    note: Option<String>,
    in_scope_of: Option<String>,
}

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

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

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

    /// Set the relationship type
    pub fn relationship_type(mut self, rel_type: AgentRelationshipType) -> Self {
        self.relationship_type = Some(rel_type);
        self
    }

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

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

    /// Build the AgentRelationship
    pub fn build(self) -> Result<AgentRelationship> {
        let id = self.id.ok_or_else(|| Error::missing_field("id"))?;
        let subject = self
            .subject
            .ok_or_else(|| Error::missing_field("subject"))?;
        let object = self.object.ok_or_else(|| Error::missing_field("object"))?;
        let relationship_type = self
            .relationship_type
            .ok_or_else(|| Error::missing_field("relationship_type"))?;

        Ok(AgentRelationship {
            id,
            subject,
            object,
            relationship_type,
            note: self.note,
            in_scope_of: self.in_scope_of,
            created_at: Utc::now(),
        })
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_agent_builder() {
        let agent = Agent::builder()
            .id("agent-001")
            .name("Test Person")
            .agent_type(AgentType::Person)
            .note("A test agent")
            .build()
            .unwrap();

        assert_eq!(agent.id, "agent-001");
        assert_eq!(agent.name, "Test Person");
        assert!(agent.is_person());
        assert_eq!(agent.note, Some("A test agent".to_string()));
    }

    #[test]
    fn test_agent_builder_missing_required() {
        let result = Agent::builder().id("test").build();
        assert!(result.is_err());
    }

    #[test]
    fn test_agent_relationship() {
        let rel = AgentRelationship::builder()
            .id("rel-001")
            .subject("agent-001")
            .object("agent-002")
            .relationship_type(AgentRelationshipType::MemberOf)
            .build()
            .unwrap();

        assert_eq!(rel.subject, "agent-001");
        assert_eq!(rel.object, "agent-002");
    }

    #[cfg(feature = "serde")]
    #[test]
    fn test_agent_serialization() {
        let agent = Agent::builder()
            .id("agent-001")
            .name("Test")
            .agent_type(AgentType::Organization)
            .build()
            .unwrap();

        let json = serde_json::to_string(&agent).unwrap();
        let parsed: Agent = serde_json::from_str(&json).unwrap();
        assert_eq!(agent.id, parsed.id);
    }
}