use crate::types::CruxId;
use chrono::prelude::*;
use edn_rs::Serialize;
static ACTION_DATE_FORMAT: &'static str = "%Y-%m-%dT%H:%M:%S%Z";
static DATETIME_FORMAT: &'static str = "%Y-%m-%dT%H:%M:%S";
#[derive(Debug, PartialEq, Clone)]
pub(crate) enum Action {
Put(String, Option<DateTime<FixedOffset>>),
Delete(String, Option<DateTime<FixedOffset>>),
Evict(String),
Match(String, String, Option<DateTime<FixedOffset>>),
}
#[cfg(feature = "mock")]
#[derive(Debug, PartialEq)]
pub enum ActionMock {
Put(String, Option<DateTime<FixedOffset>>),
Delete(String, Option<DateTime<FixedOffset>>),
Evict(String),
Match(String, String, Option<DateTime<FixedOffset>>),
}
#[derive(Debug, PartialEq, Clone)]
pub struct Actions {
actions: Vec<Action>,
}
impl Actions {
pub fn new() -> Self {
Self {
actions: Vec::new(),
}
}
pub(crate) fn is_empty(&self) -> bool {
self.actions.is_empty()
}
pub fn append_put<T: Serialize>(mut self, action: T) -> Self {
self.actions.push(Action::put(action));
self
}
pub fn append_put_timed<T: Serialize>(
mut self,
action: T,
date: DateTime<FixedOffset>,
) -> Self {
self.actions.push(Action::put(action).with_valid_date(date));
self
}
pub fn append_delete(mut self, id: CruxId) -> Self {
self.actions.push(Action::delete(id));
self
}
pub fn append_delete_timed(mut self, id: CruxId, date: DateTime<FixedOffset>) -> Self {
self.actions.push(Action::delete(id).with_valid_date(date));
self
}
pub fn append_evict(mut self, id: CruxId) -> Self {
self.actions.push(Action::evict(id));
self
}
pub fn append_match_doc<T: Serialize>(mut self, id: CruxId, action: T) -> Self {
self.actions.push(Action::match_doc(id, action));
self
}
pub fn append_match_doc_timed<T: Serialize>(
mut self,
id: CruxId,
action: T,
date: DateTime<FixedOffset>,
) -> Self {
self.actions
.push(Action::match_doc(id, action).with_valid_date(date));
self
}
pub(crate) fn build(self) -> String {
edn_rs::to_string(self.actions)
}
}
impl Action {
fn put<T: Serialize>(action: T) -> Action {
Action::Put(edn_rs::to_string(action), None)
}
fn with_valid_date(self, date: DateTime<FixedOffset>) -> Action {
match self {
Action::Put(action, _) => Action::Put(action, Some(date)),
Action::Delete(action, _) => Action::Delete(action, Some(date)),
Action::Match(id, action, _) => Action::Match(id, action, Some(date)),
action => action,
}
}
fn delete(id: CruxId) -> Action {
Action::Delete(edn_rs::to_string(id), None)
}
fn evict(id: CruxId) -> Action {
Action::Evict(edn_rs::to_string(id))
}
fn match_doc<T: Serialize>(id: CruxId, action: T) -> Action {
Action::Match(edn_rs::to_string(id), edn_rs::to_string(action), None)
}
}
impl Serialize for Action {
fn serialize(self) -> String {
match self {
Action::Put(edn, None) => format!("[:crux.tx/put {}]", edn),
Action::Put(edn, Some(date)) => format!(
"[:crux.tx/put {} #inst \"{}\"]",
edn,
date.format(ACTION_DATE_FORMAT).to_string()
),
Action::Delete(id, None) => format!("[:crux.tx/delete {}]", id),
Action::Delete(id, Some(date)) => format!(
"[:crux.tx/delete {} #inst \"{}\"]",
id,
date.format(ACTION_DATE_FORMAT).to_string()
),
Action::Evict(id) => {
if id.starts_with(":") {
format!("[:crux.tx/evict {}]", id)
} else {
"".to_string()
}
}
Action::Match(id, edn, None) => format!("[:crux.tx/match {} {}]", id, edn),
Action::Match(id, edn, Some(date)) => format!(
"[:crux.tx/match {} {} #inst \"{}\"]",
id,
edn,
date.format(ACTION_DATE_FORMAT).to_string()
),
}
}
}
#[derive(Debug, PartialEq)]
pub enum Order {
Asc,
Desc,
}
impl Serialize for Order {
fn serialize(self) -> String {
match self {
Order::Asc => String::from("asc"),
Order::Desc => String::from("desc"),
}
}
}
#[derive(Debug, PartialEq)]
pub enum TimeHistory {
ValidTime(Option<DateTime<Utc>>, Option<DateTime<Utc>>),
TransactionTime(Option<DateTime<Utc>>, Option<DateTime<Utc>>),
}
impl Serialize for TimeHistory {
fn serialize(self) -> String {
use crate::types::http::TimeHistory::TransactionTime;
use crate::types::http::TimeHistory::ValidTime;
match self {
ValidTime(Some(start), Some(end)) => format!(
"&start-valid-time={}&end-valid-time={}",
start.format(DATETIME_FORMAT).to_string(),
end.format(DATETIME_FORMAT).to_string()
),
ValidTime(None, Some(end)) => format!(
"&end-valid-time={}",
end.format(DATETIME_FORMAT).to_string()
),
ValidTime(Some(start), None) => format!(
"&start-valid-time={}",
start.format(DATETIME_FORMAT).to_string()
),
ValidTime(None, None) => format!(""),
TransactionTime(Some(start), Some(end)) => format!(
"&start-transaction-time={}&end-transaction-time={}",
start.format(DATETIME_FORMAT).to_string(),
end.format(DATETIME_FORMAT).to_string()
),
TransactionTime(None, Some(end)) => format!(
"&end-transaction-time={}",
end.format(DATETIME_FORMAT).to_string()
),
TransactionTime(Some(start), None) => format!(
"&start-transaction-time={}",
start.format(DATETIME_FORMAT).to_string()
),
TransactionTime(None, None) => format!(""),
}
}
}
#[doc(hidden)]
pub trait VecSer {
fn serialize(self) -> String;
}
#[doc(hidden)]
impl VecSer for Vec<TimeHistory> {
fn serialize(self) -> String {
if self.len() > 2 || self.len() == 0 {
String::new()
} else {
self.into_iter()
.map(edn_rs::to_string)
.collect::<Vec<String>>()
.join("")
}
}
}
#[cfg(feature = "mock")]
impl std::cmp::PartialEq<Vec<ActionMock>> for Actions {
fn eq(&self, other: &Vec<ActionMock>) -> bool {
self.actions
.iter()
.zip(other.iter())
.map(|(acs, acm)| match (acs, acm) {
(Action::Put(ap, tp), ActionMock::Put(am, tm)) if ap == am && tp == tm => true,
(Action::Evict(id), ActionMock::Evict(idm)) if id == idm => true,
(Action::Delete(id, tp), ActionMock::Delete(idm, tm)) if id == idm && tp == tm => {
true
}
(Action::Match(id, a, tp), ActionMock::Match(idm, am, tm))
if id == idm && a == am && tp == tm =>
{
true
}
_ => false,
})
.fold(true, |acc, e| acc && e)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::CruxId;
use edn_derive::Serialize;
#[test]
fn actions() {
let person1 = Person {
crux__db___id: CruxId::new("jorge-3"),
first_name: "Michael".to_string(),
last_name: "Jorge".to_string(),
};
let person2 = Person {
crux__db___id: CruxId::new("manuel-1"),
first_name: "Diego".to_string(),
last_name: "Manuel".to_string(),
};
let person3 = Person {
crux__db___id: CruxId::new("manuel-1"),
first_name: "Diego".to_string(),
last_name: "Manuel".to_string(),
};
let timed = "2014-11-28T21:00:09-09:00"
.parse::<DateTime<FixedOffset>>()
.unwrap();
let actions = Actions::new()
.append_put_timed(person1.clone(), timed)
.append_put(person2.clone())
.append_evict(person1.crux__db___id)
.append_delete(person2.crux__db___id)
.append_match_doc(person3.clone().crux__db___id, person3);
assert_eq!(actions.clone(), expected_actions());
}
fn expected_actions() -> Actions {
let person1 = Person {
crux__db___id: CruxId::new("jorge-3"),
first_name: "Michael".to_string(),
last_name: "Jorge".to_string(),
};
let person2 = Person {
crux__db___id: CruxId::new("manuel-1"),
first_name: "Diego".to_string(),
last_name: "Manuel".to_string(),
};
let person3 = Person {
crux__db___id: CruxId::new("manuel-1"),
first_name: "Diego".to_string(),
last_name: "Manuel".to_string(),
};
Actions {
actions: vec![
Action::Put(
person1.clone().serialize(),
Some(
"2014-11-28T21:00:09-09:00"
.parse::<DateTime<FixedOffset>>()
.unwrap(),
),
),
Action::Put(person2.clone().serialize(), None),
Action::Evict(person1.crux__db___id.serialize()),
Action::Delete(person2.crux__db___id.serialize(), None),
Action::Match(
person3.clone().crux__db___id.serialize(),
person3.serialize(),
None,
),
],
}
}
#[derive(Debug, Clone, Serialize)]
#[allow(non_snake_case)]
pub struct Person {
crux__db___id: CruxId,
first_name: String,
last_name: String,
}
}