use std::fmt;
use serde::de::{Deserializer, IgnoredAny, MapAccess, Visitor};
use serde::Deserialize;
const REFERENCE: &str = include_str!("../../docs/reference.md");
struct Keys(Vec<String>);
impl<'de> Deserialize<'de> for Keys {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
struct KeysVisitor;
impl<'de> Visitor<'de> for KeysVisitor {
type Value = Keys;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("a JSON object")
}
fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Keys, A::Error> {
let mut keys = Vec::new();
while let Some((key, IgnoredAny)) = map.next_entry::<String, IgnoredAny>()? {
keys.push(key);
}
Ok(Keys(keys))
}
}
deserializer.deserialize_map(KeysVisitor)
}
}
pub(super) fn json_keys(line: &str) -> Vec<String> {
match serde_json::from_str::<Keys>(line) {
Ok(Keys(keys)) => keys,
Err(error) => panic!("not a JSON object ({error}): {line}"),
}
}
pub(super) fn section<'a>(markdown: &'a str, heading: &str) -> &'a str {
let mut level = 0;
let mut start = None;
let mut in_fence = false;
let mut offset = 0;
for line in markdown.split_inclusive('\n') {
if line.starts_with("```") {
in_fence = !in_fence;
}
let hashes = line.bytes().take_while(|&b| b == b'#').count();
if !in_fence && hashes > 0 && line[hashes..].starts_with(' ') {
match start {
None if line[hashes..].trim() == heading => {
level = hashes;
start = Some(offset + line.len());
}
Some(begin) if hashes <= level => return &markdown[begin..offset],
_ => {}
}
}
offset += line.len();
}
match start {
Some(begin) => &markdown[begin..],
None => panic!("no section headed `{heading}`"),
}
}
pub(super) fn reference_section(heading: &str) -> &'static str {
section(REFERENCE, heading)
}
fn cells(row: &str) -> Vec<&str> {
row.trim()
.trim_matches('|')
.split('|')
.map(str::trim)
.collect()
}
type Table<'a> = (Vec<&'a str>, Vec<Vec<&'a str>>);
fn tables(section: &str) -> Vec<Table<'_>> {
let mut tables = Vec::new();
let mut current: Vec<&str> = Vec::new();
for line in section.lines().chain(std::iter::once("")) {
if line.trim_start().starts_with('|') {
current.push(line);
} else if !current.is_empty() {
let header = cells(current[0]);
let rows = std::mem::take(&mut current)
.into_iter()
.skip(2)
.map(cells)
.collect();
tables.push((header, rows));
}
}
tables
}
fn unticked(cell: &str) -> &str {
cell.trim_matches('`')
}
fn first_code(text: &str) -> Option<&str> {
let rest = &text[text.find('`')? + 1..];
Some(&rest[..rest.find('`')?])
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(super) struct Field {
pub(super) name: String,
pub(super) ty: String,
pub(super) meaning: String,
}
impl Field {
fn from_row(row: &[&str]) -> Self {
assert!(
row.len() >= 3,
"a field table row without name, type and meaning: {row:?}"
);
Self {
name: unticked(row[0]).to_owned(),
ty: row[1].replace('`', ""),
meaning: row[2].to_owned(),
}
}
}
pub(super) fn field_rows(section: &str, kind: &str) -> Vec<Field> {
let quoted = format!("\"{kind}\"");
let found = tables(section).into_iter().find(|(_, rows)| {
rows.iter()
.any(|row| row.len() >= 3 && unticked(row[0]) == "kind" && row[2].contains("ed))
});
match found {
Some((_, rows)) => rows.iter().map(|row| Field::from_row(row)).collect(),
None => panic!("no field table for kind `{kind}` in the section"),
}
}
pub(super) fn first_table_rows(section: &str) -> Vec<Field> {
match tables(section).into_iter().next() {
Some((_, rows)) => rows.iter().map(|row| Field::from_row(row)).collect(),
None => panic!("no table in the section"),
}
}
pub(super) fn table_fields(section: &str, kind: &str) -> Vec<String> {
field_rows(section, kind)
.into_iter()
.map(|field| field.name)
.collect()
}
pub(super) fn first_table_fields(section: &str) -> Vec<String> {
match tables(section).into_iter().next() {
Some((_, rows)) => rows.iter().map(|row| unticked(row[0]).to_owned()).collect(),
None => panic!("no table in the section"),
}
}
fn conforms(phrase: &str, value: &serde_json::Value) -> bool {
use serde_json::Value;
match phrase {
"string" => value.is_string(),
"string or null" => value.is_string() || value.is_null(),
"number" | "numbers" => value.is_number(),
"number or null" => value.is_number() || value.is_null(),
"array of strings" => value
.as_array()
.is_some_and(|items| items.iter().all(Value::is_string)),
"array of objects" => value
.as_array()
.is_some_and(|items| items.iter().all(Value::is_object)),
other => panic!("a type phrase this helper does not know: `{other}`"),
}
}
fn constant(meaning: &str) -> Option<serde_json::Value> {
let code = first_code(meaning.strip_prefix("Always ")?)?;
Some(
serde_json::from_str(code)
.unwrap_or_else(|error| panic!("`Always {code}` is not JSON ({error})")),
)
}
pub(super) fn check_type(phrase: &str, value: &serde_json::Value, what: &str) {
assert!(
conforms(phrase, value),
"{what}: the docs say `{phrase}`, the value is {value}"
);
}
pub(super) fn check_fields(rows: &[Field], value: &serde_json::Value, what: &str) {
for field in rows {
let name = &field.name;
let Some(actual) = value.get(name) else {
panic!("{what}: the docs list `{name}`, the object has no such field: {value}");
};
check_type(&field.ty, actual, &format!("{what}: `{name}`"));
if let Some(fixed) = constant(&field.meaning) {
assert_eq!(*actual, fixed, "{what}: `{name}` is always `{fixed}`");
}
}
}
pub(super) fn assert_every_variant(
ordinals: impl IntoIterator<Item = usize>,
variants: usize,
what: &str,
) {
let seen: std::collections::BTreeSet<usize> = ordinals.into_iter().collect();
let all: std::collections::BTreeSet<usize> = (0..variants).collect();
assert_eq!(seen, all, "{what}: the samples miss a variant");
}
pub(super) fn kind_codes(section: &str) -> Vec<(String, u8)> {
let Some((_, rows)) = tables(section).into_iter().find(|(header, _)| {
header.len() == 2 && unticked(header[0]) == "kind" && unticked(header[1]) == "code"
}) else {
panic!("no table headed `kind` and `code` in the section");
};
rows.iter()
.map(|row| {
assert!(
row.len() == 2,
"a `kind` and `code` row without 2 cells: {row:?}"
);
let code = unticked(row[1]);
let code = code
.parse()
.unwrap_or_else(|_| panic!("`{code}` is not an exit code"));
(unticked(row[0]).to_owned(), code)
})
.collect()
}
pub(super) fn gate_rows(section: &str) -> Vec<(String, Vec<(String, String)>)> {
let Some((_, rows)) = tables(section)
.into_iter()
.find(|(header, _)| header.first().is_some_and(|h| unticked(h) == "gate"))
else {
panic!("no table headed `gate` in the section");
};
rows.iter()
.map(|row| {
assert!(
row.len() >= 2,
"a `gate` row without a Fields cell: {row:?}"
);
let mut fields = Vec::new();
for group in row[1].split(')') {
let Some((names, ty)) = group.split_once('(') else {
continue;
};
let mut rest = names;
while let Some(name) = first_code(rest) {
fields.push((name.to_owned(), ty.replace('`', "").trim().to_owned()));
let after = rest.find('`').unwrap_or(0) + name.len() + 2;
rest = &rest[after..];
}
}
assert!(!fields.is_empty(), "no typed field in `{}`", row[1]);
(unticked(row[0]).to_owned(), fields)
})
.collect()
}
pub(super) fn kind_rows(section: &str) -> Vec<(String, Vec<Field>)> {
let Some((_, rows)) = tables(section)
.into_iter()
.find(|(header, _)| header.first().is_some_and(|h| unticked(h) == "kind"))
else {
panic!("no table headed `kind` in the section");
};
rows.iter()
.map(|row| {
assert!(
row.len() == 3,
"a `kind` table row without 3 cells: {row:?}"
);
let fields = row[2]
.split(';')
.map(|clause| {
let clause = clause.trim();
let (name, rest) = clause
.strip_prefix('`')
.and_then(|rest| rest.split_once('`'))
.unwrap_or_else(|| panic!("no field name in `{clause}`"));
let (ty, meaning) = rest
.trim_start()
.strip_prefix('(')
.and_then(|rest| rest.split_once(')'))
.unwrap_or_else(|| panic!("no field type in `{clause}`"));
assert!(!ty.trim().is_empty(), "empty field type in `{clause}`");
Field {
name: name.to_owned(),
ty: ty.replace('`', "").trim().to_owned(),
meaning: meaning.trim_start_matches(',').trim().to_owned(),
}
})
.collect();
(unticked(row[0]).to_owned(), fields)
})
.collect()
}
pub(super) fn example_line<'a>(section: &'a str, kind: &str) -> Option<&'a str> {
let mut in_json = false;
for line in section.lines() {
if line.starts_with("```") {
in_json = !in_json && line.trim_end() == "```json";
} else if in_json && line.starts_with('{') {
let value: serde_json::Value = serde_json::from_str(line)
.unwrap_or_else(|error| panic!("a `json` example is not JSON ({error}): {line}"));
if value["kind"] == kind {
return Some(line);
}
}
}
None
}
pub(super) fn example_keys(section: &str, kind: &str) -> Option<Vec<String>> {
example_line(section, kind).map(json_keys)
}
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
const SAMPLE: &str = "\
# Top
## One
text
### Inner
| Field | Type | Meaning |
|-------|------|---------|
| `level` | string | Always `\"info\"` |
| `kind` | string | Always `\"thing\"` |
| `n` | number | A count |
| `m` | number or null | A maybe count |
| `names` | array of strings | Some names |
| `gate` | Fields | Meaning |
|--------|--------|---------|
| `g1` | `p`, `q` (numbers) | Both |
| `g2` | `p` (number), `q` (number or `null`) | One each |
| `g3` | `files` (array of strings) | Some |
| `kind` | When | Fields |
|--------|------|--------|
| `a` | now | `x` (number), the x (a count); `y` (string or `null`), the y |
| `b` | later | `z` (array of strings), examples: `one`, `two` |
| `kind` | `code` |
|--------|-------:|
| `a` | `3` |
| `b` | `4` |
```json
{\"level\":\"info\",\"kind\":\"thing\",\"n\":1}
```
```
# not a heading
```
## Two
other
";
#[test]
fn json_keys_keep_the_written_order() {
assert_eq!(json_keys(r#"{"b":1,"a":{"z":0},"c":[1]}"#), ["b", "a", "c"]);
}
#[test]
fn a_section_runs_to_the_next_heading_of_its_level_or_higher() {
let inner = section(SAMPLE, "Inner");
assert!(inner.contains("`level`") && inner.contains("# not a heading"));
assert!(!inner.contains("other"));
assert!(section(SAMPLE, "One").contains("text"));
assert_eq!(section(SAMPLE, "Two").trim(), "other");
}
#[test]
#[should_panic(expected = "no section headed `Missing`")]
fn a_missing_section_is_a_failure() {
section(SAMPLE, "Missing");
}
#[test]
fn the_table_for_a_kind_is_found_by_its_kind_row() {
let inner = section(SAMPLE, "Inner");
let fields = ["level", "kind", "n", "m", "names"];
assert_eq!(table_fields(inner, "thing"), fields);
assert_eq!(first_table_fields(inner), fields);
assert_eq!(
example_keys(inner, "thing").unwrap(),
["level", "kind", "n"]
);
assert_eq!(example_keys(inner, "other"), None);
assert_eq!(
example_line(inner, "thing"),
Some(r#"{"level":"info","kind":"thing","n":1}"#)
);
assert_eq!(example_line(inner, "other"), None);
}
#[test]
fn kind_rows_read_the_name_type_and_meaning_of_each_clause() {
let field = |name: &str, ty: &str, meaning: &str| Field {
name: name.to_owned(),
ty: ty.to_owned(),
meaning: meaning.to_owned(),
};
assert_eq!(
kind_rows(section(SAMPLE, "Inner")),
[
(
"a".to_owned(),
vec![
field("x", "number", "the x (a count)"),
field("y", "string or null", "the y"),
]
),
(
"b".to_owned(),
vec![field("z", "array of strings", "examples: `one`, `two`")]
),
]
);
}
#[test]
#[should_panic(expected = "no field type")]
fn a_warning_field_without_a_type_is_a_failure() {
kind_rows(&SAMPLE.replace("`y` (string or `null`)", "`y`"));
}
#[test]
#[should_panic(expected = "empty field type")]
fn a_warning_field_with_an_empty_type_is_a_failure() {
kind_rows(&SAMPLE.replace("`y` (string or `null`)", "`y` ()"));
}
#[test]
#[should_panic(expected = "no field table for kind `nothing`")]
fn an_undocumented_kind_is_a_failure() {
table_fields(section(SAMPLE, "Inner"), "nothing");
}
fn thing(extra: &str) -> serde_json::Value {
let line =
format!(r#"{{"level":"info","kind":"thing","n":1,"m":null,"names":["x"]{extra}}}"#);
serde_json::from_str(&line).unwrap()
}
#[test]
fn a_value_that_fits_the_table_passes() {
let rows = field_rows(section(SAMPLE, "Inner"), "thing");
check_fields(&rows, &thing(""), "thing");
let types: Vec<_> = rows.iter().map(|f| f.ty.as_str()).collect();
assert_eq!(
types,
[
"string",
"string",
"number",
"number or null",
"array of strings"
]
);
}
#[test]
#[should_panic(expected = "thing: `n`: the docs say `number`, the value is \"1\"")]
fn a_value_of_another_type_is_a_failure() {
let rows = field_rows(section(SAMPLE, "Inner"), "thing");
let mut value = thing("");
value["n"] = "1".into();
check_fields(&rows, &value, "thing");
}
#[test]
#[should_panic(expected = "`level` is always `\"info\"`")]
fn a_value_other_than_the_constant_is_a_failure() {
let rows = field_rows(section(SAMPLE, "Inner"), "thing");
let mut value = thing("");
value["level"] = "warning".into();
check_fields(&rows, &value, "thing");
}
#[test]
#[should_panic(expected = "the object has no such field")]
fn a_documented_field_that_is_missing_is_a_failure() {
let rows = field_rows(section(SAMPLE, "Inner"), "thing");
let mut value = thing("");
value.as_object_mut().unwrap().remove("m");
check_fields(&rows, &value, "thing");
}
#[test]
fn each_type_phrase_accepts_its_own_values_only() {
use serde_json::json;
let cases = [
("string", json!("s"), json!(1)),
("string or null", json!("s"), json!(1)),
("string or null", json!(null), json!([])),
("number", json!(1.5), json!(null)),
("numbers", json!(2), json!("2")),
("number or null", json!(null), json!("x")),
("array of strings", json!(["a"]), json!(["a", 1])),
("array of objects", json!([{}]), json!([1])),
];
for (phrase, good, bad) in cases {
assert!(conforms(phrase, &good), "{phrase}: {good}");
assert!(!conforms(phrase, &bad), "{phrase}: {bad}");
}
}
#[test]
#[should_panic(expected = "a type phrase this helper does not know: `integer`")]
fn an_unknown_type_phrase_is_a_failure() {
conforms("integer", &serde_json::json!(1));
}
#[test]
fn always_followed_by_a_json_value_is_a_constant() {
use serde_json::json;
assert_eq!(
constant("Always `\"info\"`; more text"),
Some(json!("info"))
);
assert_eq!(constant("Always `3`"), Some(json!(3)));
assert_eq!(constant("Always `true`"), Some(json!(true)));
assert_eq!(constant("Always"), None);
assert_eq!(constant("A count"), None);
}
#[test]
#[should_panic(expected = "`Always info` is not JSON")]
fn a_constant_that_is_not_json_is_a_failure() {
constant("Always `info`");
}
#[test]
#[should_panic(expected = "`n` is always `3`")]
fn a_number_other_than_the_constant_is_a_failure() {
let rows = vec![Field {
name: "n".to_owned(),
ty: "number".to_owned(),
meaning: "Always `3`".to_owned(),
}];
check_fields(&rows, &serde_json::json!({"n": 4}), "thing");
}
#[test]
fn the_kind_and_code_table_is_read_by_its_header() {
let inner = section(SAMPLE, "Inner");
assert_eq!(
kind_codes(inner),
[("a".to_owned(), 3), ("b".to_owned(), 4)]
);
}
#[test]
fn the_gate_table_gives_each_field_its_type() {
let pair = |name: &str, ty: &str| (name.to_owned(), ty.to_owned());
assert_eq!(
gate_rows(section(SAMPLE, "Inner")),
[
(
"g1".to_owned(),
vec![pair("p", "numbers"), pair("q", "numbers")]
),
(
"g2".to_owned(),
vec![pair("p", "number"), pair("q", "number or null")]
),
("g3".to_owned(), vec![pair("files", "array of strings")]),
]
);
}
#[test]
fn every_variant_needs_a_sample() {
assert_every_variant([0, 2, 1, 1], 3, "thing");
}
#[test]
#[should_panic(expected = "thing: the samples miss a variant")]
fn a_variant_without_a_sample_is_a_failure() {
assert_every_variant([0, 2], 3, "thing");
}
}