Skip to main content

datumesh_core/
model.rs

1//! Typed scalars (3, 4.1), argument maps (4.2), cell references (4.3) and cells (4.4).
2
3use std::collections::BTreeMap;
4
5use crate::error::{err, member, ErrorCode, Result};
6use crate::json::{self, jcs_sorted, Value};
7use crate::lexical::{self, limits};
8use crate::schema;
9
10/// One of the six scalar types (3).
11#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
12pub enum Type {
13    /// An exact decimal written as a string (3.1).
14    Number,
15    /// An NFC string of Unicode 15.1 code points (3.5).
16    String,
17    /// `true` or `false`.
18    Boolean,
19    /// `YYYY-MM-DD` (3.2).
20    Date,
21    /// `YYYY-MM-DDTHH:MM:SS.ffffffZ` (3.3).
22    Instant,
23    /// `<sha256>:<length>:<media type>` (3.7).
24    Blob,
25}
26
27impl Type {
28    /// The type name as written in JSON.
29    pub const fn as_str(self) -> &'static str {
30        match self {
31            Type::Number => "number",
32            Type::String => "string",
33            Type::Boolean => "boolean",
34            Type::Date => "date",
35            Type::Instant => "instant",
36            Type::Blob => "blob",
37        }
38    }
39
40    /// The type of a type name; `None` outside the six.
41    pub fn from_name(name: &str) -> Option<Type> {
42        Some(match name {
43            "number" => Type::Number,
44            "string" => Type::String,
45            "boolean" => Type::Boolean,
46            "date" => Type::Date,
47            "instant" => Type::Instant,
48            "blob" => Type::Blob,
49            _ => return None,
50        })
51    }
52
53    pub(crate) fn parse(name: &str, path: &str) -> Result<Type> {
54        match Type::from_name(name) {
55            Some(t) => Ok(t),
56            None => err(ErrorCode::CanonType, path, format!("unknown type {name:?}")),
57        }
58    }
59
60    /// Whether a valid-time argument may have this type (date, instant or string).
61    pub fn may_be_valid_time(self) -> bool {
62        matches!(self, Type::Date | Type::Instant | Type::String)
63    }
64}
65
66/// A typed scalar value in its one canonical lexical form.
67#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
68pub enum Scalar {
69    /// A canonical decimal string.
70    Number(String),
71    /// A valid string value.
72    String(String),
73    /// A boolean.
74    Boolean(bool),
75    /// A calendar date.
76    Date(String),
77    /// A UTC instant with microseconds.
78    Instant(String),
79    /// A blob reference.
80    Blob(String),
81}
82
83impl Scalar {
84    /// Builds a scalar of type `ty` from its lexical form, checking it (3). For a boolean,
85    /// `text` must be `true` or `false` (`E_CANON_TYPE` otherwise).
86    pub fn from_lexical(ty: Type, text: &str) -> Result<Scalar> {
87        check_lexical(ty, text, "$")?;
88        Ok(unchecked(ty, text))
89    }
90
91    /// Parses a typed scalar from JSON text (4.1): strict JSON, structure, canonical form.
92    pub fn from_json(bytes: &[u8]) -> Result<Scalar> {
93        let v = json::parse(bytes)?;
94        scalar_structure(&v, "$")?;
95        scalar_canonical(&v, "$")
96    }
97
98    /// The scalar's type.
99    pub fn ty(&self) -> Type {
100        match self {
101            Scalar::Number(_) => Type::Number,
102            Scalar::String(_) => Type::String,
103            Scalar::Boolean(_) => Type::Boolean,
104            Scalar::Date(_) => Type::Date,
105            Scalar::Instant(_) => Type::Instant,
106            Scalar::Blob(_) => Type::Blob,
107        }
108    }
109
110    /// The lexical form (`true`/`false` for booleans).
111    pub fn lexical(&self) -> &str {
112        match self {
113            Scalar::Number(s) | Scalar::String(s) | Scalar::Date(s) | Scalar::Instant(s) | Scalar::Blob(s) => s,
114            Scalar::Boolean(true) => "true",
115            Scalar::Boolean(false) => "false",
116        }
117    }
118
119    /// The JSON form `{"type": …, "value": …}`.
120    pub fn to_value(&self) -> Value {
121        let value = match self {
122            Scalar::Boolean(b) => Value::Bool(*b),
123            other => Value::String(other.lexical().into()),
124        };
125        Value::Object(vec![("type".into(), Value::String(self.ty().as_str().into())), ("value".into(), value)])
126    }
127
128    /// The canonical bytes (6.2) of the typed scalar.
129    pub fn canonical(&self) -> Vec<u8> {
130        self.to_value().to_jcs()
131    }
132}
133
134/// A scalar from a lexical form already checked against `ty`.
135pub(crate) fn unchecked(ty: Type, text: &str) -> Scalar {
136    match ty {
137        Type::Number => Scalar::Number(text.into()),
138        Type::String => Scalar::String(text.into()),
139        Type::Date => Scalar::Date(text.into()),
140        Type::Instant => Scalar::Instant(text.into()),
141        Type::Blob => Scalar::Blob(text.into()),
142        Type::Boolean => Scalar::Boolean(text == "true"),
143    }
144}
145
146/// Checks a lexical form against its type.
147pub(crate) fn check_lexical(ty: Type, text: &str, path: &str) -> Result<()> {
148    match ty {
149        Type::Number => lexical::check_number(text, path),
150        Type::String => lexical::check_string(text, path),
151        Type::Date => lexical::check_date(text, path),
152        Type::Instant => lexical::check_instant(text, path),
153        Type::Blob => lexical::check_blob(text, path),
154        Type::Boolean => {
155            if text == "true" || text == "false" {
156                Ok(())
157            } else {
158                err(ErrorCode::CanonType, path, format!("a boolean is true or false, not {text:?}"))
159            }
160        }
161    }
162}
163
164pub(crate) fn scalar_structure(v: &Value, path: &str) -> Result<()> {
165    let m = schema::object(v, path)?;
166    schema::exact(m, &["type", "value"], &[], path, ErrorCode::Schema)?;
167    schema::string(m, "type", path)?;
168    match schema::get(m, "value") {
169        Value::String(_) | Value::Bool(_) => Ok(()),
170        _ => err(ErrorCode::Schema, &member(path, "value"), "value must be a string or a boolean"),
171    }
172}
173
174/// Canonical check of a structurally valid typed scalar.
175pub(crate) fn scalar_canonical(v: &Value, path: &str) -> Result<Scalar> {
176    let m = schema::object(v, path)?;
177    let ty = Type::parse(schema::string(m, "type", path)?, &member(path, "type"))?;
178    let vp = member(path, "value");
179    match (ty, schema::get(m, "value")) {
180        (Type::Boolean, Value::Bool(b)) => Ok(Scalar::Boolean(*b)),
181        (Type::Boolean, _) => err(ErrorCode::CanonType, &vp, "boolean value must be JSON true or false"),
182        (t, Value::String(s)) => {
183            check_lexical(t, s, &vp)?;
184            Ok(unchecked(t, s))
185        }
186        (t, _) => err(ErrorCode::CanonType, &vp, format!("{} value must be a JSON string", t.as_str())),
187    }
188}
189
190/// An argument: a typed scalar, or the slice marker `"*"` (4.2).
191#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
192pub enum Arg {
193    /// A bound value.
194    Value(Scalar),
195    /// The slice marker: the argument is open.
196    Slice,
197}
198
199impl Arg {
200    /// The JSON form: a typed scalar or `"*"`.
201    pub fn to_value(&self) -> Value {
202        match self {
203            Arg::Value(s) => s.to_value(),
204            Arg::Slice => Value::String("*".into()),
205        }
206    }
207
208    /// The scalar, if bound.
209    pub fn scalar(&self) -> Option<&Scalar> {
210        match self {
211            Arg::Value(s) => Some(s),
212            Arg::Slice => None,
213        }
214    }
215}
216
217/// An argument map, keyed by argument name (byte order equals JCS order for names).
218pub type Args = BTreeMap<String, Arg>;
219
220pub(crate) fn args_to_value(args: &Args) -> Value {
221    Value::Object(args.iter().map(|(k, a)| (k.clone(), a.to_value())).collect())
222}
223
224pub(crate) fn args_structure(v: &Value, path: &str) -> Result<()> {
225    let m = schema::object(v, path)?;
226    for (k, a) in jcs_sorted(m) {
227        let p = member(path, k);
228        match a {
229            Value::String(s) if s == "*" => {}
230            Value::String(_) => return err(ErrorCode::Schema, &p, "an argument is a typed scalar or \"*\""),
231            other => scalar_structure(other, &p)?,
232        }
233    }
234    Ok(())
235}
236
237pub(crate) fn args_canonical(v: &Value, path: &str) -> Result<Args> {
238    let m = schema::object(v, path)?;
239    if m.len() > limits::ARGS {
240        return err(ErrorCode::CanonLimit, path, "more than 32 arguments");
241    }
242    let mut out = Args::new();
243    for (k, a) in jcs_sorted(m) {
244        let p = member(path, k);
245        lexical::check_arg_name(k, &p)?;
246        let arg = match a {
247            Value::String(s) if s == "*" => Arg::Slice,
248            other => Arg::Value(scalar_canonical(other, &p)?),
249        };
250        out.insert(k.clone(), arg);
251    }
252    Ok(out)
253}
254
255fn check_fn_length(ns: &str, f: &str) -> Result<()> {
256    if ns.len() + 1 + f.len() > limits::FUNCTION_NAME {
257        return err(ErrorCode::CanonLimit, "$", "function name longer than 512 characters");
258    }
259    Ok(())
260}
261
262/// A cell (4.4): a function, pinned by hash, applied to arguments. May denote a slice.
263#[derive(Clone, Debug, PartialEq, Eq, Hash)]
264pub struct Cell {
265    /// The namespace, e.g. `rates.example.org`.
266    pub ns: String,
267    /// The function name within the namespace.
268    pub fn_name: String,
269    /// The function hash, 64 lowercase hex digits.
270    pub fn_hash: String,
271    /// The arguments.
272    pub args: Args,
273}
274
275impl Cell {
276    /// Parses a cell from JSON text: strict JSON, structure, canonical form. Slices allowed.
277    pub fn from_json(bytes: &[u8]) -> Result<Cell> {
278        let v = json::parse(bytes)?;
279        Cell::from_value(&v)
280    }
281
282    /// Checks a parsed JSON value as a cell.
283    pub fn from_value(v: &Value) -> Result<Cell> {
284        json::check_value_depth(v)?;
285        cell_structure(v, "$")?;
286        cell_canonical(v, "$")
287    }
288
289    /// The JSON form.
290    pub fn to_value(&self) -> Value {
291        Value::Object(vec![
292            ("args".into(), args_to_value(&self.args)),
293            ("fn".into(), Value::String(self.fn_name.clone())),
294            ("fn_hash".into(), Value::String(self.fn_hash.clone())),
295            ("ns".into(), Value::String(self.ns.clone())),
296        ])
297    }
298
299    /// The canonical bytes (6.2).
300    pub fn canonical(&self) -> Vec<u8> {
301        self.to_value().to_jcs()
302    }
303
304    /// The cell id: SHA-256 of the canonical bytes, lowercase hex.
305    pub fn id(&self) -> String {
306        crate::crypto::sha256_hex(&self.canonical())
307    }
308
309    /// Whether any argument is the slice marker.
310    pub fn is_slice(&self) -> bool {
311        self.args.values().any(|a| *a == Arg::Slice)
312    }
313
314    /// The fully qualified function name `<ns>/<fn>`.
315    pub fn function_name(&self) -> String {
316        format!("{}/{}", self.ns, self.fn_name)
317    }
318}
319
320pub(crate) fn cell_structure(v: &Value, path: &str) -> Result<()> {
321    let m = schema::object(v, path)?;
322    schema::exact(m, &["args", "fn", "fn_hash", "ns"], &[], path, ErrorCode::Schema)?;
323    args_structure(schema::get(m, "args"), &member(path, "args"))?;
324    schema::string(m, "fn", path)?;
325    schema::string(m, "fn_hash", path)?;
326    schema::string(m, "ns", path)?;
327    Ok(())
328}
329
330pub(crate) fn cell_canonical(v: &Value, path: &str) -> Result<Cell> {
331    let m = schema::object(v, path)?;
332    let args = args_canonical(schema::get(m, "args"), &member(path, "args"))?;
333    let f = schema::string(m, "fn", path)?;
334    lexical::check_name(f, &member(path, "fn"))?;
335    let h = schema::string(m, "fn_hash", path)?;
336    lexical::check_hex(h, 64, &member(path, "fn_hash"))?;
337    let ns = schema::string(m, "ns", path)?;
338    lexical::check_namespace(ns, &member(path, "ns"))?;
339    check_fn_length(ns, f).map_err(|e| crate::error::Error::new(e.code(), path, e.detail()))?;
340    Ok(Cell { ns: ns.into(), fn_name: f.into(), fn_hash: h.into(), args })
341}
342
343/// A cell reference (4.3): what a cell URI denotes before the function name is resolved.
344///
345/// It has no place for the pin of `spec-0.4.2` (`fn_hash`): [`CellRef::from_json`] refuses a
346/// reference with one (`E_SCHEMA`, as in 0.3.5) and [`crate::parse_uri`] a pinned URI. Use
347/// [`Reference`], which has it.
348#[derive(Clone, Debug, PartialEq, Eq, Hash)]
349pub struct CellRef {
350    /// The namespace.
351    pub ns: String,
352    /// The function name within the namespace.
353    pub fn_name: String,
354    /// The arguments, typed by the function's signature.
355    pub args: Args,
356    /// The transaction time asked for; `None` means now.
357    pub asof: Option<String>,
358}
359
360impl CellRef {
361    /// Parses a cell reference from JSON text: strict JSON, structure, canonical form.
362    pub fn from_json(bytes: &[u8]) -> Result<CellRef> {
363        let v = json::parse(bytes)?;
364        let m = schema::object(&v, "$")?;
365        schema::exact(m, &["args", "fn", "ns"], &["asof"], "$", ErrorCode::Schema)?;
366        args_structure(schema::get(m, "args"), "$.args")?;
367        schema::string(m, "fn", "$")?;
368        schema::string(m, "ns", "$")?;
369        if schema::opt(m, "asof").is_some() {
370            schema::string(m, "asof", "$")?;
371        }
372        let args = args_canonical(schema::get(m, "args"), "$.args")?;
373        let asof = match schema::opt(m, "asof") {
374            Some(Value::String(s)) => {
375                lexical::check_instant(s, "$.asof")?;
376                Some(s.clone())
377            }
378            _ => None,
379        };
380        let f = schema::string(m, "fn", "$")?;
381        lexical::check_name(f, "$.fn")?;
382        let ns = schema::string(m, "ns", "$")?;
383        lexical::check_namespace(ns, "$.ns")?;
384        check_fn_length(ns, f)?;
385        Ok(CellRef { ns: ns.into(), fn_name: f.into(), args, asof })
386    }
387
388    /// The JSON form.
389    pub fn to_value(&self) -> Value {
390        let mut m = vec![("args".to_string(), args_to_value(&self.args))];
391        if let Some(a) = &self.asof {
392            m.push(("asof".into(), Value::String(a.clone())));
393        }
394        m.push(("fn".into(), Value::String(self.fn_name.clone())));
395        m.push(("ns".into(), Value::String(self.ns.clone())));
396        Value::Object(m)
397    }
398
399    /// The canonical bytes (6.2).
400    pub fn canonical(&self) -> Vec<u8> {
401        self.to_value().to_jcs()
402    }
403
404    /// The cell this reference denotes once its function name has resolved to `fn_hash`.
405    pub fn resolve(&self, fn_hash: &str) -> Result<Cell> {
406        lexical::check_hex(fn_hash, 64, "$.fn_hash")?;
407        Ok(Cell {
408            ns: self.ns.clone(),
409            fn_name: self.fn_name.clone(),
410            fn_hash: fn_hash.into(),
411            args: self.args.clone(),
412        })
413    }
414}
415
416/// A cell reference (4.3) with the optional pin of `spec-0.4.2`: `fn_hash`, the function the
417/// reference is pinned to, whatever its name is bound to (5.1, 5.8). Unpinned, it is the
418/// [`CellRef`] of `spec-0.4.1` ([`From<CellRef>`], [`Reference::to_cell_ref`]).
419#[derive(Clone, Debug, PartialEq, Eq, Hash)]
420#[non_exhaustive]
421pub struct Reference {
422    /// The namespace.
423    pub ns: String,
424    /// The function name within the namespace.
425    pub fn_name: String,
426    /// The arguments, typed by the function's signature.
427    pub args: Args,
428    /// The pin: the function hash the reference is pinned to, or `None`.
429    pub fn_hash: Option<String>,
430    /// The transaction time asked for; `None` means now.
431    pub asof: Option<String>,
432}
433
434impl Reference {
435    /// Parses a reference from JSON text: strict JSON, structure (`fn_hash` a string,
436    /// `E_SCHEMA`), canonical form in JCS order (`args`, `asof`, `fn`, `fn_hash` as 64
437    /// lowercase hex digits (`E_CANON_HEX`), `ns`), then the 512-character function name.
438    pub fn from_json(bytes: &[u8]) -> Result<Reference> {
439        let v = json::parse(bytes)?;
440        json::check_value_depth(&v)?;
441        let m = schema::object(&v, "$")?;
442        schema::exact(m, &["args", "fn", "ns"], &["asof", "fn_hash"], "$", ErrorCode::Schema)?;
443        args_structure(schema::get(m, "args"), "$.args")?;
444        schema::string(m, "fn", "$")?;
445        schema::string(m, "ns", "$")?;
446        for o in ["asof", "fn_hash"] {
447            if schema::opt(m, o).is_some() {
448                schema::string(m, o, "$")?;
449            }
450        }
451        let args = args_canonical(schema::get(m, "args"), "$.args")?;
452        let asof = match schema::opt(m, "asof") {
453            Some(Value::String(s)) => {
454                lexical::check_instant(s, "$.asof")?;
455                Some(s.clone())
456            }
457            _ => None,
458        };
459        let f = schema::string(m, "fn", "$")?;
460        lexical::check_name(f, "$.fn")?;
461        let fn_hash = match schema::opt(m, "fn_hash") {
462            Some(Value::String(h)) => {
463                lexical::check_hex(h, 64, "$.fn_hash")?;
464                Some(h.clone())
465            }
466            _ => None,
467        };
468        let ns = schema::string(m, "ns", "$")?;
469        lexical::check_namespace(ns, "$.ns")?;
470        check_fn_length(ns, f)?;
471        Ok(Reference { ns: ns.into(), fn_name: f.into(), args, fn_hash, asof })
472    }
473
474    /// A reference to `ns/fn_name` with these arguments, unpinned and as of now; set
475    /// [`fn_hash`](Self::fn_hash) and [`asof`](Self::asof) to pin it or ask as of a time. Not
476    /// checked: [`crate::format_reference`] writes what it is given.
477    pub fn new(ns: impl Into<String>, fn_name: impl Into<String>, args: Args) -> Reference {
478        Reference { ns: ns.into(), fn_name: fn_name.into(), args, fn_hash: None, asof: None }
479    }
480
481    /// The JSON form.
482    pub fn to_value(&self) -> Value {
483        let mut m = vec![("args".to_string(), args_to_value(&self.args))];
484        if let Some(a) = &self.asof {
485            m.push(("asof".into(), Value::String(a.clone())));
486        }
487        m.push(("fn".into(), Value::String(self.fn_name.clone())));
488        if let Some(h) = &self.fn_hash {
489            m.push(("fn_hash".into(), Value::String(h.clone())));
490        }
491        m.push(("ns".into(), Value::String(self.ns.clone())));
492        Value::Object(m)
493    }
494
495    /// The canonical bytes (6.2). A pinned reference without `asof` has the canonical bytes,
496    /// and so the id, of the cell it denotes.
497    pub fn canonical(&self) -> Vec<u8> {
498        self.to_value().to_jcs()
499    }
500
501    /// The cell a pinned reference denotes; `None` when it is not pinned (resolve its name
502    /// with [`Reference::resolve`]).
503    pub fn cell(&self) -> Option<Cell> {
504        let h = self.fn_hash.as_ref()?;
505        Some(Cell { ns: self.ns.clone(), fn_name: self.fn_name.clone(), fn_hash: h.clone(), args: self.args.clone() })
506    }
507
508    /// The cell this reference denotes once its name has resolved to `fn_hash`. A pinned
509    /// reference denotes its pin's cell only: another hash is `E_FN_HASH_MISMATCH`.
510    pub fn resolve(&self, fn_hash: &str) -> Result<Cell> {
511        lexical::check_hex(fn_hash, 64, "$.fn_hash")?;
512        if self.fn_hash.as_deref().is_some_and(|p| p != fn_hash) {
513            return err(ErrorCode::FnHashMismatch, "$.fn_hash", "the reference is pinned to another function");
514        }
515        Ok(Cell {
516            ns: self.ns.clone(),
517            fn_name: self.fn_name.clone(),
518            fn_hash: fn_hash.into(),
519            args: self.args.clone(),
520        })
521    }
522
523    /// The `spec-0.4.1` form, when the reference is not pinned.
524    pub fn to_cell_ref(&self) -> Option<CellRef> {
525        self.fn_hash.is_none().then(|| CellRef {
526            ns: self.ns.clone(),
527            fn_name: self.fn_name.clone(),
528            args: self.args.clone(),
529            asof: self.asof.clone(),
530        })
531    }
532}
533
534impl From<CellRef> for Reference {
535    fn from(r: CellRef) -> Reference {
536        Reference { ns: r.ns, fn_name: r.fn_name, args: r.args, fn_hash: None, asof: r.asof }
537    }
538}