Skip to main content

ytsaurus_api/
value.rs

1//! The row model both transports speak.
2//!
3//! HTTP carries rows as YSON and RPC carries them in the wire protocol, and
4//! neither type belongs in an interface the other has to implement. This is the
5//! meeting point: a column name, and a value from the set both formats can
6//! represent.
7//!
8//! Column **names**, not ids. The RPC wire format numbers its values and
9//! resolves them through a name table; HTTP names them directly. A caller
10//! should not have to know which, so the id is an implementation detail of the
11//! RPC side.
12
13use std::collections::BTreeMap;
14use std::fmt;
15
16/// One column value.
17///
18/// The variants are the YTsaurus value types that survive both transports
19/// unchanged. `Composite` is deliberately absent: HTTP renders it as YSON and
20/// RPC as a distinct wire type, and unifying them would mean claiming a
21/// round-trip this crate cannot guarantee. Use [`Value::Any`] and read the YSON.
22#[derive(Debug, Clone, PartialEq)]
23pub enum Value {
24    Null,
25    Int64(i64),
26    Uint64(u64),
27    Double(f64),
28    Boolean(bool),
29    /// A byte string. YTsaurus strings are bytes, not text, so this is not
30    /// `String`: a column may legitimately hold something that is not UTF-8.
31    String(Vec<u8>),
32    /// A YSON-encoded value of any shape.
33    Any(Vec<u8>),
34}
35
36impl Value {
37    /// The value as a byte string, if it is one.
38    pub fn as_bytes(&self) -> Option<&[u8]> {
39        match self {
40            Self::String(bytes) | Self::Any(bytes) => Some(bytes),
41            _ => None,
42        }
43    }
44
45    /// The value as text, if it is a byte string that happens to be UTF-8.
46    pub fn as_str(&self) -> Option<&str> {
47        std::str::from_utf8(self.as_bytes()?).ok()
48    }
49
50    pub fn as_i64(&self) -> Option<i64> {
51        match self {
52            Self::Int64(value) => Some(*value),
53            _ => None,
54        }
55    }
56
57    pub fn as_u64(&self) -> Option<u64> {
58        match self {
59            Self::Uint64(value) => Some(*value),
60            _ => None,
61        }
62    }
63
64    pub fn as_f64(&self) -> Option<f64> {
65        match self {
66            Self::Double(value) => Some(*value),
67            _ => None,
68        }
69    }
70
71    pub fn as_bool(&self) -> Option<bool> {
72        match self {
73            Self::Boolean(value) => Some(*value),
74            _ => None,
75        }
76    }
77
78    pub fn is_null(&self) -> bool {
79        matches!(self, Self::Null)
80    }
81}
82
83impl From<i64> for Value {
84    fn from(value: i64) -> Self {
85        Self::Int64(value)
86    }
87}
88
89impl From<u64> for Value {
90    fn from(value: u64) -> Self {
91        Self::Uint64(value)
92    }
93}
94
95impl From<f64> for Value {
96    fn from(value: f64) -> Self {
97        Self::Double(value)
98    }
99}
100
101impl From<bool> for Value {
102    fn from(value: bool) -> Self {
103        Self::Boolean(value)
104    }
105}
106
107impl From<&str> for Value {
108    fn from(value: &str) -> Self {
109        Self::String(value.as_bytes().to_vec())
110    }
111}
112
113impl From<String> for Value {
114    fn from(value: String) -> Self {
115        Self::String(value.into_bytes())
116    }
117}
118
119impl From<Vec<u8>> for Value {
120    fn from(value: Vec<u8>) -> Self {
121        Self::String(value)
122    }
123}
124
125impl<T: Into<Value>> From<Option<T>> for Value {
126    fn from(value: Option<T>) -> Self {
127        match value {
128            Some(value) => value.into(),
129            None => Self::Null,
130        }
131    }
132}
133
134/// One row: named columns, in insertion order.
135///
136/// Ordered rather than a map because a key row's column order is the table's
137/// key order, and a lookup that reordered the key columns would ask for a
138/// different row. `BTreeMap` would silently sort them.
139#[derive(Debug, Clone, Default, PartialEq)]
140pub struct Row {
141    columns: Vec<(String, Value)>,
142}
143
144impl Row {
145    pub fn new() -> Self {
146        Self::default()
147    }
148
149    /// Adds a column, keeping the order it was added in.
150    #[must_use]
151    pub fn with(mut self, name: impl Into<String>, value: impl Into<Value>) -> Self {
152        self.columns.push((name.into(), value.into()));
153        self
154    }
155
156    /// Adds a column in place.
157    pub fn set(&mut self, name: impl Into<String>, value: impl Into<Value>) {
158        self.columns.push((name.into(), value.into()));
159    }
160
161    /// The value of a column, if the row has one.
162    pub fn get(&self, name: &str) -> Option<&Value> {
163        self.columns
164            .iter()
165            .find(|(column, _)| column == name)
166            .map(|(_, value)| value)
167    }
168
169    /// The columns, in order.
170    pub fn columns(&self) -> &[(String, Value)] {
171        &self.columns
172    }
173
174    /// The column names, in order.
175    pub fn names(&self) -> impl Iterator<Item = &str> {
176        self.columns.iter().map(|(name, _)| name.as_str())
177    }
178
179    pub fn len(&self) -> usize {
180        self.columns.len()
181    }
182
183    pub fn is_empty(&self) -> bool {
184        self.columns.is_empty()
185    }
186
187    /// The row as a map, for callers that would rather look columns up than
188    /// walk them. Loses the ordering, which is why it is not the representation.
189    pub fn to_map(&self) -> BTreeMap<&str, &Value> {
190        self.columns
191            .iter()
192            .map(|(name, value)| (name.as_str(), value))
193            .collect()
194    }
195}
196
197impl FromIterator<(String, Value)> for Row {
198    fn from_iter<I: IntoIterator<Item = (String, Value)>>(iterator: I) -> Self {
199        Self {
200            columns: iterator.into_iter().collect(),
201        }
202    }
203}
204
205impl fmt::Display for Row {
206    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
207        formatter.write_str("{")?;
208        for (index, (name, value)) in self.columns.iter().enumerate() {
209            if index > 0 {
210                formatter.write_str(", ")?;
211            }
212            match value {
213                Value::String(bytes) | Value::Any(bytes) => match std::str::from_utf8(bytes) {
214                    Ok(text) => write!(formatter, "{name}={text:?}")?,
215                    Err(_) => write!(formatter, "{name}=<{} bytes>", bytes.len())?,
216                },
217                other => write!(formatter, "{name}={other:?}")?,
218            }
219        }
220        formatter.write_str("}")
221    }
222}
223
224/// A row that may be absent.
225///
226/// A lookup returns one of these per key asked for, in order, and `None` means
227/// the key had no row. Shortening the list instead would silently misalign
228/// every answer after the missing one.
229pub type MaybeRow = Option<Row>;
230
231#[cfg(test)]
232mod tests {
233    use super::*;
234
235    #[test]
236    fn a_row_keeps_the_order_its_columns_were_added_in() {
237        // Key order is table order; sorting would ask for a different row.
238        let row = Row::new().with("b", 2i64).with("a", 1i64).with("c", 3i64);
239        assert_eq!(row.names().collect::<Vec<_>>(), ["b", "a", "c"]);
240    }
241
242    #[test]
243    fn columns_are_read_by_name() {
244        let row = Row::new().with("key", 42i64).with("value", "hello");
245        assert_eq!(row.get("key"), Some(&Value::Int64(42)));
246        assert_eq!(row.get("value").and_then(Value::as_str), Some("hello"));
247        assert_eq!(row.get("absent"), None);
248    }
249
250    #[test]
251    fn strings_are_bytes_and_need_not_be_utf8() {
252        let row = Row::new().with("raw", vec![0xff, 0xfe]);
253        assert_eq!(row.get("raw").unwrap().as_bytes(), Some(&[0xff, 0xfe][..]));
254        assert_eq!(
255            row.get("raw").unwrap().as_str(),
256            None,
257            "not UTF-8, and that is allowed"
258        );
259    }
260
261    #[test]
262    fn an_option_becomes_null() {
263        let row = Row::new()
264            .with("present", Some(1i64))
265            .with("absent", None::<i64>);
266        assert_eq!(row.get("present"), Some(&Value::Int64(1)));
267        assert!(row.get("absent").unwrap().is_null());
268    }
269
270    #[test]
271    fn display_is_readable_and_does_not_choke_on_binary() {
272        let row = Row::new().with("key", 1i64).with("blob", vec![0xff, 0x00]);
273        assert_eq!(row.to_string(), r#"{key=Int64(1), blob=<2 bytes>}"#);
274    }
275
276    #[test]
277    fn accessors_report_the_wrong_type_as_absent() {
278        let value = Value::Int64(1);
279        assert_eq!(value.as_i64(), Some(1));
280        assert_eq!(value.as_u64(), None);
281        assert_eq!(value.as_str(), None);
282        assert!(!value.is_null());
283    }
284}