Skip to main content

corium_client/
tx.rs

1//! Transaction data as builder-patterned values.
2//!
3//! A [`TxBuilder`] assembles the Datomic-dialect transaction forms corium
4//! accepts — map forms with `:db/id`, and the list ops `[:db/add …]`,
5//! `[:db/retract …]`, `[:db/cas …]`, `[:db/retractEntity …]` — into a
6//! [`TxData`] value ready to submit through a [`crate::Peer`].
7//!
8//! ```
9//! use corium_client::tx::{TxBuilder, EntityMap, tempid, lookup};
10//!
11//! let tx = TxBuilder::new()
12//!     .entity(
13//!         EntityMap::with_id(tempid("alice"))
14//!             .set("person/name", "Alice")
15//!             .set("person/age", 39_i64),
16//!     )
17//!     .add(lookup("person/email", "bob@example.com"), "person/age", 40_i64)
18//!     .build();
19//! ```
20
21use corium_query::edn::Edn;
22
23use crate::value::IntoEdn;
24
25/// A string tempid such as `"alice"`, unified with an allocated entity id
26/// after the transaction commits.
27#[must_use]
28pub fn tempid(name: impl Into<String>) -> Edn {
29    Edn::Str(name.into())
30}
31
32/// A lookup ref `[:attr value]` naming an existing entity by a unique
33/// attribute value.
34#[must_use]
35pub fn lookup(attr: &str, value: impl IntoEdn) -> Edn {
36    Edn::Vector(vec![Edn::keyword(attr), value.into_edn()])
37}
38
39/// An `#eid` reference to a raw entity id, for entity positions where a bare
40/// long would be ambiguous.
41#[must_use]
42pub fn eid(id: impl IntoEdn) -> Edn {
43    Edn::Tagged("eid".into(), Box::new(id.into_edn()))
44}
45
46/// A map-form entity: `{:db/id … :attr value …}`. Without an id the
47/// transactor allocates a fresh entity.
48#[derive(Clone, Debug, Default, Eq, PartialEq)]
49pub struct EntityMap {
50    id: Option<Edn>,
51    pairs: Vec<(Edn, Edn)>,
52}
53
54impl EntityMap {
55    /// A map-form entity with no `:db/id` (the transactor allocates one).
56    #[must_use]
57    pub fn new() -> Self {
58        Self::default()
59    }
60
61    /// A map-form entity with an explicit `:db/id` (a tempid, entity id,
62    /// ident, or lookup ref).
63    #[must_use]
64    pub fn with_id(id: impl IntoEdn) -> Self {
65        Self {
66            id: Some(id.into_edn()),
67            pairs: Vec::new(),
68        }
69    }
70
71    /// Sets a single-valued attribute.
72    #[must_use]
73    pub fn set(mut self, attr: &str, value: impl IntoEdn) -> Self {
74        self.pairs.push((Edn::keyword(attr), value.into_edn()));
75        self
76    }
77
78    /// Sets a cardinality-many attribute to a vector of values.
79    #[must_use]
80    pub fn set_many<V: IntoEdn>(mut self, attr: &str, values: impl IntoIterator<Item = V>) -> Self {
81        let values = values.into_iter().map(IntoEdn::into_edn).collect();
82        self.pairs.push((Edn::keyword(attr), Edn::Vector(values)));
83        self
84    }
85
86    fn into_edn(self) -> Edn {
87        let mut pairs = Vec::with_capacity(self.pairs.len() + 1);
88        if let Some(id) = self.id {
89            pairs.push((Edn::keyword("db/id"), id));
90        }
91        pairs.extend(self.pairs);
92        Edn::Map(pairs)
93    }
94}
95
96/// A completed set of transaction forms, ready to submit through a
97/// [`crate::Peer`].
98#[derive(Clone, Debug, Default, Eq, PartialEq)]
99pub struct TxData(Vec<Edn>);
100
101impl TxData {
102    /// The transaction forms.
103    #[must_use]
104    pub fn forms(&self) -> &[Edn] {
105        &self.0
106    }
107
108    /// Consumes the value, yielding the transaction forms.
109    #[must_use]
110    pub fn into_forms(self) -> Vec<Edn> {
111        self.0
112    }
113}
114
115impl From<Vec<Edn>> for TxData {
116    fn from(forms: Vec<Edn>) -> Self {
117        Self(forms)
118    }
119}
120
121/// Builds a [`TxData`] value from transaction forms.
122#[derive(Clone, Debug, Default)]
123pub struct TxBuilder {
124    forms: Vec<Edn>,
125}
126
127impl TxBuilder {
128    /// An empty transaction.
129    #[must_use]
130    pub fn new() -> Self {
131        Self::default()
132    }
133
134    /// Adds a map-form entity.
135    #[must_use]
136    pub fn entity(mut self, entity: EntityMap) -> Self {
137        self.forms.push(entity.into_edn());
138        self
139    }
140
141    /// Asserts a fact: `[:db/add e a v]`.
142    #[must_use]
143    pub fn add(mut self, e: impl IntoEdn, a: &str, v: impl IntoEdn) -> Self {
144        self.forms.push(Edn::Vector(vec![
145            Edn::keyword("db/add"),
146            e.into_edn(),
147            Edn::keyword(a),
148            v.into_edn(),
149        ]));
150        self
151    }
152
153    /// Retracts a fact: `[:db/retract e a v]`.
154    #[must_use]
155    pub fn retract(mut self, e: impl IntoEdn, a: &str, v: impl IntoEdn) -> Self {
156        self.forms.push(Edn::Vector(vec![
157            Edn::keyword("db/retract"),
158            e.into_edn(),
159            Edn::keyword(a),
160            v.into_edn(),
161        ]));
162        self
163    }
164
165    /// Compare-and-swap: `[:db/cas e a old new]`.
166    #[must_use]
167    pub fn cas(mut self, e: impl IntoEdn, a: &str, old: impl IntoEdn, new: impl IntoEdn) -> Self {
168        self.forms.push(Edn::Vector(vec![
169            Edn::keyword("db/cas"),
170            e.into_edn(),
171            Edn::keyword(a),
172            old.into_edn(),
173            new.into_edn(),
174        ]));
175        self
176    }
177
178    /// Retracts a whole entity: `[:db/retractEntity e]`.
179    #[must_use]
180    pub fn retract_entity(mut self, e: impl IntoEdn) -> Self {
181        self.forms.push(Edn::Vector(vec![
182            Edn::keyword("db/retractEntity"),
183            e.into_edn(),
184        ]));
185        self
186    }
187
188    /// Appends a raw transaction form (an escape hatch for forms the typed
189    /// builder does not model, e.g. `:db/fn` invocations).
190    #[must_use]
191    pub fn form(mut self, form: Edn) -> Self {
192        self.forms.push(form);
193        self
194    }
195
196    /// Finalizes the transaction.
197    #[must_use]
198    pub fn build(self) -> TxData {
199        TxData(self.forms)
200    }
201}
202
203#[cfg(test)]
204mod tests {
205    use super::*;
206
207    #[test]
208    fn map_and_list_forms_render() {
209        let tx = TxBuilder::new()
210            .entity(
211                EntityMap::with_id(tempid("alice"))
212                    .set("person/name", "Alice")
213                    .set_many("person/aliases", ["Al", "Ali"]),
214            )
215            .add(1_i64, "person/age", 40_i64)
216            .retract(
217                lookup("person/email", "bob@example.com"),
218                "person/age",
219                39_i64,
220            )
221            .retract_entity(eid(2_i64))
222            .build();
223        let forms = tx.into_forms();
224        assert_eq!(forms.len(), 4);
225        assert_eq!(
226            forms[0].to_string(),
227            "{:db/id \"alice\", :person/name \"Alice\", :person/aliases [\"Al\" \"Ali\"]}"
228        );
229        assert_eq!(forms[1].to_string(), "[:db/add 1 :person/age 40]");
230        assert_eq!(
231            forms[2].to_string(),
232            "[:db/retract [:person/email \"bob@example.com\"] :person/age 39]"
233        );
234        assert_eq!(forms[3].to_string(), "[:db/retractEntity #eid 2]");
235    }
236}