1use mino_rs::collections::map::PMap;
9use mino_rs::error::throw_str;
10use mino_rs::symbol::Symbol;
11use mino_rs::{Gc, Throw, Value};
12
13#[derive(Clone, Copy, Debug)]
18pub struct DbRef {
19 pub conn: i64,
20 pub basis_tx: i64,
21 pub as_of: Option<i64>,
22 pub since: Option<i64>,
23}
24
25#[derive(Clone, Debug, Default)]
30pub struct TxReport {
31 pub tx_id: i64,
32 pub tempids: Vec<(String, i64)>,
33}
34
35pub fn kw_ns(ns: &str, name: &str) -> Value {
37 Value::Keyword(Symbol::namespaced(ns, name))
38}
39
40pub fn str_val(s: &str) -> Value {
42 Value::Str(Gc::new(s.to_string()))
43}
44
45pub fn db_value(conn: i64, basis_tx: i64, as_of: Option<i64>, since: Option<i64>) -> Value {
49 let opt = |o: Option<i64>| o.map(Value::Int).unwrap_or(Value::Nil);
50 let m = PMap::empty()
51 .assoc(kw_ns("mentat.store", "db"), Value::Bool(true))
52 .assoc(kw_ns("mentat.store", "conn"), Value::Int(conn))
53 .assoc(kw_ns("mentat.store", "basis-tx"), Value::Int(basis_tx))
54 .assoc(kw_ns("mentat.store", "as-of"), opt(as_of))
55 .assoc(kw_ns("mentat.store", "since"), opt(since));
56 Value::Map(Gc::new(m))
57}
58
59pub fn destructure_db(prim: &str, arg: Option<&Value>) -> Result<DbRef, Throw> {
63 match arg {
64 Some(Value::Int(n)) => Ok(DbRef {
65 conn: *n,
66 basis_tx: 0,
67 as_of: None,
68 since: None,
69 }),
70 Some(Value::Map(m)) => {
71 let conn = match m.get(&kw_ns("mentat.store", "conn")) {
72 Some(Value::Int(n)) => *n,
73 _ => {
74 return Err(throw_str(&format!(
75 "{prim}: db value missing :mentat.store/conn"
76 )))
77 }
78 };
79 let basis_tx = match m.get(&kw_ns("mentat.store", "basis-tx")) {
80 Some(Value::Int(n)) => *n,
81 _ => 0,
82 };
83 let opt = |k: &str| match m.get(&kw_ns("mentat.store", k)) {
84 Some(Value::Int(n)) => Some(*n),
85 _ => None,
86 };
87 Ok(DbRef {
88 conn,
89 basis_tx,
90 as_of: opt("as-of"),
91 since: opt("since"),
92 })
93 }
94 _ => Err(throw_str(&format!(
95 "{prim}: first arg must be a db value (from mentat.store/db) or a conn handle"
96 ))),
97 }
98}
99
100pub fn tx_report_value(report: &TxReport, db_after: Option<Value>) -> Value {
104 let mut tempids = PMap::empty();
105 for (k, v) in &report.tempids {
106 tempids = tempids.assoc(str_val(k), Value::Int(*v));
107 }
108 let mut m = PMap::empty()
109 .assoc(kw_ns("mentat.store", "tx-id"), Value::Int(report.tx_id))
110 .assoc(
111 kw_ns("mentat.store", "tempids"),
112 Value::Map(Gc::new(tempids)),
113 );
114 if let Some(db) = db_after {
115 m = m.assoc(kw_ns("mentat.store", "db-after"), db);
116 }
117 Value::Map(Gc::new(m))
118}
119
120pub fn inst_value(rfc3339: &str) -> Value {
125 cons_call(
126 Symbol::namespaced("clojure.instant", "read-instant-date"),
127 str_val(rfc3339),
128 )
129}
130
131pub fn uuid_value(uuid: &str) -> Value {
136 match parse_uuid_bytes(uuid) {
137 Some(bytes) => Value::Uuid(Gc::new(mino_rs::value::UuidVal(bytes))),
138 None => cons_call(Symbol::plain("parse-uuid"), str_val(uuid)),
139 }
140}
141
142fn parse_uuid_bytes(s: &str) -> Option<[u8; 16]> {
145 if s.len() != 36 || s.as_bytes().iter().filter(|&&b| b == b'-').count() != 4 {
146 return None;
147 }
148 let hex: String = s.chars().filter(|&c| c != '-').collect();
149 if hex.len() != 32 {
150 return None;
151 }
152 let mut bytes = [0u8; 16];
153 for (i, b) in bytes.iter_mut().enumerate() {
154 *b = u8::from_str_radix(&hex[i * 2..i * 2 + 2], 16).ok()?;
155 }
156 Some(bytes)
157}
158
159fn cons_call(sym: Symbol, arg: Value) -> Value {
161 Value::Cons(Gc::new((
162 Value::Sym(sym),
163 Value::Cons(Gc::new((arg, Value::EmptyList))),
164 )))
165}
166
167#[cfg(test)]
168mod tests {
169 use super::*;
170 use mino_rs::printer::print_str;
171 use mino_rs::reader::read_one;
172
173 #[test]
174 fn uuid_builder_round_trips_through_the_reader() {
175 let lit = "12345678-1234-5678-1234-567812345678";
177 let built = uuid_value(lit);
178 assert_eq!(print_str(&built), format!("#uuid \"{lit}\""));
179 let (from_lit, _) = read_one(&format!("#uuid \"{lit}\"")).unwrap();
180 assert_eq!(print_str(&from_lit), print_str(&built));
181 }
182
183 #[test]
184 fn inst_builder_matches_the_reader_form() {
185 let built = inst_value("2017-01-01T00:00:00Z");
186 assert_eq!(
187 print_str(&built),
188 "(clojure.instant/read-instant-date \"2017-01-01T00:00:00Z\")"
189 );
190 let (from_lit, _) = read_one(r#"#inst "2017-01-01T00:00:00Z""#).unwrap();
191 assert_eq!(print_str(&from_lit), print_str(&built));
192 }
193
194 #[test]
195 fn db_value_round_trips_through_destructure() {
196 let v = db_value(1, 42, Some(7), None);
197 let d = destructure_db("t", Some(&v)).unwrap();
198 assert_eq!(
199 (d.conn, d.basis_tx, d.as_of, d.since),
200 (1, 42, Some(7), None)
201 );
202 let d2 = destructure_db("t", Some(&Value::Int(3))).unwrap();
204 assert_eq!((d2.conn, d2.as_of, d2.since), (3, None, None));
205 }
206
207 #[test]
208 fn tx_report_projects_tx_id_and_tempids() {
209 let r = TxReport {
210 tx_id: 268435458,
211 tempids: vec![("t".to_string(), 268435460)],
212 };
213 let s = print_str(&tx_report_value(&r, None));
214 assert!(s.contains(":mentat.store/tx-id 268435458"), "{s}");
215 assert!(s.contains("\"t\" 268435460"), "{s}");
216 }
217
218 #[test]
219 fn bad_uuid_falls_back_to_the_reader_call_form() {
220 let s = print_str(&uuid_value("not-a-uuid"));
221 assert_eq!(s, "(parse-uuid \"not-a-uuid\")");
222 }
223}