Skip to main content

asc_hap/
value.rs

1//! Tolerant, by-hand reading of `serde_json` documents: a wrongly typed
2//! field becomes a [`Note`] and is skipped, a field nobody asked for is
3//! reported as unmodelled, and nothing fails the document.
4
5use std::borrow::Cow;
6use std::fmt;
7
8use serde_json::{Map, Value};
9
10use crate::HapError;
11
12/// A field that was present but could not be used.
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub struct Note {
15    /// JSON path of the field (`module.abilities[0].exported`).
16    pub path: String,
17    pub message: String,
18}
19
20impl fmt::Display for Note {
21    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
22        write!(f, "{}: {}", self.path, self.message)
23    }
24}
25
26/// What reading a document left behind besides the model.
27#[derive(Debug, Default)]
28pub(crate) struct Findings {
29    pub notes: Vec<Note>,
30    /// JSON paths of fields the model has no place for.
31    pub unmodelled: Vec<String>,
32}
33
34/// Parse `bytes` as a JSON document whose root is an object. A UTF-8 byte
35/// order mark is accepted.
36pub(crate) fn parse_object(bytes: &[u8]) -> Result<Map<String, Value>, HapError> {
37    let bytes = bytes.strip_prefix(b"\xef\xbb\xbf").unwrap_or(bytes);
38    match serde_json::from_slice(bytes)? {
39        Value::Object(map) => Ok(map),
40        other => Err(HapError::NotAnObject { found: kind(&other) }),
41    }
42}
43
44/// The document re-serialized with two-space indentation and a trailing
45/// newline.
46pub(crate) fn pretty(raw: &Value) -> String {
47    let mut text = serde_json::to_string_pretty(raw).expect("a serde_json::Value always serializes");
48    text.push('\n');
49    text
50}
51
52pub(crate) fn kind(value: &Value) -> &'static str {
53    match value {
54        Value::Null => "null",
55        Value::Bool(_) => "boolean",
56        Value::Number(_) => "number",
57        Value::String(_) => "string",
58        Value::Array(_) => "array",
59        Value::Object(_) => "object",
60    }
61}
62
63/// One JSON object being read. Every accessor records the key as modelled;
64/// [`Obj::finish`] reports the keys nobody asked for.
65pub(crate) struct Obj<'v, 'f> {
66    map: &'v Map<String, Value>,
67    path: String,
68    findings: &'f mut Findings,
69    read: Vec<&'static str>,
70}
71
72impl<'v, 'f> Obj<'v, 'f> {
73    pub fn root(map: &'v Map<String, Value>, findings: &'f mut Findings) -> Self {
74        Obj { map, path: String::new(), findings, read: Vec::new() }
75    }
76
77    pub fn to_value(&self) -> Value {
78        Value::Object(self.map.clone())
79    }
80
81    fn child_path(&self, key: &str) -> String {
82        if self.path.is_empty() { key.to_owned() } else { format!("{}.{key}", self.path) }
83    }
84
85    /// The value under `key`; JSON `null` counts as absent.
86    fn get(&mut self, key: &'static str) -> Option<&'v Value> {
87        self.read.push(key);
88        self.map.get(key).filter(|v| !v.is_null())
89    }
90
91    fn mismatch(&mut self, path: String, expected: &str, found: &Value) {
92        self.findings.notes.push(Note { path, message: format!("expected {expected}, found {}", kind(found)) });
93    }
94
95    fn typed<T>(&mut self, key: &'static str, expected: &str, pick: impl FnOnce(&'v Value) -> Option<T>) -> Option<T> {
96        let value = self.get(key)?;
97        let picked = pick(value);
98        if picked.is_none() {
99            self.mismatch(self.child_path(key), expected, value);
100        }
101        picked
102    }
103
104    pub fn str(&mut self, key: &'static str) -> Option<String> {
105        self.typed(key, "string", |v| v.as_str().map(str::to_owned))
106    }
107
108    /// A string, or a number or boolean spelled as text: for fields such
109    /// as `port` and metadata values that tools write either way.
110    pub fn text(&mut self, key: &'static str) -> Option<String> {
111        self.typed(key, "string, number or boolean", |v| match v {
112            Value::String(s) => Some(s.clone()),
113            Value::Number(n) => Some(n.to_string()),
114            Value::Bool(b) => Some(b.to_string()),
115            Value::Null | Value::Array(_) | Value::Object(_) => None,
116        })
117    }
118
119    pub fn bool(&mut self, key: &'static str) -> Option<bool> {
120        self.typed(key, "boolean", Value::as_bool)
121    }
122
123    pub fn int(&mut self, key: &'static str) -> Option<i64> {
124        self.typed(key, "integer", Value::as_i64)
125    }
126
127    /// An array of strings; elements of another type are noted and skipped.
128    pub fn strings(&mut self, key: &'static str) -> Vec<String> {
129        let Some(items) = self.typed(key, "array", Value::as_array) else {
130            return Vec::new();
131        };
132        let mut out = Vec::with_capacity(items.len());
133        for (i, item) in items.iter().enumerate() {
134            match item.as_str() {
135                Some(s) => out.push(s.to_owned()),
136                None => self.mismatch(format!("{}[{i}]", self.child_path(key)), "string", item),
137            }
138        }
139        out
140    }
141
142    pub fn object<T>(&mut self, key: &'static str, read: impl FnOnce(&mut Obj<'v, '_>) -> T) -> Option<T> {
143        let map = self.typed(key, "object", Value::as_object)?;
144        Some(self.descend(map, self.child_path(key), read))
145    }
146
147    /// An array of objects; elements of another type are noted and skipped.
148    pub fn objects<T>(&mut self, key: &'static str, mut read: impl FnMut(&mut Obj<'v, '_>) -> T) -> Vec<T> {
149        let Some(items) = self.typed(key, "array", Value::as_array) else {
150            return Vec::new();
151        };
152        let mut out = Vec::with_capacity(items.len());
153        for (i, item) in items.iter().enumerate() {
154            let path = format!("{}[{i}]", self.child_path(key));
155            match item.as_object() {
156                Some(map) => out.push(self.descend(map, path, &mut read)),
157                None => self.mismatch(path, "object", item),
158            }
159        }
160        out
161    }
162
163    fn descend<T>(&mut self, map: &'v Map<String, Value>, path: String, read: impl FnOnce(&mut Obj<'v, '_>) -> T) -> T {
164        let mut child = Obj { map, path, findings: self.findings, read: Vec::new() };
165        let value = read(&mut child);
166        child.finish();
167        value
168    }
169
170    /// Mark presentation-only keys as known so they are not reported.
171    pub fn ignore(&mut self, keys: &[&'static str]) {
172        self.read.extend_from_slice(keys);
173    }
174
175    /// Report every key that no accessor asked for. `fooId`, the numeric
176    /// resource id the packer writes next to a `$type:name` reference in
177    /// `foo`, is not reported when `foo` is modelled.
178    pub fn finish(self) {
179        for key in self.map.keys() {
180            let key = key.as_str();
181            let known = |k: &str| self.read.contains(&k);
182            if known(key) || key.strip_suffix("Id").is_some_and(known) {
183                continue;
184            }
185            self.findings.unmodelled.push(self.child_path(key));
186        }
187    }
188}
189
190/// A value as one whitespace-free token: verbatim when it already is one,
191/// otherwise as a JSON string literal.
192pub(crate) fn token(text: &str) -> Cow<'_, str> {
193    if !text.is_empty() && !text.chars().any(|c| c.is_whitespace() || c.is_control() || c == '"') {
194        return Cow::Borrowed(text);
195    }
196    Cow::Owned(Value::from(text).to_string())
197}
198
199/// `key=value` fragments of a summary line; absent values print as `-`.
200pub(crate) struct Line(String);
201
202impl Line {
203    pub fn new(record: &str) -> Self {
204        Line(record.to_owned())
205    }
206
207    /// A bare positional word.
208    #[must_use]
209    pub fn word(mut self, text: &str) -> Self {
210        self.0.push(' ');
211        self.0.push_str(&token(text));
212        self
213    }
214
215    #[must_use]
216    pub fn raw(mut self, key: &str, value: impl fmt::Display) -> Self {
217        self.0.push_str(&format!(" {key}={value}"));
218        self
219    }
220
221    #[must_use]
222    pub fn opt(self, key: &str, value: Option<&str>) -> Self {
223        match value {
224            Some(v) => {
225                let v = token(v);
226                self.raw(key, v)
227            }
228            None => self.raw(key, "-"),
229        }
230    }
231
232    #[must_use]
233    pub fn flag(self, key: &str, value: Option<bool>) -> Self {
234        self.num(key, value)
235    }
236
237    #[must_use]
238    pub fn num<T: fmt::Display>(self, key: &str, value: Option<T>) -> Self {
239        match value {
240            Some(v) => self.raw(key, v),
241            None => self.raw(key, "-"),
242        }
243    }
244
245    #[must_use]
246    pub fn list(self, key: &str, values: &[String]) -> Self {
247        if values.is_empty() {
248            return self.raw(key, "-");
249        }
250        let joined = values.iter().map(|v| token(v)).collect::<Vec<_>>().join(",");
251        self.raw(key, joined)
252    }
253
254    /// Like [`Line::opt`], but prints nothing when the value is absent.
255    #[must_use]
256    pub fn opt_present(self, key: &str, value: Option<&str>) -> Self {
257        if value.is_some() { self.opt(key, value) } else { self }
258    }
259
260    #[must_use]
261    pub fn flag_present(self, key: &str, value: Option<bool>) -> Self {
262        if value.is_some() { self.flag(key, value) } else { self }
263    }
264
265    #[must_use]
266    pub fn list_present(self, key: &str, values: &[String]) -> Self {
267        if values.is_empty() { self } else { self.list(key, values) }
268    }
269
270    pub fn end(self) -> String {
271        self.0
272    }
273}