use crate::error::{Error, Result};
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 ProposalStatus {
Draft,
Open,
Accepted,
Rejected,
Expired,
Withdrawn,
}
impl Default for ProposalStatus {
fn default() -> Self {
ProposalStatus::Draft
}
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
pub struct ProposedIntent {
pub id: String,
pub publishes: String,
pub published_in: String,
pub reciprocal: bool,
pub created_at: DateTime<Utc>,
}
impl ProposedIntent {
pub fn new(
id: impl Into<String>,
publishes: impl Into<String>,
published_in: impl Into<String>,
reciprocal: bool,
) -> Self {
ProposedIntent {
id: id.into(),
publishes: publishes.into(),
published_in: published_in.into(),
reciprocal,
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 ProposedTo {
pub id: String,
pub proposed_to: String,
pub proposed: String,
pub created_at: DateTime<Utc>,
}
impl ProposedTo {
pub fn new(
id: impl Into<String>,
proposed_to: impl Into<String>,
proposed: impl Into<String>,
) -> Self {
ProposedTo {
id: id.into(),
proposed_to: proposed_to.into(),
proposed: proposed.into(),
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 Proposal {
pub id: String,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub name: Option<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 eligible_location: Option<String>,
pub status: ProposalStatus,
#[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 unit_based: Option<bool>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub unit_of_measure: 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>,
pub updated_at: DateTime<Utc>,
}
impl Proposal {
pub fn builder() -> ProposalBuilder {
ProposalBuilder::default()
}
pub fn publish(&mut self) {
if self.status == ProposalStatus::Draft {
self.status = ProposalStatus::Open;
self.has_beginning = Some(Utc::now());
self.updated_at = Utc::now();
}
}
pub fn accept(&mut self) {
if self.status == ProposalStatus::Open {
self.status = ProposalStatus::Accepted;
self.updated_at = Utc::now();
}
}
pub fn reject(&mut self) {
if self.status == ProposalStatus::Open {
self.status = ProposalStatus::Rejected;
self.updated_at = Utc::now();
}
}
pub fn withdraw(&mut self) {
if matches!(self.status, ProposalStatus::Draft | ProposalStatus::Open) {
self.status = ProposalStatus::Withdrawn;
self.updated_at = Utc::now();
}
}
pub fn expire(&mut self) {
if self.status == ProposalStatus::Open {
self.status = ProposalStatus::Expired;
self.has_end = Some(Utc::now());
self.updated_at = Utc::now();
}
}
pub fn is_open(&self) -> bool {
matches!(self.status, ProposalStatus::Open)
}
pub fn is_expired(&self) -> bool {
if let Some(end) = self.has_end {
end < Utc::now()
} else {
false
}
}
}
#[derive(Debug, Default)]
pub struct ProposalBuilder {
id: Option<String>,
name: Option<String>,
description: Option<String>,
eligible_location: Option<String>,
has_beginning: Option<DateTime<Utc>>,
has_end: Option<DateTime<Utc>>,
unit_based: Option<bool>,
unit_of_measure: Option<String>,
in_scope_of: Option<String>,
note: Option<String>,
}
impl ProposalBuilder {
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 description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
pub fn eligible_location(mut self, location: impl Into<String>) -> Self {
self.eligible_location = Some(location.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 unit_based(mut self, unit_based: bool) -> Self {
self.unit_based = Some(unit_based);
self
}
pub fn unit_of_measure(mut self, unit: impl Into<String>) -> Self {
self.unit_of_measure = Some(unit.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 build(self) -> Result<Proposal> {
let id = self.id.ok_or_else(|| Error::missing_field("id"))?;
let now = Utc::now();
Ok(Proposal {
id,
name: self.name,
description: self.description,
eligible_location: self.eligible_location,
status: ProposalStatus::Draft,
has_beginning: self.has_beginning,
has_end: self.has_end,
unit_based: self.unit_based,
unit_of_measure: self.unit_of_measure,
in_scope_of: self.in_scope_of,
note: self.note,
created_at: now,
updated_at: now,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_proposal_builder() {
let proposal = Proposal::builder()
.id("proposal-001")
.name("Wheat for Sale")
.description("100 bushels of organic wheat")
.build()
.unwrap();
assert_eq!(proposal.id, "proposal-001");
assert_eq!(proposal.name, Some("Wheat for Sale".to_string()));
assert_eq!(proposal.status, ProposalStatus::Draft);
}
#[test]
fn test_proposal_lifecycle() {
let mut proposal = Proposal::builder()
.id("proposal-001")
.name("Test Proposal")
.build()
.unwrap();
assert_eq!(proposal.status, ProposalStatus::Draft);
proposal.publish();
assert!(proposal.is_open());
assert!(proposal.has_beginning.is_some());
proposal.accept();
assert_eq!(proposal.status, ProposalStatus::Accepted);
}
#[test]
fn test_proposal_withdraw() {
let mut proposal = Proposal::builder()
.id("proposal-001")
.name("Test Proposal")
.build()
.unwrap();
proposal.withdraw();
assert_eq!(proposal.status, ProposalStatus::Withdrawn);
}
#[test]
fn test_proposed_intent() {
let proposed_intent = ProposedIntent::new(
"pi-001",
"intent-001",
"proposal-001",
false,
);
assert_eq!(proposed_intent.publishes, "intent-001");
assert_eq!(proposed_intent.published_in, "proposal-001");
assert!(!proposed_intent.reciprocal);
}
#[test]
fn test_proposed_to() {
let proposed_to = ProposedTo::new(
"pt-001",
"agent-001",
"proposal-001",
);
assert_eq!(proposed_to.proposed_to, "agent-001");
assert_eq!(proposed_to.proposed, "proposal-001");
}
#[cfg(feature = "serde")]
#[test]
fn test_proposal_serialization() {
let proposal = Proposal::builder()
.id("proposal-001")
.name("Test Proposal")
.build()
.unwrap();
let json = serde_json::to_string(&proposal).unwrap();
let parsed: Proposal = serde_json::from_str(&json).unwrap();
assert_eq!(proposal.id, parsed.id);
}
}