sheeter_engine/
json_tools.rs1use std::collections::HashSet;
45use std::error::Error;
46use std::path::{Path, PathBuf};
47use unicode_normalization::UnicodeNormalization;
48
49pub fn concat(inputs: &[String], output: &str) -> Result<(), Box<dyn Error>> {
56 let mut map = serde_json::Map::new();
57 let mut used: HashSet<String> = HashSet::new();
58 for input in inputs {
59 let bytes = std::fs::read(input)?;
60 let value: serde_json::Value = serde_json::from_slice(&bytes)?;
61 let stem = Path::new(input)
62 .file_stem()
63 .and_then(|s| s.to_str())
64 .unwrap_or(input)
65 .to_string();
66 let key = unique_key(&stem, &mut used);
67 map.insert(key, value);
68 }
69 let out = serde_json::to_string_pretty(&serde_json::Value::Object(map))?;
70 std::fs::write(output, out)?;
71 Ok(())
72}
73
74pub fn append(
81 target: &str,
82 additions: &[String],
83 output: Option<&str>,
84) -> Result<(), Box<dyn Error>> {
85 let mut root: serde_json::Value = if Path::new(target).exists() {
86 let bytes = std::fs::read(target)?;
87 serde_json::from_slice(&bytes)?
88 } else {
89 serde_json::Value::Object(serde_json::Map::new())
90 };
91 let obj = root.as_object_mut().ok_or("target is not a JSON object")?;
92 for input in additions {
93 let bytes = std::fs::read(input)?;
94 let value: serde_json::Value = serde_json::from_slice(&bytes)?;
95 let stem = Path::new(input)
96 .file_stem()
97 .and_then(|s| s.to_str())
98 .unwrap_or(input)
99 .to_string();
100 obj.insert(stem, value);
101 }
102 let out = serde_json::to_string_pretty(&root)?;
103 std::fs::write(output.unwrap_or(target), out)?;
104 Ok(())
105}
106
107pub fn frag(
114 input: &str,
115 level: usize,
116 out_dir: Option<&str>,
117) -> Result<Vec<PathBuf>, Box<dyn Error>> {
118 let bytes = std::fs::read(input)?;
119 let root: serde_json::Value = serde_json::from_slice(&bytes)?;
120 let targets = collect_at_level(&root, level)?;
121 let out = match out_dir {
122 Some(d) => Path::new(d),
123 None => Path::new("."),
124 };
125 std::fs::create_dir_all(out)?;
126
127 let mut used: HashSet<String> = HashSet::new();
128 let mut written: Vec<PathBuf> = Vec::new();
129 for (path, value) in targets {
130 let base = path
131 .last()
132 .map(|s| sanitize(s))
133 .unwrap_or_else(|| "item".to_string());
134 let name = unique_file(&base, &mut used);
135 let file = out.join(format!("{name}.json"));
136 let out_bytes = serde_json::to_string_pretty(value)?;
137 std::fs::write(&file, out_bytes)?;
138 written.push(file);
139 }
140 Ok(written)
141}
142
143type FragItem<'a> = (Vec<String>, &'a serde_json::Value);
144
145fn collect_at_level(
146 value: &serde_json::Value,
147 level: usize,
148) -> Result<Vec<FragItem<'_>>, Box<dyn Error>> {
149 let children: Vec<(String, &serde_json::Value)> = match value {
150 serde_json::Value::Object(o) => o.iter().map(|(k, v)| (k.clone(), v)).collect(),
151 serde_json::Value::Array(a) => a
152 .iter()
153 .enumerate()
154 .map(|(i, v)| (i.to_string(), v))
155 .collect(),
156 _ => return Ok(Vec::new()),
157 };
158 let mut out = Vec::new();
159 if level == 0 {
160 for (k, v) in children {
161 out.push((vec![k], v));
162 }
163 } else {
164 for (k, v) in children {
165 let subs = collect_at_level(v, level - 1)?;
166 for (path, val) in subs {
167 let mut full = vec![k.clone()];
168 full.extend(path);
169 out.push((full, val));
170 }
171 }
172 }
173 Ok(out)
174}
175
176fn unique_key(raw: &str, used: &mut HashSet<String>) -> String {
177 if used.insert(raw.to_string()) {
178 return raw.to_string();
179 }
180 let mut n = 2;
181 loop {
182 let candidate = format!("{raw}_{n}");
183 if used.insert(candidate.clone()) {
184 return candidate;
185 }
186 n += 1;
187 }
188}
189
190fn unique_file(base: &str, used: &mut HashSet<String>) -> String {
191 if used.insert(base.to_string()) {
192 return base.to_string();
193 }
194 let mut n = 2;
195 loop {
196 let candidate = format!("{base}_{n}");
197 if used.insert(candidate.clone()) {
198 return candidate;
199 }
200 n += 1;
201 }
202}
203
204fn sanitize(name: &str) -> String {
205 let normalized: String = name
206 .nfd()
207 .filter(|c| !unicode_normalization::char::is_combining_mark(*c))
208 .collect::<String>()
209 .to_lowercase();
210 let mut out = String::new();
211 let mut last = false;
212 for c in normalized.chars() {
213 if c.is_ascii_alphanumeric() {
214 out.push(c);
215 last = false;
216 } else if !last && !out.is_empty() {
217 out.push('_');
218 last = true;
219 }
220 }
221 while out.ends_with('_') {
222 out.pop();
223 }
224 if out.is_empty() {
225 "item".to_string()
226 } else {
227 out
228 }
229}