substrate-data 0.2.1

One data model, text syntax, binary form, packed frames and schema system — for any program's files, saves and wire messages.
Documentation
//! One conformance case: what it is called, the forms it carries, and how
//! it is written to the manifest and the raw files.

use substrate_data::{Value, binary, text};

use crate::hex::to_hex;
use crate::json::Json;
use crate::render::value_json;
use crate::schemas::shape_text;

/// Bytes longer than this are only in their file, not inline in the manifest.
const INLINE_MAX: usize = 256;

/// A frame, and every schema that reads it.
#[derive(Debug, Clone)]
pub(crate) struct Frame {
    /// The schema the frame was packed from, and what reading it by shape gives.
    pub(crate) writer: Reading,
    /// The frame's bytes.
    pub(crate) bytes: Vec<u8>,
    /// Other schemas that read the same bytes (an older or newer record).
    pub(crate) also: Vec<Reading>,
}

/// A schema and the value reading a frame by its shape gives.
#[derive(Debug, Clone)]
pub(crate) struct Reading {
    /// The schema's name in this suite.
    pub(crate) schema: &'static str,
    /// What `frame::read_shaped` gives.
    pub(crate) value: Value,
}

/// A byte stream of length-prefixed frames.
#[derive(Debug, Clone)]
pub(crate) struct Io {
    /// The whole stream.
    pub(crate) bytes: Vec<u8>,
    /// The frames in it, in order (each a binary document).
    pub(crate) messages: Vec<Vec<u8>>,
}

/// A malformed input and what reading it says.
#[derive(Debug, Clone)]
pub(crate) enum Fault {
    /// Text that does not parse.
    Text {
        /// The text.
        input: String,
        /// 1-based line.
        line: u32,
        /// 1-based column.
        col: u32,
        /// The reference reader's message.
        message: String,
    },
    /// Text that parses but a schema finds wrong.
    Schema {
        /// The schema's name in this suite.
        schema: &'static str,
        /// The text.
        input: String,
        /// Everything found, in file order.
        found: Vec<Found>,
    },
    /// Binary bytes that do not decode.
    Binary {
        /// The bytes.
        input: Vec<u8>,
        /// The class of error, the same in every port.
        kind: &'static str,
        /// The byte the reference reader stops at.
        offset: usize,
        /// The reference reader's message.
        message: String,
        /// The diagnostic code reading them as a schema gives.
        code: String,
    },
    /// A frame no reading accepts.
    Frame {
        /// The schema it is read as.
        schema: &'static str,
        /// The bytes.
        input: Vec<u8>,
    },
    /// A stream that ends or lies badly.
    Io {
        /// The bytes.
        input: Vec<u8>,
        /// `unexpected_eof` or `invalid_data`.
        kind: &'static str,
        /// How many frames read well before it.
        frames_before: usize,
    },
}

/// One diagnostic a schema reading gives.
#[derive(Debug, Clone)]
pub(crate) struct Found {
    /// `SD00xx`.
    pub(crate) code: String,
    /// 1-based line.
    pub(crate) line: u32,
    /// 1-based column.
    pub(crate) col: u32,
    /// The value's path in the document.
    pub(crate) path: String,
    /// The reference reader's message.
    pub(crate) message: String,
}

/// A case.
#[derive(Debug, Clone)]
pub(crate) struct Case {
    /// Unique, `snake_case`: the file names derive from it.
    pub(crate) name: String,
    /// What the case shows.
    pub(crate) description: String,
    /// The text form.
    pub(crate) text: Option<String>,
    /// The expected model value.
    pub(crate) value: Option<Value>,
    /// The binary form (`SDAT` document).
    pub(crate) binary: Option<Vec<u8>>,
    /// A packed frame.
    pub(crate) frame: Option<Frame>,
    /// A length-prefixed frame stream.
    pub(crate) io: Option<Io>,
    /// A negative case.
    pub(crate) fault: Option<Fault>,
}

