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, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
pub enum IntentType {
Offer,
Request,
}
impl Default for IntentType {
fn default() -> Self {
IntentType::Offer
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
pub enum IntentStatus {
Active,
PartiallySatisfied,
Satisfied,
Cancelled,
Expired,
}
impl Default for IntentStatus {
fn default() -> Self {
IntentStatus::Active
}
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
pub struct Intent {
pub id: String,
pub action: ActionType,
pub intent_type: IntentType,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub provider: Option<String>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub receiver: 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 minimum_quantity: Option<Measure>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub available_quantity: Option<Measure>,
#[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_inventoried_as: Option<String>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub input_of: Option<String>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub output_of: Option<String>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub at_location: Option<String>,
#[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 due: Option<DateTime<Utc>>,
#[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>>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub has_point_in_time: Option<DateTime<Utc>>,
pub finished: bool,
#[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 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: IntentStatus,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub satisfied_quantity: Option<Measure>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
impl Intent {
pub fn builder() -> IntentBuilder {
IntentBuilder::default()
}
pub fn offer() -> IntentBuilder {
IntentBuilder::default().intent_type(IntentType::Offer)
}
pub fn request() -> IntentBuilder {
IntentBuilder::default().intent_type(IntentType::Request)
}
pub fn is_offer(&self) -> bool {
matches!(self.intent_type, IntentType::Offer)
}
pub fn is_request(&self) -> bool {
matches!(self.intent_type, IntentType::Request)
}
pub fn is_active(&self) -> bool {
matches!(
self.status,
IntentStatus::Active | IntentStatus::PartiallySatisfied
)
}
pub fn is_expired(&self) -> bool {
if let Some(end) = self.has_end {
end < Utc::now()
} else {
false
}
}
pub fn remaining_quantity(&self) -> Option<Measure> {
match (&self.resource_quantity, &self.satisfied_quantity) {
(Some(total), Some(satisfied)) => total.sub(satisfied),
(Some(total), None) => Some(total.clone()),
_ => self.available_quantity.clone(),
}
}
pub fn record_satisfaction(&mut self, quantity: &Measure) -> Result<()> {
if let Some(ref mut satisfied) = self.satisfied_quantity {
if !satisfied.same_unit(quantity) {
return Err(Error::UnitMismatch {
unit1: satisfied.unit.to_string(),
unit2: quantity.unit.to_string(),
});
}
*satisfied = satisfied.add(quantity).unwrap();
} else {
self.satisfied_quantity = Some(quantity.clone());
}
if let Some(ref total) = self.resource_quantity {
if let Some(ref satisfied) = self.satisfied_quantity {
if satisfied.value >= total.value {
self.status = IntentStatus::Satisfied;
self.finished = true;
} else {
self.status = IntentStatus::PartiallySatisfied;
}
}
}
self.updated_at = Utc::now();
Ok(())
}
pub fn cancel(&mut self) {
self.status = IntentStatus::Cancelled;
self.finished = true;
self.updated_at = Utc::now();
}
pub fn matches(&self, other: &Intent) -> bool {
if self.intent_type == other.intent_type {
return false;
}
if self.resource_conforms_to != other.resource_conforms_to {
return false;
}
if !self.is_active() || !other.is_active() {
return false;
}
if let (Some(self_qty), Some(other_qty)) =
(self.remaining_quantity(), other.remaining_quantity())
{
if !self_qty.same_unit(&other_qty) {
return false;
}
if self_qty.is_zero() || other_qty.is_zero() {
return false;
}
}
true
}
}
#[derive(Debug, Default)]
pub struct IntentBuilder {
id: Option<String>,
action: Option<ActionType>,
intent_type: Option<IntentType>,
provider: Option<String>,
receiver: Option<String>,
resource_quantity: Option<Measure>,
effort_quantity: Option<Measure>,
minimum_quantity: Option<Measure>,
available_quantity: Option<Measure>,
resource_conforms_to: Option<String>,
resource_inventoried_as: Option<String>,
input_of: Option<String>,
output_of: Option<String>,
at_location: Option<String>,
stage: Option<String>,
state: Option<String>,
due: Option<DateTime<Utc>>,
has_beginning: Option<DateTime<Utc>>,
has_end: Option<DateTime<Utc>>,
has_point_in_time: Option<DateTime<Utc>>,
image: Option<String>,
in_scope_of: Option<String>,
note: Option<String>,
classified_as: Vec<String>,
}
impl IntentBuilder {
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 intent_type(mut self, intent_type: IntentType) -> Self {
self.intent_type = Some(intent_type);
self
}
pub fn provider(mut self, agent_id: impl Into<String>) -> Self {
self.provider = Some(agent_id.into());
self
}
pub fn receiver(mut self, agent_id: impl Into<String>) -> Self {
self.receiver = Some(agent_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 minimum_quantity(mut self, quantity: Measure) -> Self {
self.minimum_quantity = Some(quantity);
self
}
pub fn available_quantity(mut self, quantity: Measure) -> Self {
self.available_quantity = Some(quantity);
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_inventoried_as(mut self, resource_id: impl Into<String>) -> Self {
self.resource_inventoried_as = Some(resource_id.into());
self
}
pub fn input_of(mut self, process_id: impl Into<String>) -> Self {
self.input_of = Some(process_id.into());
self
}
pub fn output_of(mut self, process_id: impl Into<String>) -> Self {
self.output_of = Some(process_id.into());
self
}
pub fn at_location(mut self, location: impl Into<String>) -> Self {
self.at_location = Some(location.into());
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 due(mut self, due: DateTime<Utc>) -> Self {
self.due = Some(due);
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 has_point_in_time(mut self, time: DateTime<Utc>) -> Self {
self.has_point_in_time = Some(time);
self
}
pub fn image(mut self, image: impl Into<String>) -> Self {
self.image = Some(image.into());
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 build(self) -> Result<Intent> {
let id = self.id.ok_or_else(|| Error::missing_field("id"))?;
let action = self.action.ok_or_else(|| Error::missing_field("action"))?;
let intent_type = self.intent_type.unwrap_or_default();
let now = Utc::now();
Ok(Intent {
id,
action,
intent_type,
provider: self.provider,
receiver: self.receiver,
resource_quantity: self.resource_quantity,
effort_quantity: self.effort_quantity,
minimum_quantity: self.minimum_quantity,
available_quantity: self.available_quantity,
resource_conforms_to: self.resource_conforms_to,
resource_inventoried_as: self.resource_inventoried_as,
input_of: self.input_of,
output_of: self.output_of,
at_location: self.at_location,
stage: self.stage,
state: self.state,
due: self.due,
has_beginning: self.has_beginning,
has_end: self.has_end,
has_point_in_time: self.has_point_in_time,
finished: false,
image: self.image,
in_scope_of: self.in_scope_of,
note: self.note,
classified_as: self.classified_as,
status: IntentStatus::Active,
satisfied_quantity: None,
created_at: now,
updated_at: now,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::measures::Unit;
#[test]
fn test_offer_builder() {
let offer = Intent::offer()
.id("intent-001")
.action(ActionType::Transfer)
.provider("agent-001")
.resource_quantity(Measure::new(100, Unit::Kilogram))
.resource_conforms_to("spec-001")
.build()
.unwrap();
assert!(offer.is_offer());
assert!(offer.is_active());
assert_eq!(offer.provider, Some("agent-001".to_string()));
}
#[test]
fn test_request_builder() {
let request = Intent::request()
.id("intent-002")
.action(ActionType::Transfer)
.receiver("agent-002")
.resource_quantity(Measure::new(50, Unit::Kilogram))
.resource_conforms_to("spec-001")
.build()
.unwrap();
assert!(request.is_request());
assert_eq!(request.receiver, Some("agent-002".to_string()));
}
#[test]
fn test_intent_matching() {
let offer = Intent::offer()
.id("intent-001")
.action(ActionType::Transfer)
.provider("agent-001")
.resource_quantity(Measure::new(100, Unit::Kilogram))
.resource_conforms_to("spec-001")
.build()
.unwrap();
let request = Intent::request()
.id("intent-002")
.action(ActionType::Transfer)
.receiver("agent-002")
.resource_quantity(Measure::new(50, Unit::Kilogram))
.resource_conforms_to("spec-001")
.build()
.unwrap();
assert!(offer.matches(&request));
assert!(request.matches(&offer));
}
#[test]
fn test_intent_no_match_same_type() {
let offer1 = Intent::offer()
.id("intent-001")
.action(ActionType::Transfer)
.provider("agent-001")
.resource_quantity(Measure::new(100, Unit::Kilogram))
.resource_conforms_to("spec-001")
.build()
.unwrap();
let offer2 = Intent::offer()
.id("intent-002")
.action(ActionType::Transfer)
.provider("agent-002")
.resource_quantity(Measure::new(50, Unit::Kilogram))
.resource_conforms_to("spec-001")
.build()
.unwrap();
assert!(!offer1.matches(&offer2));
}
#[test]
fn test_intent_satisfaction() {
let mut offer = Intent::offer()
.id("intent-001")
.action(ActionType::Transfer)
.provider("agent-001")
.resource_quantity(Measure::new(100, Unit::Each))
.build()
.unwrap();
offer
.record_satisfaction(&Measure::new(30, Unit::Each))
.unwrap();
assert_eq!(offer.status, IntentStatus::PartiallySatisfied);
assert_eq!(offer.remaining_quantity().unwrap().value, 70.into());
offer
.record_satisfaction(&Measure::new(70, Unit::Each))
.unwrap();
assert_eq!(offer.status, IntentStatus::Satisfied);
}
#[cfg(feature = "serde")]
#[test]
fn test_intent_serialization() {
let intent = Intent::offer()
.id("intent-001")
.action(ActionType::Transfer)
.provider("agent-001")
.resource_quantity(Measure::new(100, Unit::Each))
.build()
.unwrap();
let json = serde_json::to_string(&intent).unwrap();
let parsed: Intent = serde_json::from_str(&json).unwrap();
assert_eq!(intent.id, parsed.id);
assert_eq!(intent.intent_type, parsed.intent_type);
}
}