use std::borrow::Cow;
use std::fmt;
use serde_json::{Map, Value};
use crate::HapError;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Note {
pub path: String,
pub message: String,
}
impl fmt::Display for Note {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}: {}", self.path, self.message)
}
}
#[derive(Debug, Default)]
pub(crate) struct Findings {
pub notes: Vec<Note>,
pub unmodelled: Vec<String>,
}
pub(crate) fn parse_object(bytes: &[u8]) -> Result<Map<String, Value>, HapError> {
let bytes = bytes.strip_prefix(b"\xef\xbb\xbf").unwrap_or(bytes);
match serde_json::from_slice(bytes)? {
Value::Object(map) => Ok(map),
other => Err(HapError::NotAnObject { found: kind(&other) }),
}
}
pub(crate) fn pretty(raw: &Value) -> String {
let mut text = serde_json::to_string_pretty(raw).expect("a serde_json::Value always serializes");
text.push('\n');
text
}
pub(crate) fn kind(value: &Value) -> &'static str {
match value {
Value::Null => "null",
Value::Bool(_) => "boolean",
Value::Number(_) => "number",
Value::String(_) => "string",
Value::Array(_) => "array",
Value::Object(_) => "object",
}
}
pub(crate) struct Obj<'v, 'f> {
map: &'v Map<String, Value>,
path: String,
findings: &'f mut Findings,
read: Vec<&'static str>,
}
impl<'v, 'f> Obj<'v, 'f> {
pub fn root(map: &'v Map<String, Value>, findings: &'f mut Findings) -> Self {
Obj { map, path: String::new(), findings, read: Vec::new() }
}
pub fn to_value(&self) -> Value {
Value::Object(self.map.clone())
}
fn child_path(&self, key: &str) -> String {
if self.path.is_empty() { key.to_owned() } else { format!("{}.{key}", self.path) }
}
fn get(&mut self, key: &'static str) -> Option<&'v Value> {
self.read.push(key);
self.map.get(key).filter(|v| !v.is_null())
}
fn mismatch(&mut self, path: String, expected: &str, found: &Value) {
self.findings.notes.push(Note { path, message: format!("expected {expected}, found {}", kind(found)) });
}
fn typed<T>(&mut self, key: &'static str, expected: &str, pick: impl FnOnce(&'v Value) -> Option<T>) -> Option<T> {
let value = self.get(key)?;
let picked = pick(value);
if picked.is_none() {
self.mismatch(self.child_path(key), expected, value);
}
picked
}
pub fn str(&mut self, key: &'static str) -> Option<String> {
self.typed(key, "string", |v| v.as_str().map(str::to_owned))
}
pub fn text(&mut self, key: &'static str) -> Option<String> {
self.typed(key, "string, number or boolean", |v| match v {
Value::String(s) => Some(s.clone()),
Value::Number(n) => Some(n.to_string()),
Value::Bool(b) => Some(b.to_string()),
Value::Null | Value::Array(_) | Value::Object(_) => None,
})
}
pub fn bool(&mut self, key: &'static str) -> Option<bool> {
self.typed(key, "boolean", Value::as_bool)
}
pub fn int(&mut self, key: &'static str) -> Option<i64> {
self.typed(key, "integer", Value::as_i64)
}
pub fn strings(&mut self, key: &'static str) -> Vec<String> {
let Some(items) = self.typed(key, "array", Value::as_array) else {
return Vec::new();
};
let mut out = Vec::with_capacity(items.len());
for (i, item) in items.iter().enumerate() {
match item.as_str() {
Some(s) => out.push(s.to_owned()),
None => self.mismatch(format!("{}[{i}]", self.child_path(key)), "string", item),
}
}
out
}
pub fn object<T>(&mut self, key: &'static str, read: impl FnOnce(&mut Obj<'v, '_>) -> T) -> Option<T> {
let map = self.typed(key, "object", Value::as_object)?;
Some(self.descend(map, self.child_path(key), read))
}
pub fn objects<T>(&mut self, key: &'static str, mut read: impl FnMut(&mut Obj<'v, '_>) -> T) -> Vec<T> {
let Some(items) = self.typed(key, "array", Value::as_array) else {
return Vec::new();
};
let mut out = Vec::with_capacity(items.len());
for (i, item) in items.iter().enumerate() {
let path = format!("{}[{i}]", self.child_path(key));
match item.as_object() {
Some(map) => out.push(self.descend(map, path, &mut read)),
None => self.mismatch(path, "object", item),
}
}
out
}
fn descend<T>(&mut self, map: &'v Map<String, Value>, path: String, read: impl FnOnce(&mut Obj<'v, '_>) -> T) -> T {
let mut child = Obj { map, path, findings: self.findings, read: Vec::new() };
let value = read(&mut child);
child.finish();
value
}
pub fn ignore(&mut self, keys: &[&'static str]) {
self.read.extend_from_slice(keys);
}
pub fn finish(self) {
for key in self.map.keys() {
let key = key.as_str();
let known = |k: &str| self.read.contains(&k);
if known(key) || key.strip_suffix("Id").is_some_and(known) {
continue;
}
self.findings.unmodelled.push(self.child_path(key));
}
}
}
pub(crate) fn token(text: &str) -> Cow<'_, str> {
if !text.is_empty() && !text.chars().any(|c| c.is_whitespace() || c.is_control() || c == '"') {
return Cow::Borrowed(text);
}
Cow::Owned(Value::from(text).to_string())
}
pub(crate) struct Line(String);
impl Line {
pub fn new(record: &str) -> Self {
Line(record.to_owned())
}
#[must_use]
pub fn word(mut self, text: &str) -> Self {
self.0.push(' ');
self.0.push_str(&token(text));
self
}
#[must_use]
pub fn raw(mut self, key: &str, value: impl fmt::Display) -> Self {
self.0.push_str(&format!(" {key}={value}"));
self
}
#[must_use]
pub fn opt(self, key: &str, value: Option<&str>) -> Self {
match value {
Some(v) => {
let v = token(v);
self.raw(key, v)
}
None => self.raw(key, "-"),
}
}
#[must_use]
pub fn flag(self, key: &str, value: Option<bool>) -> Self {
self.num(key, value)
}
#[must_use]
pub fn num<T: fmt::Display>(self, key: &str, value: Option<T>) -> Self {
match value {
Some(v) => self.raw(key, v),
None => self.raw(key, "-"),
}
}
#[must_use]
pub fn list(self, key: &str, values: &[String]) -> Self {
if values.is_empty() {
return self.raw(key, "-");
}
let joined = values.iter().map(|v| token(v)).collect::<Vec<_>>().join(",");
self.raw(key, joined)
}
#[must_use]
pub fn opt_present(self, key: &str, value: Option<&str>) -> Self {
if value.is_some() { self.opt(key, value) } else { self }
}
#[must_use]
pub fn flag_present(self, key: &str, value: Option<bool>) -> Self {
if value.is_some() { self.flag(key, value) } else { self }
}
#[must_use]
pub fn list_present(self, key: &str, values: &[String]) -> Self {
if values.is_empty() { self } else { self.list(key, values) }
}
pub fn end(self) -> String {
self.0
}
}