premise 0.1.5

the core pattern library of Premise: sequences, tables, records, money, days and text, every function carrying the reason it exists
Documentation
use crate::hex::{bytes_of, hex_text, prefixed, unprefixed};
use patterns_macros::{because, source};
use std::collections::BTreeMap;

#[derive(Clone, Debug, PartialEq)]
pub enum Field {
    Nothing,
    Flag(bool),
    Whole(i64),
    Real(f64),
    Text(String),
    List(Vec<Field>),
    Table(Vec<(String, Field)>),
}
because!(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");

pub trait Fielded: Sized {
    fn field(&self) -> Field;
    fn refielded(field: &Field) -> Option<Self>;
}
because!(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");

pub fn table_of(field: &Field) -> Option<&[(String, Field)]> {
    match field {
        Field::Table(rows) => Some(rows),
        _ => None,
    }
}
because!(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");

pub fn looked_field<'a>(table: &'a [(String, Field)], name: &str) -> &'a Field {
    let camel = camel_of(name);
    table
        .iter()
        .find(|(key, _)| key == name || *key == camel)
        .map(|(_, field)| field)
        .unwrap_or(&Field::Nothing)
}
because!(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");

pub fn text_of_field(field: &Field) -> Option<&str> {
    match field {
        Field::Text(text) => Some(text),
        _ => None,
    }
}
because!(text_of_field, "the text under a text field, or nothing, the reading a vocabulary value is restored through");

fn whole_of(field: &Field) -> Option<i64> {
    match field {
        Field::Whole(value) => Some(*value),
        Field::Real(value) => Some(*value as i64),
        Field::Text(text) => text.parse().ok(),
        Field::Flag(flag) => Some(i64::from(*flag)),
        _ => None,
    }
}

fn real_of(field: &Field) -> Option<f64> {
    match field {
        Field::Real(value) => Some(*value),
        Field::Whole(value) => Some(*value as f64),
        Field::Text(text) => text.parse().ok(),
        _ => None,
    }
}

impl Fielded for bool {
    fn field(&self) -> Field {
        Field::Flag(*self)
    }

    fn refielded(field: &Field) -> Option<bool> {
        match field {
            Field::Flag(flag) => Some(*flag),
            other => whole_of(other).map(|value| value != 0),
        }
    }
}

impl Fielded for i64 {
    fn field(&self) -> Field {
        Field::Whole(*self)
    }

    fn refielded(field: &Field) -> Option<i64> {
        whole_of(field)
    }
}

impl Fielded for u64 {
    fn field(&self) -> Field {
        Field::Whole(i64::try_from(*self).unwrap_or(i64::MAX))
    }

    fn refielded(field: &Field) -> Option<u64> {
        whole_of(field).and_then(|value| u64::try_from(value).ok())
    }
}

impl Fielded for u32 {
    fn field(&self) -> Field {
        Field::Whole(i64::from(*self))
    }

    fn refielded(field: &Field) -> Option<u32> {
        u64::refielded(field)?.try_into().ok()
    }
}

impl Fielded for usize {
    fn field(&self) -> Field {
        u64::field(&(*self as u64))
    }

    fn refielded(field: &Field) -> Option<usize> {
        u64::refielded(field).map(|value| value as usize)
    }
}

impl Fielded for u128 {
    fn field(&self) -> Field {
        Field::Text(self.to_string())
    }

    fn refielded(field: &Field) -> Option<u128> {
        match field {
            Field::Text(text) => text.parse().ok(),
            other => whole_of(other).and_then(|value| u128::try_from(value).ok()),
        }
    }
}

impl Fielded for i128 {
    fn field(&self) -> Field {
        match *self < 0 {
            true => Field::Text(self.to_string()),
            false => u128::field(&self.unsigned_abs()),
        }
    }

    fn refielded(field: &Field) -> Option<i128> {
        text_of_field(field)
            .and_then(|text| text.parse().ok())
            .or_else(|| whole_of(field).map(i128::from))
    }
}

impl Fielded for f64 {
    fn field(&self) -> Field {
        Field::Real(*self)
    }

    fn refielded(field: &Field) -> Option<f64> {
        real_of(field)
    }
}

impl Fielded for String {
    fn field(&self) -> Field {
        Field::Text(self.clone())
    }

    fn refielded(field: &Field) -> Option<String> {
        match field {
            Field::Text(text) => Some(text.clone()),
            Field::Whole(value) => Some(value.to_string()),
            Field::Real(value) => Some(value.to_string()),
            _ => None,
        }
    }
}

impl<T: Fielded> Fielded for Option<T> {
    fn field(&self) -> Field {
        match self {
            Some(value) => value.field(),
            None => Field::Nothing,
        }
    }

    fn refielded(field: &Field) -> Option<Option<T>> {
        match field {
            Field::Nothing => Some(None),
            other => T::refielded(other).map(Some),
        }
    }
}

impl<T: Fielded> Fielded for Vec<T> {
    fn field(&self) -> Field {
        Field::List(self.iter().map(Fielded::field).collect())
    }

    fn refielded(field: &Field) -> Option<Vec<T>> {
        match field {
            Field::List(items) => items.iter().map(T::refielded).collect(),
            Field::Nothing => Some(vec![]),
            _ => None,
        }
    }
}

impl<A: Fielded, B: Fielded> Fielded for (A, B) {
    fn field(&self) -> Field {
        Field::List(vec![self.0.field(), self.1.field()])
    }

    fn refielded(field: &Field) -> Option<(A, B)> {
        match field {
            Field::List(items) => match items.as_slice() {
                [first, second] => Some((A::refielded(first)?, B::refielded(second)?)),
                _ => None,
            },
            _ => None,
        }
    }
}

impl<V: Fielded> Fielded for BTreeMap<String, V> {
    fn field(&self) -> Field {
        Field::Table(self.iter().map(|(key, value)| (key.clone(), value.field())).collect())
    }

    fn refielded(field: &Field) -> Option<BTreeMap<String, V>> {
        let rows = table_of(field)?;
        rows.iter()
            .map(|(key, value)| V::refielded(value).map(|v| (key.clone(), v)))
            .collect()
    }
}

impl<const N: usize> Fielded for [u8; N] {
    fn field(&self) -> Field {
        Field::Text(prefixed(&hex_text(self)))
    }

    fn refielded(field: &Field) -> Option<[u8; N]> {
        bytes_of(unprefixed(text_of_field(field)?))?.try_into().ok()
    }
}

pub fn recorded<T: Fielded>(value: &T) -> Field {
    value.field()
}
because!(recorded, "a value as its field, the call a binding makes where the trait method is not in reach");

pub fn restored<T: Fielded>(field: &Field) -> Option<T> {
    T::refielded(field)
}
because!(restored, "a value read back from its field, the reverse of recorded");

pub fn restored_all<T: Fielded>(fields: &[Field]) -> Vec<T> {
    fields.iter().filter_map(T::refielded).collect()
}
because!(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");

pub struct WireNaming;
source!(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");

const JOINER: char = '_';
because!(JOINER, WireNaming, "the mark between the words of a field name as a program writes it, which the wire drops");

pub fn camel_of(name: &str) -> String {
    let mut out = String::with_capacity(name.len());
    let mut lift = false;
    for c in name.chars() {
        if c == JOINER {
            lift = true;
        } else if lift {
            out.extend(c.to_uppercase());
            lift = false;
        } else {
            out.push(c);
        }
    }
    out
}
because!(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");