1use std::{
2 collections::{BTreeMap, HashMap},
3 fmt::Write,
4};
5
6use crate::{
7 types::{Ref, Variant},
8 WeakDom,
9};
10use serde::{Deserialize, Serialize};
11use ustr::Ustr;
12
13pub struct DomViewer {
20 referent_to_id: HashMap<Ref, String>,
21 next_id: usize,
22}
23
24impl DomViewer {
25 pub fn new() -> Self {
27 Self {
28 referent_to_id: HashMap::new(),
29 next_id: 0,
30 }
31 }
32
33 pub fn view(&mut self, dom: &WeakDom) -> ViewedInstance {
36 let root_referent = dom.root_ref();
37 self.populate_referent_map(dom, root_referent);
38 self.view_instance(dom, root_referent)
39 }
40
41 pub fn view_children(&mut self, dom: &WeakDom) -> Vec<ViewedInstance> {
44 let root_instance = dom.root();
45 let children = root_instance.children();
46
47 for &referent in children {
48 self.populate_referent_map(dom, referent);
49 }
50
51 children
52 .iter()
53 .map(|&referent| self.view_instance(dom, referent))
54 .collect()
55 }
56
57 fn populate_referent_map(&mut self, dom: &WeakDom, referent: Ref) {
58 let next_id = &mut self.next_id;
59 self.referent_to_id.entry(referent).or_insert_with(|| {
60 let name = format!("referent-{next_id}");
61 *next_id += 1;
62 name
63 });
64
65 let instance = dom.get_by_ref(referent).unwrap();
66 for referent in instance.children() {
67 self.populate_referent_map(dom, *referent);
68 }
69 }
70
71 fn view_instance(&self, dom: &WeakDom, referent: Ref) -> ViewedInstance {
72 let instance = dom.get_by_ref(referent).unwrap();
73
74 let children = instance
75 .children()
76 .iter()
77 .copied()
78 .map(|referent| self.view_instance(dom, referent))
79 .collect();
80
81 let properties = instance
82 .properties
83 .iter()
84 .map(|(key, value)| {
85 let new_value = match value {
86 Variant::Ref(referent) => {
87 if referent.is_some() {
88 let referent_str = self
89 .referent_to_id
90 .get(referent)
91 .cloned()
92 .unwrap_or_else(|| "[unknown ID]".to_owned());
93
94 ViewedValue::Ref(referent_str)
95 } else {
96 ViewedValue::Ref("null".to_owned())
97 }
98 }
99 Variant::SharedString(shared_string) => {
100 let hash = shared_string.hash();
101 let mut hash_hex = String::with_capacity(hash.as_bytes().len() * 2);
102
103 for byte in hash.as_bytes() {
104 write!(hash_hex, "{byte:02x}").unwrap();
105 }
106 ViewedValue::SharedString {
107 len: shared_string.data().len(),
108 hash: hash_hex,
109 }
110 }
111 Variant::NetAssetRef(net) => {
112 let hash = net.hash();
113 let mut hash_hex = String::with_capacity(hash.as_bytes().len() * 2);
114
115 for byte in hash.as_bytes() {
116 write!(hash_hex, "{byte:02x}").unwrap();
117 }
118 ViewedValue::NetAssetRef {
119 len: net.data().len(),
120 hash: hash_hex,
121 }
122 }
123 other => ViewedValue::Other(other.clone()),
124 };
125
126 (*key, new_value)
127 })
128 .collect();
129
130 ViewedInstance {
131 referent: self.referent_to_id.get(&referent).unwrap().clone(),
132 name: instance.name.clone(),
133 class: instance.class,
134 properties,
135 children,
136 }
137 }
138}
139
140impl Default for DomViewer {
141 fn default() -> Self {
142 Self::new()
143 }
144}
145
146#[derive(Debug, Clone, Serialize, Deserialize)]
149pub struct ViewedInstance {
150 referent: String,
151 name: String,
152 class: Ustr,
153 properties: BTreeMap<Ustr, ViewedValue>,
154 children: Vec<ViewedInstance>,
155}
156
157#[derive(Debug, Clone, Serialize, Deserialize)]
160#[serde(untagged)]
161enum ViewedValue {
162 Ref(String),
163 SharedString { len: usize, hash: String },
164 NetAssetRef { len: usize, hash: String },
165 Other(Variant),
166}
167
168#[cfg(test)]
169mod test {
170 use super::*;
171
172 use crate::types::SharedString;
173 use crate::InstanceBuilder;
174
175 #[test]
176 fn redact_single() {
177 let dom = WeakDom::new(InstanceBuilder::new("Folder").with_name("Root"));
178
179 insta::assert_yaml_snapshot!(DomViewer::new().view(&dom));
180 }
181
182 #[test]
183 fn redact_multi() {
184 let dom = WeakDom::new(
185 InstanceBuilder::new("Folder")
186 .with_name("Root")
187 .with_children(
188 (0..4).map(|i| InstanceBuilder::new("Folder").with_name(format!("Child {i}"))),
189 ),
190 );
191
192 insta::assert_yaml_snapshot!(DomViewer::new().view(&dom));
193 }
194
195 #[test]
196 fn redact_values() {
197 let root = InstanceBuilder::new("ObjectValue").with_name("Root");
198 let root_ref = root.referent;
199 let root = root.with_property("Value", root_ref);
200
201 let dom = WeakDom::new(root);
202
203 insta::assert_yaml_snapshot!(DomViewer::new().view(&dom));
204 }
205
206 #[test]
207 fn abbreviate_shared_string() {
208 let shared_string = SharedString::new("foo".into());
209
210 let root = InstanceBuilder::new("UnionOperation")
211 .with_name("Root")
212 .with_property("PhysicalConfigData", shared_string);
213
214 let dom = WeakDom::new(root);
215
216 insta::assert_yaml_snapshot!(DomViewer::new().view(&dom));
217 }
218}