use serde_json::{Map, Value};
pub fn scaffold(name: &str, version: &str) -> Value {
let mut obj = Map::new();
obj.insert("name".into(), Value::String(name.to_string()));
obj.insert("version".into(), Value::String(version.to_string()));
obj.insert("dependencies".into(), Value::Object(Map::new()));
Value::Object(obj)
}
pub fn upsert_dependency(doc: &mut Value, name: &str, range: &str) {
let Some(obj) = doc.as_object_mut() else {
return;
};
let deps = obj
.entry("dependencies")
.or_insert_with(|| Value::Object(Map::new()));
if let Some(map) = deps.as_object_mut() {
map.insert(name.to_string(), Value::String(range.to_string()));
sort_keys(map);
}
}
pub fn dependencies(doc: &Value) -> Vec<(String, String)> {
let mut out: Vec<(String, String)> = doc
.get("dependencies")
.and_then(Value::as_object)
.map(|map| {
map.iter()
.filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
.collect()
})
.unwrap_or_default();
out.sort_by(|a, b| a.0.cmp(&b.0));
out
}
pub fn to_pretty(doc: &Value) -> String {
let mut s = serde_json::to_string_pretty(doc).expect("serialize package.json");
s.push('\n');
s
}
fn sort_keys(map: &mut Map<String, Value>) {
let mut entries: Vec<(String, Value)> = std::mem::take(map).into_iter().collect();
entries.sort_by(|a, b| a.0.cmp(&b.0));
for (k, v) in entries {
map.insert(k, v);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn scaffold_has_name_version_and_empty_deps() {
let doc = scaffold("my-app", "1.0.0");
assert_eq!(doc["name"], "my-app");
assert_eq!(doc["version"], "1.0.0");
assert!(doc["dependencies"].as_object().unwrap().is_empty());
}
#[test]
fn upsert_preserves_other_keys_and_sorts_dependencies() {
let mut doc: Value = serde_json::from_str(
r#"{"name":"app","version":"1.0.0","dependencies":{"c":"^1","a":"^1"},"scripts":{"build":"x"}}"#,
)
.unwrap();
upsert_dependency(&mut doc, "b", "^2");
let keys: Vec<&str> = doc
.as_object()
.unwrap()
.keys()
.map(String::as_str)
.collect();
assert_eq!(keys, ["name", "version", "dependencies", "scripts"]);
assert_eq!(
dependencies(&doc),
vec![
("a".to_string(), "^1".to_string()),
("b".to_string(), "^2".to_string()),
("c".to_string(), "^1".to_string()),
]
);
assert_eq!(doc["scripts"]["build"], "x");
}
#[test]
fn upsert_updates_an_existing_range_in_place() {
let mut doc = scaffold("app", "1.0.0");
upsert_dependency(&mut doc, "lit", "^3");
upsert_dependency(&mut doc, "lit", "^3.2.0");
assert_eq!(
dependencies(&doc),
vec![("lit".to_string(), "^3.2.0".to_string())]
);
}
#[test]
fn upsert_creates_the_dependencies_object_when_absent() {
let mut doc: Value = serde_json::from_str(r#"{"name":"app","version":"1.0.0"}"#).unwrap();
upsert_dependency(&mut doc, "ms", "^2");
assert_eq!(doc["dependencies"]["ms"], "^2");
}
#[test]
fn to_pretty_is_two_space_indented_with_trailing_newline() {
let doc = scaffold("app", "1.0.0");
let text = to_pretty(&doc);
assert!(text.ends_with("}\n"));
assert!(
text.contains("\n \"name\": \"app\""),
"two-space indent: {text}"
);
}
}