impl Case {
    /// A case with nothing in it yet.
    pub(crate) fn new(name: &str, description: &str) -> Case {
        Case {
            name: name.to_owned(),
            description: description.to_owned(),
            text: None,
            value: None,
            binary: None,
            frame: None,
            io: None,
            fault: None,
        }
    }

    /// With text `t`.
    #[must_use]
    pub(crate) fn text(mut self, t: &str) -> Case {
        self.text = Some(t.to_owned());
        self
    }

    /// With the expected value `v`.
    #[must_use]
    pub(crate) fn value(mut self, v: Value) -> Case {
        self.value = Some(v);
        self
    }

    /// With the binary form `b`.
    #[must_use]
    pub(crate) fn binary(mut self, b: Vec<u8>) -> Case {
        self.binary = Some(b);
        self
    }

    /// With the value, its canonical text and its canonical binary form.
    #[must_use]
    pub(crate) fn all_forms(self, v: Value) -> Case {
        let t = text::to_string(&v);
        let b = binary::encode(&v);
        self.text(&t).binary(b).value(v)
    }

    /// With frame `f`.
    #[must_use]
    pub(crate) fn frame(mut self, f: Frame) -> Case {
        self.frame = Some(f);
        self
    }

    /// With stream `i`.
    #[must_use]
    pub(crate) fn io(mut self, i: Io) -> Case {
        self.io = Some(i);
        self
    }

    /// With the fault `f`.
    #[must_use]
    pub(crate) fn fault(mut self, f: Fault) -> Case {
        self.fault = Some(f);
        self
    }

    /// The case as a manifest entry.
    pub(crate) fn to_json(&self) -> Json {
        let mut out = vec![
            ("name".to_owned(), Json::text(&self.name)),
            ("description".to_owned(), Json::text(&self.description)),
        ];
        self.put_value_forms(&mut out);
        if let Some(b) = &self.binary {
            put_bytes("binary", b, &format!("binary/{}.sdat", self.name), &mut out);
        }
        if let Some(f) = &self.frame {
            out.push(("frame".to_owned(), frame_json(f, &self.name)));
        }
        if let Some(i) = &self.io {
            out.push(("io".to_owned(), io_json(i, &self.name)));
        }
        if let Some(f) = &self.fault {
            out.push(("error".to_owned(), fault_json(f)));
        }
        Json::Block(out)
    }

    fn put_value_forms(&self, out: &mut Vec<(String, Json)>) {
        if let Some(t) = &self.text {
            out.push(("text".to_owned(), Json::text(t)));
            let writes = self
                .value
                .as_ref()
                .is_some_and(|v| text::to_string(v) == *t);
            out.push(("text_is_written".to_owned(), Json::Bool(writes)));
        }
        if let Some(v) = &self.value {
            out.push(("value".to_owned(), value_json(v)));
        }
        if let (Some(v), Some(b)) = (&self.value, &self.binary) {
            let encodes = binary::encode(v) == *b;
            out.push(("binary_is_encoded".to_owned(), Json::Bool(encodes)));
        }
    }

    /// The raw files this case writes: path under `conformance/`, bytes.
    pub(crate) fn files(&self) -> Vec<(String, Vec<u8>)> {
        let mut files = Vec::new();
        if let Some(b) = &self.binary {
            files.push((format!("binary/{}.sdat", self.name), b.clone()));
        }
        if let Some(f) = &self.frame {
            files.push((format!("frames/{}.frame", self.name), f.bytes.clone()));
        }
        if let Some(i) = &self.io {
            files.push((format!("io/{}.stream", self.name), i.bytes.clone()));
        }
        files
    }
}

/// `bytes` as `key` (hex, when short), `key_len` and `key_file`.
fn put_bytes(key: &str, bytes: &[u8], file: &str, out: &mut Vec<(String, Json)>) {
    if bytes.len() <= INLINE_MAX {
        out.push((key.to_owned(), Json::text(to_hex(bytes))));
    }
    out.push((format!("{key}_len"), Json::num(bytes.len())));
    out.push((format!("{key}_file"), Json::text(file)));
}

