Skip to main content

patterns/
record.rs

1use crate::hex::{bytes_of, hex_text, prefixed, unprefixed};
2use patterns_macros::{because, source};
3use std::collections::BTreeMap;
4
5#[derive(Clone, Debug, PartialEq)]
6pub enum Field {
7    Nothing,
8    Flag(bool),
9    Whole(i64),
10    Real(f64),
11    Text(String),
12    List(Vec<Field>),
13    Table(Vec<(String, Field)>),
14}
15because!(Field, "the one shape every stored or sent value takes: nothing, a flag, a whole number, a real, a text, a list or a table of named fields, so a store or a wire writes each once and never sees the type behind it");
16
17pub trait Fielded: Sized {
18    fn field(&self) -> Field;
19    fn refielded(field: &Field) -> Option<Self>;
20}
21because!(Fielded, "a value read as a field and a field read back as the value, the two directions a record crosses between a program and what keeps it");
22
23pub fn table_of(field: &Field) -> Option<&[(String, Field)]> {
24    match field {
25        Field::Table(rows) => Some(rows),
26        _ => None,
27    }
28}
29because!(table_of, "the named fields under a table field, or nothing when the field is not a table, the first step of reading any record back");
30
31pub fn looked_field<'a>(table: &'a [(String, Field)], name: &str) -> &'a Field {
32    let camel = camel_of(name);
33    table
34        .iter()
35        .find(|(key, _)| key == name || *key == camel)
36        .map(|(_, field)| field)
37        .unwrap_or(&Field::Nothing)
38}
39because!(looked_field, "one named field out of a table, or nothing when the table lacks it, so an absent column reads as an absent value rather than a failure");
40
41pub fn text_of_field(field: &Field) -> Option<&str> {
42    match field {
43        Field::Text(text) => Some(text),
44        _ => None,
45    }
46}
47because!(text_of_field, "the text under a text field, or nothing, the reading a vocabulary value is restored through");
48
49fn whole_of(field: &Field) -> Option<i64> {
50    match field {
51        Field::Whole(value) => Some(*value),
52        Field::Real(value) => Some(*value as i64),
53        Field::Text(text) => text.parse().ok(),
54        Field::Flag(flag) => Some(i64::from(*flag)),
55        _ => None,
56    }
57}
58
59fn real_of(field: &Field) -> Option<f64> {
60    match field {
61        Field::Real(value) => Some(*value),
62        Field::Whole(value) => Some(*value as f64),
63        Field::Text(text) => text.parse().ok(),
64        _ => None,
65    }
66}
67
68impl Fielded for bool {
69    fn field(&self) -> Field {
70        Field::Flag(*self)
71    }
72
73    fn refielded(field: &Field) -> Option<bool> {
74        match field {
75            Field::Flag(flag) => Some(*flag),
76            other => whole_of(other).map(|value| value != 0),
77        }
78    }
79}
80
81impl Fielded for i64 {
82    fn field(&self) -> Field {
83        Field::Whole(*self)
84    }
85
86    fn refielded(field: &Field) -> Option<i64> {
87        whole_of(field)
88    }
89}
90
91impl Fielded for u64 {
92    fn field(&self) -> Field {
93        Field::Whole(i64::try_from(*self).unwrap_or(i64::MAX))
94    }
95
96    fn refielded(field: &Field) -> Option<u64> {
97        whole_of(field).and_then(|value| u64::try_from(value).ok())
98    }
99}
100
101impl Fielded for u32 {
102    fn field(&self) -> Field {
103        Field::Whole(i64::from(*self))
104    }
105
106    fn refielded(field: &Field) -> Option<u32> {
107        u64::refielded(field)?.try_into().ok()
108    }
109}
110
111impl Fielded for usize {
112    fn field(&self) -> Field {
113        u64::field(&(*self as u64))
114    }
115
116    fn refielded(field: &Field) -> Option<usize> {
117        u64::refielded(field).map(|value| value as usize)
118    }
119}
120
121impl Fielded for u128 {
122    fn field(&self) -> Field {
123        Field::Text(self.to_string())
124    }
125
126    fn refielded(field: &Field) -> Option<u128> {
127        match field {
128            Field::Text(text) => text.parse().ok(),
129            other => whole_of(other).and_then(|value| u128::try_from(value).ok()),
130        }
131    }
132}
133
134impl Fielded for i128 {
135    fn field(&self) -> Field {
136        match *self < 0 {
137            true => Field::Text(self.to_string()),
138            false => u128::field(&self.unsigned_abs()),
139        }
140    }
141
142    fn refielded(field: &Field) -> Option<i128> {
143        text_of_field(field)
144            .and_then(|text| text.parse().ok())
145            .or_else(|| whole_of(field).map(i128::from))
146    }
147}
148
149impl Fielded for f64 {
150    fn field(&self) -> Field {
151        Field::Real(*self)
152    }
153
154    fn refielded(field: &Field) -> Option<f64> {
155        real_of(field)
156    }
157}
158
159impl Fielded for String {
160    fn field(&self) -> Field {
161        Field::Text(self.clone())
162    }
163
164    fn refielded(field: &Field) -> Option<String> {
165        match field {
166            Field::Text(text) => Some(text.clone()),
167            Field::Whole(value) => Some(value.to_string()),
168            Field::Real(value) => Some(value.to_string()),
169            _ => None,
170        }
171    }
172}
173
174impl<T: Fielded> Fielded for Option<T> {
175    fn field(&self) -> Field {
176        match self {
177            Some(value) => value.field(),
178            None => Field::Nothing,
179        }
180    }
181
182    fn refielded(field: &Field) -> Option<Option<T>> {
183        match field {
184            Field::Nothing => Some(None),
185            other => T::refielded(other).map(Some),
186        }
187    }
188}
189
190impl<T: Fielded> Fielded for Vec<T> {
191    fn field(&self) -> Field {
192        Field::List(self.iter().map(Fielded::field).collect())
193    }
194
195    fn refielded(field: &Field) -> Option<Vec<T>> {
196        match field {
197            Field::List(items) => items.iter().map(T::refielded).collect(),
198            Field::Nothing => Some(vec![]),
199            _ => None,
200        }
201    }
202}
203
204impl<A: Fielded, B: Fielded> Fielded for (A, B) {
205    fn field(&self) -> Field {
206        Field::List(vec![self.0.field(), self.1.field()])
207    }
208
209    fn refielded(field: &Field) -> Option<(A, B)> {
210        match field {
211            Field::List(items) => match items.as_slice() {
212                [first, second] => Some((A::refielded(first)?, B::refielded(second)?)),
213                _ => None,
214            },
215            _ => None,
216        }
217    }
218}
219
220impl<V: Fielded> Fielded for BTreeMap<String, V> {
221    fn field(&self) -> Field {
222        Field::Table(self.iter().map(|(key, value)| (key.clone(), value.field())).collect())
223    }
224
225    fn refielded(field: &Field) -> Option<BTreeMap<String, V>> {
226        let rows = table_of(field)?;
227        rows.iter()
228            .map(|(key, value)| V::refielded(value).map(|v| (key.clone(), v)))
229            .collect()
230    }
231}
232
233impl<const N: usize> Fielded for [u8; N] {
234    fn field(&self) -> Field {
235        Field::Text(prefixed(&hex_text(self)))
236    }
237
238    fn refielded(field: &Field) -> Option<[u8; N]> {
239        bytes_of(unprefixed(text_of_field(field)?))?.try_into().ok()
240    }
241}
242
243pub fn recorded<T: Fielded>(value: &T) -> Field {
244    value.field()
245}
246because!(recorded, "a value as its field, the call a binding makes where the trait method is not in reach");
247
248pub fn restored<T: Fielded>(field: &Field) -> Option<T> {
249    T::refielded(field)
250}
251because!(restored, "a value read back from its field, the reverse of recorded");
252
253pub fn restored_all<T: Fielded>(fields: &[Field]) -> Vec<T> {
254    fields.iter().filter_map(T::refielded).collect()
255}
256because!(restored_all, "every value that reads back whole out of a list of fields, with the rows that do not fit the shape left out rather than failing the whole read");
257
258pub struct WireNaming;
259source!(WireNaming, "the convention the retired dashboard's wire and its stores follow: a program writes a field name with underscores between its words and the wire writes the same name with each later word capitalised instead");
260
261const JOINER: char = '_';
262because!(JOINER, WireNaming, "the mark between the words of a field name as a program writes it, which the wire drops");
263
264pub fn camel_of(name: &str) -> String {
265    let mut out = String::with_capacity(name.len());
266    let mut lift = false;
267    for c in name.chars() {
268        if c == JOINER {
269            lift = true;
270        } else if lift {
271            out.extend(c.to_uppercase());
272            lift = false;
273        } else {
274            out.push(c);
275        }
276    }
277    out
278}
279because!(camel_of, "a field name as a wire writes it, the words run together with each one after the first capitalised, so a record read off a wire finds its fields under either spelling");