diagnostics_hierarchy/serialization/
deserialize.rs

1// Copyright 2020 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use crate::{ArrayContent, DiagnosticsHierarchy, ExponentialHistogram, LinearHistogram, Property};
6use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
7use base64::engine::Engine as _;
8use serde::de::{self, MapAccess, SeqAccess, Visitor};
9use serde::{Deserialize, Deserializer};
10use std::collections::HashMap;
11use std::fmt;
12use std::hash::Hash;
13use std::marker::PhantomData;
14use std::str::FromStr;
15
16#[cfg(feature = "json_schema")]
17use schemars::schema::{
18    InstanceType, Metadata, ObjectValidation, Schema, SchemaObject, SingleOrVec,
19    SubschemaValidation,
20};
21
22struct RootVisitor<Key> {
23    // Key is unused.
24    marker: PhantomData<Key>,
25}
26
27impl<'de, Key> Visitor<'de> for RootVisitor<Key>
28where
29    Key: FromStr + Clone + Hash + Eq + AsRef<str>,
30{
31    type Value = DiagnosticsHierarchy<Key>;
32
33    fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
34        formatter.write_str("there should be a single root")
35    }
36
37    fn visit_map<V>(self, mut map: V) -> Result<DiagnosticsHierarchy<Key>, V::Error>
38    where
39        V: MapAccess<'de>,
40    {
41        let result = match map.next_entry::<String, FieldValue<Key>>()? {
42            Some((map_key, value)) => {
43                let key = Key::from_str(&map_key)
44                    .map_err(|_| de::Error::custom("failed to parse key"))?;
45                value.into_node(&key)
46            }
47            None => return Err(de::Error::invalid_length(0, &"expected a root node")),
48        };
49
50        let mut found = 1;
51        while map.next_key::<String>()?.is_some() {
52            found += 1;
53        }
54
55        if found > 1 {
56            return Err(de::Error::invalid_length(found, &"expected a single root"));
57        }
58
59        result.ok_or_else(|| de::Error::custom("expected node for root"))
60    }
61}
62
63impl<'de, Key> Deserialize<'de> for DiagnosticsHierarchy<Key>
64where
65    Key: FromStr + Clone + Hash + Eq + AsRef<str>,
66{
67    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
68    where
69        D: Deserializer<'de>,
70    {
71        deserializer.deserialize_map(RootVisitor { marker: PhantomData })
72    }
73}
74
75trait IntoProperty<Key> {
76    fn into_property(self, key: &Key) -> Option<Property<Key>>;
77}
78
79/// The value of an inspect tree field (node or property).
80enum FieldValue<Key> {
81    String(String),
82    Bytes(Vec<u8>),
83    Int(i64),
84    Uint(u64),
85    Double(f64),
86    Bool(bool),
87    Array(Vec<NumericValue>),
88    LinearIntHistogram(LinearHistogram<i64>),
89    LinearUintHistogram(LinearHistogram<u64>),
90    LinearDoubleHistogram(LinearHistogram<f64>),
91    ExponentialIntHistogram(ExponentialHistogram<i64>),
92    ExponentialUintHistogram(ExponentialHistogram<u64>),
93    ExponentialDoubleHistogram(ExponentialHistogram<f64>),
94    Node(HashMap<Key, FieldValue<Key>>),
95    StringList(Vec<String>),
96}
97
98impl<Key: Clone> IntoProperty<Key> for FieldValue<Key> {
99    fn into_property(self, key: &Key) -> Option<Property<Key>> {
100        match self {
101            Self::String(value) => Some(Property::String(key.clone(), value)),
102            Self::Bytes(value) => Some(Property::Bytes(key.clone(), value)),
103            Self::Int(value) => Some(Property::Int(key.clone(), value)),
104            Self::Uint(value) => Some(Property::Uint(key.clone(), value)),
105            Self::Double(value) => Some(Property::Double(key.clone(), value)),
106            Self::Bool(value) => Some(Property::Bool(key.clone(), value)),
107            Self::Array(values) => values.into_property(key),
108            Self::ExponentialIntHistogram(histogram) => {
109                Some(Property::IntArray(key.clone(), ArrayContent::ExponentialHistogram(histogram)))
110            }
111            Self::ExponentialUintHistogram(histogram) => Some(Property::UintArray(
112                key.clone(),
113                ArrayContent::ExponentialHistogram(histogram),
114            )),
115            Self::ExponentialDoubleHistogram(histogram) => Some(Property::DoubleArray(
116                key.clone(),
117                ArrayContent::ExponentialHistogram(histogram),
118            )),
119            Self::LinearIntHistogram(histogram) => {
120                Some(Property::IntArray(key.clone(), ArrayContent::LinearHistogram(histogram)))
121            }
122            Self::LinearUintHistogram(histogram) => {
123                Some(Property::UintArray(key.clone(), ArrayContent::LinearHistogram(histogram)))
124            }
125            Self::LinearDoubleHistogram(histogram) => {
126                Some(Property::DoubleArray(key.clone(), ArrayContent::LinearHistogram(histogram)))
127            }
128            Self::StringList(list) => Some(Property::StringList(key.clone(), list)),
129            Self::Node(_) => None,
130        }
131    }
132}
133
134impl<Key: AsRef<str> + Clone> FieldValue<Key> {
135    fn is_property(&self) -> bool {
136        !matches!(self, Self::Node(_))
137    }
138
139    fn into_node(self, key: &Key) -> Option<DiagnosticsHierarchy<Key>> {
140        match self {
141            Self::Node(map) => {
142                let mut properties = vec![];
143                let mut children = vec![];
144                for (map_key, value) in map {
145                    if value.is_property() {
146                        properties.push(value.into_property(&map_key).unwrap());
147                    } else {
148                        children.push(value.into_node(&map_key).unwrap());
149                    }
150                }
151                Some(DiagnosticsHierarchy::new(key.as_ref(), properties, children))
152            }
153            _ => None,
154        }
155    }
156}
157
158impl<'de, Key> Deserialize<'de> for FieldValue<Key>
159where
160    Key: FromStr + Hash + Eq,
161{
162    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
163    where
164        D: Deserializer<'de>,
165    {
166        deserializer.deserialize_any(FieldVisitor { marker: PhantomData })
167    }
168}
169
170struct FieldVisitor<Key> {
171    marker: PhantomData<Key>,
172}
173
174// Histogram type may be: linear or exponential; double, int, or uint.
175// If it's linear, step will be Some and initial_step and step_multiplier will be None.
176// if it's exponential, step will be None and initial_step and step_multiplier will be Some.
177// In all cases, size must be usize (u64).
178// indexes will be either None or Some(Vec<NumericValue>) which must be convertible to Vec<usize>.
179// counts will be Vec<NumericValue>.
180// If the histogram is double, every number must be f64 and counts must convert to Vec<f64>.
181// If it's not double, the numbers will be some mix of i64 and u64. If even one can't fit in
182// an i64, then the histogram must be represented as u64 and none of the numbers can be
183// negative.
184// The deserializer will have used i64 for any number that fits, and we can assume most
185// histograms will fit in i64, so first we check if all numbers are i64 and counts can convert
186// to Vec<i64>. If not, then we have to check if a mix of i64 and u64 can all be u64.
187
188fn value_as_u64<Key>(value: &FieldValue<Key>) -> Option<u64> {
189    match value {
190        FieldValue::Uint(value) => Some(*value),
191        FieldValue::Int(value) => u64::try_from(*value).ok(),
192        _ => None,
193    }
194}
195
196fn value_as_i64<Key>(value: &FieldValue<Key>) -> Option<i64> {
197    match value {
198        FieldValue::Int(value) => Some(*value),
199        FieldValue::Uint(value) => i64::try_from(*value).ok(),
200        _ => None,
201    }
202}
203
204// Try to convert indexes to Option<Vec<usize>> and declared_len to usize, and check consistency of
205// several sizes. Return Err(()) if anything goes wrong, and the converted (indexes, declared_len)
206// if it's all good,
207fn sanitize_histogram_parameters<Key>(
208    indexes: Option<&FieldValue<Key>>,
209    n_parameters: usize,
210    dict_len: usize,
211    counts: &FieldValue<Key>,
212    size: &FieldValue<Key>,
213) -> Result<(Option<Vec<usize>>, usize), ()> {
214    let size = match size {
215        FieldValue::Uint(size) => *size as usize,
216        FieldValue::Int(size) if *size >= 0 => *size as usize,
217        _ => return Err(()),
218    };
219    // An empty array will be cast as a StringList. A condensed histogram of all-zeroes will thus
220    // have empty StringList counts and indexes. If we sanitize successfully, we'll return an
221    // empty Vec<usize> for indexes, and we will have verified that counts is empty too.
222    let counts_len = match counts {
223        FieldValue::Array(counts) => counts.len(),
224        FieldValue::StringList(counts) if counts.is_empty() => 0,
225        _ => return Err(()),
226    };
227    if size < 3 {
228        // We need at least 3 (overflow + 1) buckets to be a valid histogram
229        return Err(());
230    }
231    // It's OK if indexes is None. If it's Some, it must be a Vec<NumericValue> that converts to a
232    // Vec<usize>. Also, check that various lengths are consistent.
233    match indexes {
234        None => {
235            if counts_len != size || dict_len != n_parameters - 1 {
236                return Err(());
237            }
238            Ok((None, size))
239        }
240        Some(FieldValue::StringList(indexes)) if indexes.is_empty() => {
241            if counts_len != 0 || dict_len != n_parameters {
242                return Err(());
243            }
244            Ok((Some(vec![]), size))
245        }
246        Some(FieldValue::Array(indexes)) => {
247            if indexes.len() != counts_len || counts_len > size || dict_len != n_parameters {
248                return Err(());
249            }
250            let indexes = indexes
251                .iter()
252                .map(|value| match value.as_u64() {
253                    None => None,
254                    Some(i) if (i as usize) < size => Some(i as usize),
255                    _ => None,
256                })
257                .collect::<Option<Vec<_>>>();
258            match indexes {
259                None => Err(()),
260                Some(indexes) => Ok((Some(indexes), size)),
261            }
262        }
263        _ => Err(()),
264    }
265}
266
267fn match_linear_histogram<Key>(
268    floor: &FieldValue<Key>,
269    step: &FieldValue<Key>,
270    counts: &FieldValue<Key>,
271    indexes: Option<&FieldValue<Key>>,
272    size: &FieldValue<Key>,
273    dict_len: usize,
274) -> Option<FieldValue<Key>> {
275    let (indexes, size) = match sanitize_histogram_parameters(indexes, 5, dict_len, counts, size) {
276        Ok((indexes, size)) => (indexes, size),
277        Err(()) => return None,
278    };
279    // A double histogram will have all types double. An int or uint histogram may have a mix
280    // of int and uint members.
281    match (floor, step, counts) {
282        (FieldValue::Double(floor), FieldValue::Double(step), counts) => {
283            let counts = parse_f64_list(counts)?;
284            Some(FieldValue::LinearDoubleHistogram(LinearHistogram {
285                floor: *floor,
286                step: *step,
287                counts,
288                indexes,
289                size,
290            }))
291        }
292        (floor, step, counts) => {
293            let counts_i64 = parse_i64_list(counts);
294            if let (Some(counts), Some(floor), Some(step)) =
295                (counts_i64, value_as_i64(floor), value_as_i64(step))
296            {
297                return Some(FieldValue::LinearIntHistogram(LinearHistogram {
298                    floor,
299                    step,
300                    counts,
301                    indexes,
302                    size,
303                }));
304            }
305            // At this point, it's unsigned, or nothing.
306            if let (Some(counts), Some(floor), Some(step)) =
307                (parse_u64_list(counts), value_as_u64(floor), value_as_u64(step))
308            {
309                return Some(FieldValue::LinearUintHistogram(LinearHistogram {
310                    floor,
311                    step,
312                    counts,
313                    indexes,
314                    size,
315                }));
316            }
317            None
318        }
319    }
320}
321
322fn match_exponential_histogram<Key>(
323    floor: &FieldValue<Key>,
324    initial_step: &FieldValue<Key>,
325    step_multiplier: &FieldValue<Key>,
326    counts: &FieldValue<Key>,
327    indexes: Option<&FieldValue<Key>>,
328    size: &FieldValue<Key>,
329    dict_len: usize,
330) -> Option<FieldValue<Key>> {
331    let (indexes, size) = match sanitize_histogram_parameters(indexes, 6, dict_len, counts, size) {
332        Ok((indexes, size)) => (indexes, size),
333        Err(()) => return None,
334    };
335    match (floor, initial_step, step_multiplier, counts) {
336        (
337            FieldValue::Double(floor),
338            FieldValue::Double(initial_step),
339            FieldValue::Double(step_multiplier),
340            counts,
341        ) => {
342            let counts = parse_f64_list(counts)?;
343            Some(FieldValue::ExponentialDoubleHistogram(ExponentialHistogram {
344                floor: *floor,
345                initial_step: *initial_step,
346                step_multiplier: *step_multiplier,
347                counts,
348                indexes,
349                size,
350            }))
351        }
352        (floor, initial_step, step_multiplier, counts) => {
353            let counts_i64 = parse_i64_list(counts);
354            if let (Some(counts), Some(floor), Some(initial_step), Some(step_multiplier)) = (
355                counts_i64,
356                value_as_i64(floor),
357                value_as_i64(initial_step),
358                value_as_i64(step_multiplier),
359            ) {
360                return Some(FieldValue::ExponentialIntHistogram(ExponentialHistogram {
361                    floor,
362                    initial_step,
363                    step_multiplier,
364                    counts,
365                    indexes,
366                    size,
367                }));
368            }
369            // At this point, it's unsigned, or nothing.
370            if let (Some(counts), Some(floor), Some(initial_step), Some(step_multiplier)) = (
371                parse_u64_list(counts),
372                value_as_u64(floor),
373                value_as_u64(initial_step),
374                value_as_u64(step_multiplier),
375            ) {
376                return Some(FieldValue::ExponentialUintHistogram(ExponentialHistogram {
377                    floor,
378                    initial_step,
379                    step_multiplier,
380                    counts,
381                    indexes,
382                    size,
383                }));
384            }
385            None
386        }
387    }
388}
389
390fn match_histogram<Key>(dict: &HashMap<Key, FieldValue<Key>>) -> Option<FieldValue<Key>>
391where
392    Key: FromStr + Hash + Eq,
393{
394    // Quick checks for efficiency - most maps won't be histograms.
395    let dict_len = dict.len();
396    if !(4..=6).contains(&dict_len) {
397        return None;
398    }
399    let floor = dict.get(&Key::from_str("floor").ok().unwrap())?;
400    let step = dict.get(&Key::from_str("step").ok().unwrap());
401    let initial_step = dict.get(&Key::from_str("initial_step").ok().unwrap());
402    let step_multiplier = dict.get(&Key::from_str("step_multiplier").ok().unwrap());
403    let counts = dict.get(&Key::from_str("counts").ok().unwrap());
404    let indexes = dict.get(&Key::from_str("indexes").ok().unwrap());
405    let size = dict.get(&Key::from_str("size").ok().unwrap());
406    // Indexes may be None if the histogram isn't condensed.
407    match (step, initial_step, step_multiplier, counts, indexes, size) {
408        (Some(step), None, None, Some(counts), indexes, Some(size)) => {
409            match_linear_histogram(floor, step, counts, indexes, size, dict_len)
410        }
411        (None, Some(initial_step), Some(step_multiplier), Some(counts), indexes, Some(size)) => {
412            match_exponential_histogram(
413                floor,
414                initial_step,
415                step_multiplier,
416                counts,
417                indexes,
418                size,
419                dict_len,
420            )
421        }
422        _ => None,
423    }
424}
425
426impl<'de, Key> Visitor<'de> for FieldVisitor<Key>
427where
428    Key: FromStr + Hash + Eq,
429{
430    type Value = FieldValue<Key>;
431
432    fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
433        formatter.write_str("failed to field")
434    }
435
436    fn visit_map<V>(self, mut map: V) -> Result<Self::Value, V::Error>
437    where
438        V: MapAccess<'de>,
439    {
440        let mut entries = vec![];
441        while let Some(entry) = map.next_entry::<String, FieldValue<Key>>()? {
442            entries.push(entry);
443        }
444
445        let node = entries
446            .into_iter()
447            .map(|(key, value)| Key::from_str(&key).map(|key| (key, value)))
448            .collect::<Result<HashMap<Key, FieldValue<Key>>, _>>()
449            .map_err(|_| de::Error::custom("failed to parse key"))?;
450        if let Some(histogram) = match_histogram(&node) {
451            Ok(histogram)
452        } else {
453            Ok(FieldValue::Node(node))
454        }
455    }
456
457    fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E> {
458        Ok(FieldValue::Int(value))
459    }
460
461    fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E> {
462        Ok(FieldValue::Uint(value))
463    }
464
465    fn visit_f64<E>(self, value: f64) -> Result<Self::Value, E> {
466        Ok(FieldValue::Double(value))
467    }
468
469    fn visit_bool<E>(self, value: bool) -> Result<Self::Value, E> {
470        Ok(FieldValue::Bool(value))
471    }
472
473    fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
474    where
475        E: de::Error,
476    {
477        if value.starts_with("b64:") {
478            let bytes64 = value.replace("b64:", "");
479            let bytes = BASE64_STANDARD
480                .decode(&bytes64)
481                .map_err(|_| de::Error::custom("failed to decode bytes"))?;
482            return Ok(FieldValue::Bytes(bytes));
483        }
484        Ok(FieldValue::String(value.to_string()))
485    }
486
487    fn visit_seq<S>(self, mut seq: S) -> Result<Self::Value, S::Error>
488    where
489        S: SeqAccess<'de>,
490    {
491        let mut result = vec![];
492        while let Some(elem) = seq.next_element::<SeqItem>()? {
493            result.push(elem);
494        }
495        // There can be two types of sequences: regular arrays (containing numeric values),
496        // and string lists (containing only strings). There cannot be a
497        // sequence containing a mix of them.
498        let mut array = vec![];
499        let mut strings = vec![];
500        for item in result {
501            match item {
502                SeqItem::Value(x) => array.push(x),
503                SeqItem::StringValue(x) => strings.push(x),
504            }
505        }
506
507        match (!array.is_empty(), !strings.is_empty()) {
508            (true, false) => Ok(FieldValue::Array(array)),
509            (false, _) => {
510                // Numeric arrays cannot be empty, but string lists can.
511                // Histograms can contain empty arrays of numbers, but we'll check for empty
512                // StringList in the histogram-matching code, and know what it means.
513                Ok(FieldValue::StringList(strings))
514            }
515            _ => Err(de::Error::custom("unexpected sequence containing mixed values")),
516        }
517    }
518}
519
520#[derive(Deserialize)]
521#[serde(untagged)]
522enum SeqItem {
523    Value(NumericValue),
524    StringValue(String),
525}
526
527enum NumericValue {
528    Positive(u64),
529    Negative(i64),
530    Double(f64),
531}
532
533impl NumericValue {
534    #[inline]
535    fn as_i64(&self) -> Option<i64> {
536        match self {
537            Self::Positive(x) if *x <= i64::MAX as u64 => Some(*x as i64),
538            Self::Negative(x) => Some(*x),
539            _ => None,
540        }
541    }
542
543    #[inline]
544    fn as_u64(&self) -> Option<u64> {
545        match self {
546            Self::Positive(x) => Some(*x),
547            _ => None,
548        }
549    }
550
551    #[inline]
552    fn as_f64(&self) -> Option<f64> {
553        match self {
554            Self::Double(x) => Some(*x),
555            _ => None,
556        }
557    }
558}
559
560impl<'de> Deserialize<'de> for NumericValue {
561    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
562    where
563        D: Deserializer<'de>,
564    {
565        deserializer.deserialize_any(NumericValueVisitor)
566    }
567}
568
569impl<Key: Clone> IntoProperty<Key> for Vec<NumericValue> {
570    fn into_property(self, key: &Key) -> Option<Property<Key>> {
571        if let Some(values) = parse_f64_vec(&self) {
572            return Some(Property::DoubleArray(key.clone(), ArrayContent::Values(values)));
573        }
574        if let Some(values) = parse_i64_vec(&self) {
575            return Some(Property::IntArray(key.clone(), ArrayContent::Values(values)));
576        }
577        if let Some(values) = parse_u64_vec(&self) {
578            return Some(Property::UintArray(key.clone(), ArrayContent::Values(values)));
579        }
580        None
581    }
582}
583
584macro_rules! parse_numeric_vec_impls {
585    ($($type:ty),*) => {
586        $(
587            paste::paste! {
588                fn [<parse_ $type _vec>](vec: &[NumericValue]) -> Option<Vec<$type>> {
589                    vec.iter().map(|value| value.[<as_ $type>]()).collect::<Option<Vec<_>>>()
590                }
591
592                // Histograms can contain empty lists for counts and indexes. These will be
593                // deserialized as empty StringLists and should parse to empty vecs
594                // of any numeric type.
595                fn [<parse_ $type _list>]<Key>(list: &FieldValue<Key>) -> Option<Vec<$type>> {
596                    match list {
597                        FieldValue::StringList(list) if list.len() == 0 => Some(vec![]),
598                        FieldValue::Array(vec) => [<parse_ $type _vec>](vec),
599                        _ => None,
600                    }
601                }
602            }
603        )*
604    };
605}
606
607// Generates the following functions:
608// fn parse_f64_vec()
609// fn parse_u64_vec()
610// fn parse_i64_vec()
611// fn parse_f64_list()
612// fn parse_u64_list()
613// fn parse_i64_list()
614parse_numeric_vec_impls!(f64, u64, i64);
615
616struct NumericValueVisitor;
617
618impl Visitor<'_> for NumericValueVisitor {
619    type Value = NumericValue;
620
621    fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
622        formatter.write_str("failed to deserialize bucket array")
623    }
624
625    fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E> {
626        if value < 0 {
627            return Ok(NumericValue::Negative(value));
628        }
629        Ok(NumericValue::Positive(value as u64))
630    }
631
632    fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E> {
633        Ok(NumericValue::Positive(value))
634    }
635
636    fn visit_f64<E>(self, value: f64) -> Result<Self::Value, E> {
637        Ok(NumericValue::Double(value))
638    }
639}
640
641// Due to the custom encoding/decoding in use, the json schema does not adhere to the structure of
642// the rust definition, so must be implemented manually here.
643#[cfg(feature = "json_schema")]
644impl<T> schemars::JsonSchema for DiagnosticsHierarchy<T> {
645    fn schema_name() -> String {
646        "DiagnosticsHierarchy".to_owned()
647    }
648
649    fn json_schema(generator: &mut schemars::gen::SchemaGenerator) -> Schema {
650        let property_schema = SchemaObject {
651            metadata: Some(Box::new(Metadata {
652                description: Some(
653                    "A property, which can be any standard object, or an array, or a histogram"
654                        .to_owned(),
655                ),
656                ..Default::default()
657            })),
658            subschemas: Some(Box::new(SubschemaValidation {
659                any_of: Some(vec![
660                    String::json_schema(generator),
661                    Vec::<f64>::json_schema(generator),
662                    Vec::<i64>::json_schema(generator),
663                    Vec::<u64>::json_schema(generator),
664                    Vec::<String>::json_schema(generator),
665                    LinearHistogram::<u64>::json_schema(generator),
666                    LinearHistogram::<i64>::json_schema(generator),
667                    LinearHistogram::<f64>::json_schema(generator),
668                    ExponentialHistogram::<u64>::json_schema(generator),
669                    ExponentialHistogram::<i64>::json_schema(generator),
670                    ExponentialHistogram::<f64>::json_schema(generator),
671                    i64::json_schema(generator),
672                    u64::json_schema(generator),
673                    f64::json_schema(generator),
674                    bool::json_schema(generator),
675                    // Includes a recursive self-reference.
676                    SchemaObject {
677                        instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Object))),
678                        reference: Some(format!("#/definitions/{}", Self::schema_name())),
679                        ..Default::default()
680                    }
681                    .into(),
682                ]),
683                ..Default::default()
684            })),
685            ..Default::default()
686        };
687        SchemaObject {
688            instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Object))),
689            object: Some(Box::new(ObjectValidation {
690                min_properties: Some(1),
691                pattern_properties: [("^.*$".to_owned(), property_schema.into())]
692                    .into_iter()
693                    .collect(),
694                ..Default::default()
695            })),
696            ..Default::default()
697        }
698        .into()
699    }
700}
701
702#[cfg(test)]
703mod tests {
704    use super::*;
705    use crate::ArrayFormat;
706
707    #[cfg(feature = "json_schema")]
708    use ffx_writer::VerifiedMachineWriter;
709
710    #[fuchsia::test]
711    fn deserialize_json() {
712        let json_string = get_single_json_hierarchy();
713        let mut parsed_hierarchy: DiagnosticsHierarchy =
714            serde_json::from_str(&json_string).expect("deserialized");
715        let mut expected_hierarchy = get_unambigious_deserializable_hierarchy();
716        parsed_hierarchy.sort();
717        expected_hierarchy.sort();
718        assert_eq!(expected_hierarchy, parsed_hierarchy);
719    }
720
721    #[cfg(feature = "json_schema")]
722    #[fuchsia::test]
723    fn verify_schema() {
724        let json_string = get_single_json_hierarchy();
725        let data: serde_json::Value =
726            serde_json::from_str(&json_string).expect("valid json string");
727        let root = data.get("root").expect("expected root node");
728        if let Err(e) = VerifiedMachineWriter::<DiagnosticsHierarchy>::verify_schema(root) {
729            panic!("Error verifying schema of `{data:#?}`: {e:#?}")
730        }
731    }
732
733    #[fuchsia::test]
734    fn reversible_deserialize() {
735        let mut original_hierarchy = get_unambigious_deserializable_hierarchy();
736        let result =
737            serde_json::to_string(&original_hierarchy).expect("failed to format hierarchy");
738        let mut parsed_hierarchy: DiagnosticsHierarchy =
739            serde_json::from_str(&result).expect("deserialized");
740        parsed_hierarchy.sort();
741        original_hierarchy.sort();
742        assert_eq!(original_hierarchy, parsed_hierarchy);
743    }
744
745    #[fuchsia::test]
746    fn test_exp_histogram() {
747        let mut hierarchy = DiagnosticsHierarchy::new(
748            "root".to_string(),
749            vec![Property::IntArray(
750                "histogram".to_string(),
751                ArrayContent::new(
752                    vec![1000, 1000, 2, 1, 2, 3, 4, 5, 6],
753                    ArrayFormat::ExponentialHistogram,
754                )
755                .unwrap(),
756            )],
757            vec![],
758        );
759        let expected_json = serde_json::json!({
760            "root": {
761                "histogram": {
762                    "floor": 1000,
763                    "initial_step": 1000,
764                    "step_multiplier": 2,
765                    "counts": [1, 2, 3, 4, 5, 6],
766                    "size": 6
767                }
768            }
769        });
770        let result_json = serde_json::json!(hierarchy);
771        assert_eq!(result_json, expected_json);
772        let mut parsed_hierarchy: DiagnosticsHierarchy =
773            serde_json::from_value(result_json).expect("deserialized");
774        parsed_hierarchy.sort();
775        hierarchy.sort();
776        assert_eq!(hierarchy, parsed_hierarchy);
777    }
778
779    // Creates a hierarchy that isn't lossy due to its unambigious values.
780    fn get_unambigious_deserializable_hierarchy() -> DiagnosticsHierarchy {
781        DiagnosticsHierarchy::new(
782            "root",
783            vec![
784                Property::UintArray(
785                    "array".to_string(),
786                    ArrayContent::Values(vec![0, 2, u64::MAX]),
787                ),
788                Property::Bool("bool_true".to_string(), true),
789                Property::Bool("bool_false".to_string(), false),
790                Property::StringList(
791                    "string_list".to_string(),
792                    vec!["foo".to_string(), "bar".to_string()],
793                ),
794                Property::StringList("empty_string_list".to_string(), vec![]),
795            ],
796            vec![
797                DiagnosticsHierarchy::new(
798                    "a",
799                    vec![
800                        Property::Double("double".to_string(), 2.5),
801                        Property::DoubleArray(
802                            "histogram".to_string(),
803                            ArrayContent::new(
804                                vec![0.0, 2.0, 4.0, 1.0, 3.0, 4.0, 7.0],
805                                ArrayFormat::ExponentialHistogram,
806                            )
807                            .unwrap(),
808                        ),
809                    ],
810                    vec![],
811                ),
812                DiagnosticsHierarchy::new(
813                    "b",
814                    vec![
815                        Property::Int("int".to_string(), -2),
816                        Property::String("string".to_string(), "some value".to_string()),
817                        Property::IntArray(
818                            "histogram".to_string(),
819                            ArrayContent::new(vec![0, 2, 4, 1, 3], ArrayFormat::LinearHistogram)
820                                .unwrap(),
821                        ),
822                    ],
823                    vec![],
824                ),
825            ],
826        )
827    }
828
829    pub fn get_single_json_hierarchy() -> String {
830        "{ \"root\": {
831                \"a\": {
832                    \"double\": 2.5,
833                    \"histogram\": {
834                        \"floor\": 0.0,
835                        \"initial_step\": 2.0,
836                        \"step_multiplier\": 4.0,
837                        \"counts\": [1.0, 3.0, 4.0, 7.0],
838                        \"size\": 4
839                    }
840                },
841                \"array\": [
842                    0,
843                    2,
844                    18446744073709551615
845                ],
846                \"string_list\": [\"foo\", \"bar\"],
847                \"empty_string_list\": [],
848                \"b\": {
849                    \"histogram\": {
850                        \"floor\": 0,
851                        \"step\": 2,
852                        \"counts\": [4, 1, 3],
853                        \"size\": 3
854                    },
855                    \"int\": -2,
856                    \"string\": \"some value\"
857                },
858                \"bool_false\": false,
859                \"bool_true\": true
860            }}"
861        .to_string()
862    }
863}