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");