fn reading_json(r: &Reading) -> Vec<(String, Json)> {
    vec![
        ("schema".to_owned(), Json::text(r.schema)),
        ("shape".to_owned(), Json::text(shape_text(r.schema))),
        ("value".to_owned(), value_json(&r.value)),
    ]
}

fn frame_json(f: &Frame, name: &str) -> Json {
    let mut out = reading_json(&f.writer);
    put_bytes("bytes", &f.bytes, &format!("frames/{name}.frame"), &mut out);
    if !f.also.is_empty() {
        let also = f.also.iter().map(|r| Json::Obj(reading_json(r))).collect();
        out.push(("also_read_by".to_owned(), Json::Arr(also)));
    }
    Json::Block(out)
}

fn io_json(i: &Io, name: &str) -> Json {
    let mut out = Vec::new();
    put_bytes("bytes", &i.bytes, &format!("io/{name}.stream"), &mut out);
    let messages = i.messages.iter().map(|m| Json::text(to_hex(m))).collect();
    out.push(("messages".to_owned(), Json::Arr(messages)));
    out.push(("end".to_owned(), Json::text("clean")));
    Json::Block(out)
}

fn fault_json(f: &Fault) -> Json {
    let entries = match f {
        Fault::Text {
            input,
            line,
            col,
            message,
        } => text_fault(input, *line, *col, message),
        Fault::Schema {
            schema,
            input,
            found,
        } => schema_fault(schema, input, found),
        Fault::Binary {
            input,
            kind,
            offset,
            message,
            code,
        } => binary_fault(input, kind, *offset, message, code),
        Fault::Frame { schema, input } => vec![
            ("form", Json::text("frame")),
            ("schema", Json::text(*schema)),
            ("shape", Json::text(shape_text(schema))),
            ("input", Json::text(to_hex(input))),
        ],
        Fault::Io {
            input,
            kind,
            frames_before,
        } => vec![
            ("form", Json::text("io")),
            ("input", Json::text(to_hex(input))),
            ("frames_before", Json::num(*frames_before)),
            ("kind", Json::text(*kind)),
        ],
    };
    Json::Block(
        entries
            .into_iter()
            .map(|(k, v)| (k.to_owned(), v))
            .collect(),
    )
}

type Entries = Vec<(&'static str, Json)>;

fn text_fault(input: &str, line: u32, col: u32, message: &str) -> Entries {
    vec![
        ("form", Json::text("text")),
        ("input", Json::text(input)),
        ("code", Json::text("SD0001")),
        ("line", Json::Num(u64::from(line))),
        ("col", Json::Num(u64::from(col))),
        ("message", Json::text(message)),
    ]
}

fn schema_fault(schema: &str, input: &str, found: &[Found]) -> Entries {
    vec![
        ("form", Json::text("text_schema")),
        ("schema", Json::text(schema)),
        ("input", Json::text(input)),
        (
            "diagnostics",
            Json::Arr(found.iter().map(found_json).collect()),
        ),
    ]
}

fn binary_fault(input: &[u8], kind: &str, offset: usize, message: &str, code: &str) -> Entries {
    vec![
        ("form", Json::text("binary")),
        ("input", Json::text(to_hex(input))),
        ("kind", Json::text(kind)),
        ("offset", Json::num(offset)),
        ("message", Json::text(message)),
        ("schema_code", Json::text(code)),
    ]
}

fn found_json(d: &Found) -> Json {
    Json::obj([
        ("code", Json::text(&d.code)),
        ("line", Json::Num(u64::from(d.line))),
        ("col", Json::Num(u64::from(d.col))),
        ("path", Json::text(&d.path)),
        ("message", Json::text(&d.message)),
    ])
}