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