use kallo_core::{
FILM_STOCKS, FilmProcess, FilmStock, PAPER_STOCKS, PaperEmulsion, PaperStock, Provenance,
};
use serde_json::{Map, Value, json};
const CLAIMS: [&str; 4] = ["measured", "borrowed", "placeholder", "none"];
pub fn value() -> Value {
json!({
"schema": "kallo/catalogue/1",
"films": FILM_STOCKS.iter().map(film).collect::<Vec<Value>>(),
"papers": PAPER_STOCKS.iter().map(paper).collect::<Vec<Value>>(),
})
}
fn film(stock: &FilmStock) -> Value {
json!({
"id": stock.id,
"short": stock.short,
"label": stock.label,
"process": process(stock.process),
"note": stock.note,
"reference": {
"minutes": stock.reference_time_min,
"temp_c": stock.reference_temp_c,
},
"provenance": provenance(&stock.provenance),
"measured": measured(stock.measured),
})
}
fn paper(stock: &PaperStock) -> Value {
json!({
"id": stock.id,
"short": stock.short,
"label": stock.label,
"emulsion": match stock.emulsion {
PaperEmulsion::Colour => "colour",
PaperEmulsion::Silver => "silver",
},
"note": stock.note,
"provenance": provenance(&stock.provenance),
"measured": measured(stock.measured),
})
}
fn process(process: FilmProcess) -> &'static str {
match process {
FilmProcess::Negative => "negative",
FilmProcess::Reversal => "reversal",
FilmProcess::Monochrome => "monochrome",
}
}
fn provenance(provenance: &Provenance) -> Value {
json!({
"publication": provenance.publication,
"page": provenance.page,
"retrieved": provenance.retrieved,
"checksum": provenance.checksum,
})
}
fn measured(rows: &[(&str, &str)]) -> Vec<Value> {
rows.iter()
.map(|(field, text)| {
let (claim, detail) = split(text);
let mut row = Map::new();
row.insert("field".to_owned(), json!(field));
row.insert("claim".to_owned(), json!(claim));
row.insert("detail".to_owned(), json!(detail));
Value::Object(row)
})
.collect()
}
fn split(text: &str) -> (&str, &str) {
match text.split_once(':') {
Some((head, rest)) if is_claim(head) => (head, rest.trim()),
_ if is_claim(text) => (text, ""),
_ => ("", text),
}
}
fn is_claim(word: &str) -> bool {
let word = word.trim();
CLAIMS.iter().any(|claim| claim.eq_ignore_ascii_case(word))
}
#[must_use]
pub fn json() -> String {
serde_json::to_string(&value()).expect("a catalogue of strings is serialisable")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_stock_is_in_the_catalogue() {
let Value::Array(films) = &value()["films"] else {
panic!("no films");
};
assert_eq!(films.len(), FILM_STOCKS.len());
assert!(!films.is_empty());
let Value::Array(papers) = &value()["papers"] else {
panic!("no papers");
};
assert_eq!(papers.len(), PAPER_STOCKS.len());
}
#[test]
fn every_film_cites_its_sheet_and_its_borrows() {
for stock in FILM_STOCKS {
assert!(
!stock.provenance.publication.is_empty(),
"{} cites no publication",
stock.id
);
assert!(!stock.measured.is_empty(), "{} admits to nothing", stock.id);
assert!(!stock.note.trim().is_empty(), "{} has no note", stock.id);
for (field, text) in stock.measured {
assert!(
!text.trim().is_empty(),
"{} says nothing about its {field}",
stock.id
);
}
}
}
#[test]
fn the_papers_say_what_they_carry() {
let silver: Vec<&str> = PAPER_STOCKS
.iter()
.filter(|paper| paper.emulsion == PaperEmulsion::Silver)
.map(|paper| paper.id)
.collect();
assert_eq!(silver, ["polymax_ii_rc"]);
assert!(
PAPER_STOCKS
.iter()
.any(|paper| paper.emulsion == PaperEmulsion::Colour),
"no colour paper to print a colour film on"
);
let Value::Array(papers) = &value()["papers"] else {
panic!("no papers");
};
for paper in papers {
let emulsion = paper["emulsion"].as_str().expect("an emulsion");
assert!(emulsion == "colour" || emulsion == "silver", "{emulsion}");
}
}
#[test]
fn the_claim_comes_off_the_sentence() {
assert_eq!(
split("measured: page 4, three curves"),
("measured", "page 4, three curves")
);
assert_eq!(split("borrowed"), ("borrowed", ""));
assert_eq!(
split("none: a reversal has no orange mask"),
("none", "a reversal has no orange mask")
);
assert_eq!(
split("the sheet prints 0.4 at 5 nm: a note"),
("", "the sheet prints 0.4 at 5 nm: a note")
);
}
}