Skip to main content

lino_rest_api/
value.rs

1//! Helpers for building and reading [`LinoValue`] objects.
2//!
3//! Links Notation objects keep their keys in insertion order, which is what makes
4//! entity tags reproducible across implementations, so every helper here
5//! preserves that order.
6
7use lino_objects_codec::LinoValue;
8
9/// Build an object from its members, keeping them in the given order.
10///
11/// # Examples
12///
13/// ```
14/// use lino_rest_api::value::{object, string};
15///
16/// let task = object([("title", string("Ship it"))]);
17/// assert_eq!(lino_rest_api::codec::encode(&task), "(\n  title \"Ship it\"\n)");
18/// ```
19pub fn object<I, K>(members: I) -> LinoValue
20where
21    I: IntoIterator<Item = (K, LinoValue)>,
22    K: Into<String>,
23{
24    LinoValue::Object(
25        members
26            .into_iter()
27            .map(|(key, value)| (key.into(), value))
28            .collect(),
29    )
30}
31
32/// Build an array from its items.
33pub fn array<I: IntoIterator<Item = LinoValue>>(items: I) -> LinoValue {
34    LinoValue::Array(items.into_iter().collect())
35}
36
37/// A string value.
38pub fn string<S: Into<String>>(text: S) -> LinoValue {
39    LinoValue::String(text.into())
40}
41
42/// An integer value.
43pub fn int(number: i64) -> LinoValue {
44    LinoValue::Int(number)
45}
46
47/// A boolean value.
48pub fn boolean(flag: bool) -> LinoValue {
49    LinoValue::Bool(flag)
50}
51
52/// The members of a value when it is an object.
53pub fn as_object(value: &LinoValue) -> Option<&Vec<(String, LinoValue)>> {
54    match value {
55        LinoValue::Object(members) => Some(members),
56        _ => None,
57    }
58}
59
60/// The items of a value when it is an array.
61pub fn as_array(value: &LinoValue) -> Option<&Vec<LinoValue>> {
62    match value {
63        LinoValue::Array(items) => Some(items),
64        _ => None,
65    }
66}
67
68/// The text of a value when it is a string.
69pub fn as_str(value: &LinoValue) -> Option<&str> {
70    match value {
71        LinoValue::String(text) => Some(text),
72        _ => None,
73    }
74}
75
76/// Read a member of an object, or [`None`] when it is absent or the value is not
77/// an object.
78pub fn get<'a>(value: &'a LinoValue, key: &str) -> Option<&'a LinoValue> {
79    as_object(value)?
80        .iter()
81        .find(|(name, _)| name == key)
82        .map(|(_, member)| member)
83}
84
85/// Whether an object carries a member.
86pub fn has(value: &LinoValue, key: &str) -> bool {
87    get(value, key).is_some()
88}
89
90/// Set a member, replacing it in place when it is already present so that the
91/// order of the other members is untouched.
92pub fn set<S: Into<String>>(value: &mut LinoValue, key: S, member: LinoValue) {
93    let key = key.into();
94    if let LinoValue::Object(members) = value {
95        if let Some(existing) = members.iter_mut().find(|(name, _)| *name == key) {
96            existing.1 = member;
97        } else {
98            members.push((key, member));
99        }
100    }
101}
102
103/// An object with one member replaced or appended, leaving the original untouched.
104pub fn with<S: Into<String>>(value: &LinoValue, key: S, member: LinoValue) -> LinoValue {
105    let mut copy = value.clone();
106    if !matches!(copy, LinoValue::Object(_)) {
107        copy = LinoValue::Object(Vec::new());
108    }
109    set(&mut copy, key, member);
110    copy
111}
112
113/// Merge the members of `patch` into `base`, as `PATCH` does.
114pub fn merge(base: &LinoValue, patch: &LinoValue) -> LinoValue {
115    let mut merged = base.clone();
116    if !matches!(merged, LinoValue::Object(_)) {
117        merged = LinoValue::Object(Vec::new());
118    }
119    if let Some(members) = as_object(patch) {
120        for (key, member) in members {
121            set(&mut merged, key.clone(), member.clone());
122        }
123    }
124    merged
125}
126
127/// Render a value the way a query string or a path segment spells it.
128///
129/// Booleans are `true`/`false` and whole floats lose their fraction, so that the
130/// Rust client sends exactly what the Python and JavaScript clients send.
131pub fn to_query_text(value: &LinoValue) -> String {
132    match value {
133        LinoValue::Null => "null".to_string(),
134        LinoValue::Bool(flag) => flag.to_string(),
135        LinoValue::Int(number) => number.to_string(),
136        LinoValue::Float(number) => format_float(*number),
137        LinoValue::String(text) => text.clone(),
138        other => crate::codec::encode_single_line(other),
139    }
140}
141
142/// Format a float the way JSON does: whole numbers keep no fraction.
143pub fn format_float(number: f64) -> String {
144    if number.is_finite() && number.fract() == 0.0 && number.abs() < 1e15 {
145        format!("{}", number as i64)
146    } else {
147        format!("{number}")
148    }
149}
150
151/// Compare two values in a total order that works across every scalar type.
152///
153/// Absent values sort first, numbers sort numerically among themselves, and
154/// everything else sorts as the text of [`to_query_text`], which is the order
155/// the JavaScript and Python packages produce.
156pub fn compare(left: &LinoValue, right: &LinoValue) -> std::cmp::Ordering {
157    use std::cmp::Ordering;
158    match (left, right) {
159        (LinoValue::Null, LinoValue::Null) => Ordering::Equal,
160        (LinoValue::Null, _) => Ordering::Less,
161        (_, LinoValue::Null) => Ordering::Greater,
162        (LinoValue::Int(a), LinoValue::Int(b)) => a.cmp(b),
163        (LinoValue::Int(a), LinoValue::Float(b)) => {
164            (*a as f64).partial_cmp(b).unwrap_or(Ordering::Equal)
165        }
166        (LinoValue::Float(a), LinoValue::Int(b)) => {
167            a.partial_cmp(&(*b as f64)).unwrap_or(Ordering::Equal)
168        }
169        (LinoValue::Float(a), LinoValue::Float(b)) => a.partial_cmp(b).unwrap_or(Ordering::Equal),
170        (a, b) => to_query_text(a).cmp(&to_query_text(b)),
171    }
172}