use std::collections::HashMap;
use std::fmt;
use serde::de::{Deserializer, MapAccess, SeqAccess, Visitor};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "lowercase")]
pub(crate) enum Shape {
Text,
Object,
Other,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum Node {
Text(String),
Object(Vec<(String, Node)>),
Other,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Entry {
pub(crate) key: String,
pub(crate) shape: Shape,
pub(crate) text: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Duplicate {
pub(crate) key: String,
pub(crate) occurrences: usize,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub(crate) struct Parsed {
pub(crate) entries: Vec<Entry>,
pub(crate) duplicates: Vec<Duplicate>,
}
pub(crate) fn parse(content: &str) -> Result<Parsed, String> {
let content = content.strip_prefix('\u{feff}').unwrap_or(content);
let node: Node = serde_json::from_str(content).map_err(|error| error.to_string())?;
let Node::Object(root) = node else {
return Err("a catalogue must be a JSON object".to_string());
};
let mut parsed = Parsed::default();
walk("", &root, &mut parsed);
Ok(parsed)
}
fn walk(prefix: &str, object: &[(String, Node)], parsed: &mut Parsed) {
for (name, node) in resolve(prefix, object, parsed) {
let key = path(prefix, name);
match node {
Node::Text(text) => parsed.entries.push(Entry {
key,
shape: Shape::Text,
text: Some(text.clone()),
}),
Node::Other => parsed.entries.push(Entry {
key,
shape: Shape::Other,
text: None,
}),
Node::Object(children) => {
parsed.entries.push(Entry {
key: key.clone(),
shape: Shape::Object,
text: None,
});
walk(&key, children, parsed);
}
}
}
}
fn path(prefix: &str, name: &str) -> String {
if prefix.is_empty() {
return name.to_string();
}
format!("{prefix}.{name}")
}
fn resolve<'a>(
prefix: &str,
object: &'a [(String, Node)],
parsed: &mut Parsed,
) -> Vec<(&'a str, &'a Node)> {
let mut resolved: Vec<(&'a str, &'a Node)> = Vec::new();
let mut occurrences: Vec<usize> = Vec::new();
let mut first: HashMap<&'a str, usize> = HashMap::new();
for (name, node) in object {
if let Some(&at) = first.get(name.as_str()) {
resolved[at].1 = node;
occurrences[at] += 1;
continue;
}
first.insert(name.as_str(), resolved.len());
resolved.push((name.as_str(), node));
occurrences.push(1);
}
for ((name, _), count) in resolved.iter().zip(&occurrences) {
if *count < 2 {
continue;
}
parsed.duplicates.push(Duplicate {
key: path(prefix, name),
occurrences: *count,
});
}
resolved
}
impl<'de> Deserialize<'de> for Node {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
deserializer.deserialize_any(NodeVisitor)
}
}
struct NodeVisitor;
impl<'de> Visitor<'de> for NodeVisitor {
type Value = Node;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a JSON value")
}
fn visit_str<E>(self, value: &str) -> Result<Node, E> {
Ok(Node::Text(value.to_string()))
}
fn visit_string<E>(self, value: String) -> Result<Node, E> {
Ok(Node::Text(value))
}
fn visit_bool<E>(self, _: bool) -> Result<Node, E> {
Ok(Node::Other)
}
fn visit_i64<E>(self, _: i64) -> Result<Node, E> {
Ok(Node::Other)
}
fn visit_u64<E>(self, _: u64) -> Result<Node, E> {
Ok(Node::Other)
}
fn visit_f64<E>(self, _: f64) -> Result<Node, E> {
Ok(Node::Other)
}
fn visit_unit<E>(self) -> Result<Node, E> {
Ok(Node::Other)
}
fn visit_none<E>(self) -> Result<Node, E> {
Ok(Node::Other)
}
fn visit_seq<A: SeqAccess<'de>>(self, mut sequence: A) -> Result<Node, A::Error> {
while sequence.next_element::<serde::de::IgnoredAny>()?.is_some() {}
Ok(Node::Other)
}
fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Node, A::Error> {
let mut entries = Vec::new();
while let Some((key, value)) = map.next_entry::<String, Node>()? {
entries.push((key, value));
}
Ok(Node::Object(entries))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn keys(content: &str) -> Vec<String> {
parse(content)
.expect("parses")
.entries
.into_iter()
.map(|entry| entry.key)
.collect()
}
#[test]
fn a_flat_catalogue_yields_its_keys() {
assert_eq!(keys(r#"{"a":"one","b":"two"}"#), ["a", "b"]);
}
#[test]
fn a_nested_catalogue_flattens_to_dotted_paths() {
assert_eq!(
keys(r#"{"a":{"b":{"c":"x"}}}"#),
["a", "a.b", "a.b.c"],
"the objects on the way down are entries too"
);
let nested: Vec<String> = parse(r#"{"a":{"b":"x"}}"#)
.expect("parses")
.entries
.into_iter()
.filter(|entry| entry.shape == Shape::Text)
.map(|entry| entry.key)
.collect();
assert_eq!(nested, ["a.b"]);
assert_eq!(keys(r#"{"a.b":"x"}"#), ["a.b"]);
}
#[test]
fn a_duplicate_key_is_reported_and_the_last_value_wins() {
let parsed = parse(r#"{"a":"first","b":"other","a":"second"}"#).expect("parses");
assert_eq!(parsed.duplicates.len(), 1);
assert_eq!(parsed.duplicates[0].key, "a");
assert_eq!(parsed.duplicates[0].occurrences, 2);
assert_eq!(
parsed.entries[0].text.as_deref(),
Some("second"),
"the runtime would read the last one"
);
assert_eq!(keys(r#"{"a":"first","b":"o","a":"second"}"#), ["a", "b"]);
}
#[test]
fn a_duplicate_nested_key_is_named_by_its_path() {
let parsed = parse(r#"{"menu":{"open":"a","open":"b"}}"#).expect("parses");
assert_eq!(parsed.duplicates[0].key, "menu.open");
}
#[test]
fn the_shape_of_every_path_is_recorded() {
let parsed = parse(r#"{"t":"x","o":{"n":"y"},"n":1,"a":[1,2],"z":null}"#).expect("parses");
let shapes: Vec<(String, Shape)> = parsed
.entries
.into_iter()
.map(|entry| (entry.key, entry.shape))
.collect();
assert_eq!(
shapes,
[
("t".to_string(), Shape::Text),
("o".to_string(), Shape::Object),
("o.n".to_string(), Shape::Text),
("n".to_string(), Shape::Other),
("a".to_string(), Shape::Other),
("z".to_string(), Shape::Other),
]
);
}
#[test]
fn metadata_keys_are_read_rather_than_dropped() {
assert_eq!(
keys(r#"{"@@locale":"es","greeting":"Hola","@greeting":{"description":"x"}}"#),
["@@locale", "greeting", "@greeting", "@greeting.description"]
);
}
#[test]
fn a_leading_byte_order_mark_is_not_part_of_the_document() {
assert_eq!(keys("\u{feff}{\"a\":\"x\"}"), ["a"]);
}
#[test]
fn every_scalar_shape_is_read_as_neither_text_nor_object() {
let parsed =
parse(r#"{"i":-3,"u":7,"f":1.5,"t":true,"f2":false,"z":null,"a":[1,{"b":"x"}]}"#)
.expect("parses");
let shapes: Vec<Shape> = parsed.entries.iter().map(|entry| entry.shape).collect();
assert_eq!(shapes, [Shape::Other; 7]);
}
#[test]
fn a_document_that_is_not_an_object_is_refused() {
assert!(parse("[1,2]").is_err());
assert!(parse("\"just a string\"").is_err());
}
#[test]
fn malformed_json_is_refused_rather_than_guessed_at() {
assert!(parse("{\"a\":").is_err());
}
#[test]
fn a_document_nested_past_the_readers_limit_is_refused_rather_than_crashing() {
let depth = 2_000;
let document = format!("{}\"leaf\"{}", "{\"a\":".repeat(depth), "}".repeat(depth));
assert!(parse(&document).is_err());
}
#[test]
fn a_key_repeated_many_times_is_counted_once() {
let repeats = 500;
let pairs: Vec<String> = (0..repeats).map(|n| format!("\"a\":\"{n}\"")).collect();
let parsed = parse(&format!("{{{}}}", pairs.join(","))).expect("parses");
assert_eq!(parsed.entries.len(), 1);
assert_eq!(parsed.entries[0].text.as_deref(), Some("499"));
assert_eq!(parsed.duplicates.len(), 1);
assert_eq!(parsed.duplicates[0].occurrences, repeats);
}
#[test]
fn an_empty_catalogue_yields_nothing() {
assert_eq!(parse("{}").expect("parses"), Parsed::default());
}
#[test]
fn a_refusal_names_the_position_and_never_the_content() {
for document in [
r#"{"a":"Bienvenido de nuevo""#,
r#"{"a":"Bienvenido", }"#,
r#"{"a": Bienvenido}"#,
r#"{"a":"Bienvenido"} Bienvenido"#,
"{\"a\":\"Bienv\u{1}enido\"}",
r#"{"a":"Bienvenido\q"}"#,
r#"{"a":"Bienvenido\ud800"}"#,
r#"{"Bienvenido"}"#,
r#"{a:"Bienvenido"}"#,
r#"{"a":'Bienvenido'}"#,
r#"{"a":"Bienvenido",,}"#,
r#"{"a":1e999999,"b":"Bienvenido"}"#,
r#"["Bienvenido"]"#,
r#""Bienvenido""#,
] {
let refusal = parse(document).expect_err(document);
assert!(!refusal.contains("Bienvenido"), "{refusal}");
}
}
}