use std::collections::HashSet;
use std::error::Error;
use std::path::{Path, PathBuf};
use unicode_normalization::UnicodeNormalization;
pub fn concat(inputs: &[String], output: &str) -> Result<(), Box<dyn Error>> {
let mut map = serde_json::Map::new();
let mut used: HashSet<String> = HashSet::new();
for input in inputs {
let bytes = std::fs::read(input)?;
let value: serde_json::Value = serde_json::from_slice(&bytes)?;
let stem = Path::new(input)
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or(input)
.to_string();
let key = unique_key(&stem, &mut used);
map.insert(key, value);
}
let out = serde_json::to_string_pretty(&serde_json::Value::Object(map))?;
std::fs::write(output, out)?;
Ok(())
}
pub fn append(
target: &str,
additions: &[String],
output: Option<&str>,
) -> Result<(), Box<dyn Error>> {
let mut root: serde_json::Value = if Path::new(target).exists() {
let bytes = std::fs::read(target)?;
serde_json::from_slice(&bytes)?
} else {
serde_json::Value::Object(serde_json::Map::new())
};
let obj = root.as_object_mut().ok_or("target is not a JSON object")?;
for input in additions {
let bytes = std::fs::read(input)?;
let value: serde_json::Value = serde_json::from_slice(&bytes)?;
let stem = Path::new(input)
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or(input)
.to_string();
obj.insert(stem, value);
}
let out = serde_json::to_string_pretty(&root)?;
std::fs::write(output.unwrap_or(target), out)?;
Ok(())
}
pub fn frag(
input: &str,
level: usize,
out_dir: Option<&str>,
) -> Result<Vec<PathBuf>, Box<dyn Error>> {
let bytes = std::fs::read(input)?;
let root: serde_json::Value = serde_json::from_slice(&bytes)?;
let targets = collect_at_level(&root, level)?;
let out = match out_dir {
Some(d) => Path::new(d),
None => Path::new("."),
};
std::fs::create_dir_all(out)?;
let mut used: HashSet<String> = HashSet::new();
let mut written: Vec<PathBuf> = Vec::new();
for (path, value) in targets {
let base = path
.last()
.map(|s| sanitize(s))
.unwrap_or_else(|| "item".to_string());
let name = unique_file(&base, &mut used);
let file = out.join(format!("{name}.json"));
let out_bytes = serde_json::to_string_pretty(value)?;
std::fs::write(&file, out_bytes)?;
written.push(file);
}
Ok(written)
}
type FragItem<'a> = (Vec<String>, &'a serde_json::Value);
fn collect_at_level(
value: &serde_json::Value,
level: usize,
) -> Result<Vec<FragItem<'_>>, Box<dyn Error>> {
let children: Vec<(String, &serde_json::Value)> = match value {
serde_json::Value::Object(o) => o.iter().map(|(k, v)| (k.clone(), v)).collect(),
serde_json::Value::Array(a) => a
.iter()
.enumerate()
.map(|(i, v)| (i.to_string(), v))
.collect(),
_ => return Ok(Vec::new()),
};
let mut out = Vec::new();
if level == 0 {
for (k, v) in children {
out.push((vec![k], v));
}
} else {
for (k, v) in children {
let subs = collect_at_level(v, level - 1)?;
for (path, val) in subs {
let mut full = vec![k.clone()];
full.extend(path);
out.push((full, val));
}
}
}
Ok(out)
}
fn unique_key(raw: &str, used: &mut HashSet<String>) -> String {
if used.insert(raw.to_string()) {
return raw.to_string();
}
let mut n = 2;
loop {
let candidate = format!("{raw}_{n}");
if used.insert(candidate.clone()) {
return candidate;
}
n += 1;
}
}
fn unique_file(base: &str, used: &mut HashSet<String>) -> String {
if used.insert(base.to_string()) {
return base.to_string();
}
let mut n = 2;
loop {
let candidate = format!("{base}_{n}");
if used.insert(candidate.clone()) {
return candidate;
}
n += 1;
}
}
fn sanitize(name: &str) -> String {
let normalized: String = name
.nfd()
.filter(|c| !unicode_normalization::char::is_combining_mark(*c))
.collect::<String>()
.to_lowercase();
let mut out = String::new();
let mut last = false;
for c in normalized.chars() {
if c.is_ascii_alphanumeric() {
out.push(c);
last = false;
} else if !last && !out.is_empty() {
out.push('_');
last = true;
}
}
while out.ends_with('_') {
out.pop();
}
if out.is_empty() {
"item".to_string()
} else {
out
}
}