first commit
This commit is contained in:
274
examples/normalize-hue-get.rs
Normal file
274
examples/normalize-hue-get.rs
Normal file
@@ -0,0 +1,274 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use clap::Parser;
|
||||
use clap_stdin::FileOrStdin;
|
||||
use json_diff_ng::compare_serde_values;
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{Deserializer, Value};
|
||||
|
||||
use bifrost::error::ApiResult;
|
||||
use hue::api::ResourceRecord;
|
||||
use hue::legacy_api::{
|
||||
ApiConfig, ApiGroup, ApiLight, ApiResourceLink, ApiRule, ApiScene, ApiSchedule, ApiSensor,
|
||||
};
|
||||
|
||||
fn false_positive((a, b): &(&Value, &Value)) -> bool {
|
||||
a.is_number() && b.is_number() && a.as_f64() == b.as_f64()
|
||||
}
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(untagged)]
|
||||
pub enum Input {
|
||||
V1 {
|
||||
config: Value,
|
||||
groups: HashMap<String, Value>,
|
||||
lights: HashMap<String, Value>,
|
||||
resourcelinks: HashMap<String, Value>,
|
||||
rules: HashMap<String, Value>,
|
||||
scenes: HashMap<String, Value>,
|
||||
schedules: HashMap<String, Value>,
|
||||
sensors: HashMap<String, Value>,
|
||||
},
|
||||
V2 {
|
||||
errors: Vec<Value>,
|
||||
data: Vec<Value>,
|
||||
},
|
||||
V2Flat(Value),
|
||||
}
|
||||
|
||||
fn compare(before: &Value, after: &Value, report: bool) -> ApiResult<bool> {
|
||||
let diffs = compare_serde_values(before, after, true, &[]).unwrap();
|
||||
let all_diffs = diffs.all_diffs();
|
||||
|
||||
if !all_diffs
|
||||
.iter()
|
||||
.any(|x| x.1.values.is_none_or(|q| !false_positive(&q)))
|
||||
{
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
/* in report mode, hide diff details */
|
||||
if report {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
log::error!("Difference detected");
|
||||
eprintln!("--------------------------------------------------------------------------------");
|
||||
println!("{}", serde_json::to_string(before)?);
|
||||
eprintln!("--------------------------------------------------------------------------------");
|
||||
println!("{}", serde_json::to_string(after)?);
|
||||
eprintln!("--------------------------------------------------------------------------------");
|
||||
for (d_type, d_path) in all_diffs {
|
||||
if let Some(ref ab) = d_path.values {
|
||||
if false_positive(ab) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
match d_type {
|
||||
json_diff_ng::DiffType::LeftExtra => {
|
||||
eprintln!(" - {d_path}");
|
||||
}
|
||||
json_diff_ng::DiffType::Mismatch => {
|
||||
eprintln!(" * {d_path}");
|
||||
}
|
||||
json_diff_ng::DiffType::RightExtra => {
|
||||
eprintln!(" + {d_path}");
|
||||
}
|
||||
json_diff_ng::DiffType::RootMismatch => {
|
||||
eprintln!("{d_type}: {d_path}");
|
||||
}
|
||||
}
|
||||
}
|
||||
eprintln!();
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
pub struct Normalizer<'a> {
|
||||
name: &'a str,
|
||||
items: usize,
|
||||
errors: usize,
|
||||
width: usize,
|
||||
index: usize,
|
||||
report: bool,
|
||||
}
|
||||
|
||||
impl<'a> Normalizer<'a> {
|
||||
#[must_use]
|
||||
pub const fn new(name: &'a str, width: usize, report: bool) -> Self {
|
||||
Self {
|
||||
name,
|
||||
index: 0,
|
||||
items: 0,
|
||||
errors: 0,
|
||||
width,
|
||||
report,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn error(&mut self) {
|
||||
self.errors += 1;
|
||||
}
|
||||
|
||||
pub fn parse<T>(&mut self, obj: Value) -> ApiResult<T>
|
||||
where
|
||||
T: DeserializeOwned + std::fmt::Debug,
|
||||
{
|
||||
let data: Result<T, _> = serde_json::from_value(obj);
|
||||
Ok(data.inspect_err(|err| {
|
||||
self.errors += 1;
|
||||
log::error!(
|
||||
"{name:width$} | >> Parse error {err} (object index {index})",
|
||||
name = self.name,
|
||||
width = self.width,
|
||||
index = self.index
|
||||
);
|
||||
/* eprintln!("{}", &serde_json::to_string(&before)?); */
|
||||
})?)
|
||||
}
|
||||
|
||||
fn roundtrip<T>(&mut self, item: &Value) -> ApiResult<()>
|
||||
where
|
||||
T: Serialize + DeserializeOwned + std::fmt::Debug,
|
||||
{
|
||||
let value = self.parse::<T>(item.clone())?;
|
||||
self.items += 1;
|
||||
self.index += 1;
|
||||
let after = serde_json::to_value(&value)?;
|
||||
|
||||
if !compare(item, &after, self.report)? {
|
||||
self.errors += 1;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn test<T>(&mut self, item: &Value)
|
||||
where
|
||||
T: Serialize + DeserializeOwned + std::fmt::Debug,
|
||||
{
|
||||
let _ = self.roundtrip::<T>(item);
|
||||
}
|
||||
|
||||
pub fn summary(&self) {
|
||||
let errors = self.errors;
|
||||
let items = self.items;
|
||||
let name = self.name;
|
||||
let width = self.width;
|
||||
if errors > 0 {
|
||||
log::error!("{name:width$} | {items:5} items | {errors:5} errors |");
|
||||
} else {
|
||||
log::info!("{name:width$} | {items:5} items | OK |");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn process_file(file: FileOrStdin, width: usize, report: bool) -> ApiResult<()> {
|
||||
let name = if file.is_stdin() {
|
||||
"<stdin>"
|
||||
} else {
|
||||
&file.filename().to_string()
|
||||
};
|
||||
let stream = Deserializer::from_reader(file.into_reader().unwrap()).into_iter::<Value>();
|
||||
|
||||
let mut nml = Normalizer::new(name, width, report);
|
||||
|
||||
for obj in stream {
|
||||
let obj = obj?;
|
||||
|
||||
let Ok(msg) = nml.parse::<Input>(obj) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
match msg {
|
||||
Input::V1 {
|
||||
config,
|
||||
groups,
|
||||
lights,
|
||||
resourcelinks,
|
||||
rules,
|
||||
scenes,
|
||||
schedules,
|
||||
sensors,
|
||||
} => {
|
||||
/* log::info!("v1 detected"); */
|
||||
nml.test::<ApiConfig>(&config);
|
||||
|
||||
for item in groups.values() {
|
||||
nml.test::<ApiGroup>(item);
|
||||
}
|
||||
for item in lights.values() {
|
||||
nml.test::<ApiLight>(item);
|
||||
}
|
||||
for item in resourcelinks.values() {
|
||||
nml.test::<ApiResourceLink>(item);
|
||||
}
|
||||
for item in rules.values() {
|
||||
nml.test::<ApiRule>(item);
|
||||
}
|
||||
for item in scenes.values() {
|
||||
nml.test::<ApiScene>(item);
|
||||
}
|
||||
for item in schedules.values() {
|
||||
nml.test::<ApiSchedule>(item);
|
||||
}
|
||||
for item in sensors.values() {
|
||||
nml.test::<ApiSensor>(item);
|
||||
}
|
||||
}
|
||||
Input::V2 { data, .. } => {
|
||||
/* log::info!("v2 detected"); */
|
||||
for item in data {
|
||||
nml.test::<ResourceRecord>(&item);
|
||||
}
|
||||
}
|
||||
Input::V2Flat(item) => {
|
||||
/* log::info!("v2flat detected"); */
|
||||
nml.test::<ResourceRecord>(&item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nml.summary();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(about, long_about = None)]
|
||||
struct Args {
|
||||
/// input files
|
||||
#[arg(name = "files")]
|
||||
files: Vec<FileOrStdin>,
|
||||
|
||||
/// show only per-file summary
|
||||
#[arg(short, name = "report", default_value_t = false)]
|
||||
report: bool,
|
||||
}
|
||||
|
||||
impl Args {
|
||||
pub fn longest_filename(&self) -> usize {
|
||||
self.files
|
||||
.iter()
|
||||
.map(|b| b.filename().len().max(5))
|
||||
.max()
|
||||
.unwrap_or(5)
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> ApiResult<()> {
|
||||
pretty_env_logger::formatted_builder()
|
||||
.filter_level(log::LevelFilter::Debug)
|
||||
.parse_default_env()
|
||||
.init();
|
||||
|
||||
let args = Args::parse();
|
||||
let width = args.longest_filename();
|
||||
|
||||
for file in args.files {
|
||||
process_file(file, width, args.report)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user