use crate::session::SessionMeta;
pub const MANIFEST: &str = "manifest.json";
pub const BASE_SNAPSHOT: &str = "snapshots/base.json";
pub const PACKAGE_FORMAT_VERSION: u32 = crate::session::SESSION_FORMAT_VERSION;
pub fn snapshot_filename(entry_id: Option<&str>, revision: u64) -> String {
match entry_id {
Some(id) => format!("snapshots/entry-{}.json", sanitize(id)),
None => format!("snapshots/rev-{revision}.json"),
}
}
fn sanitize(id: &str) -> String {
id.chars()
.map(|c| {
if c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-') {
c
} else {
'_'
}
})
.collect()
}
pub fn compact_plan(meta: &SessionMeta, head_revision: u64) -> SessionMeta {
let mut compacted = meta.clone();
compacted.base_revision = head_revision;
compacted
}
pub fn manifest_text(manifest: &serde_json::Value) -> String {
let mut world = manifest.clone();
if let Some(entities) = world.get_mut("entities").and_then(|e| e.as_array_mut()) {
entities.sort_by_key(|e| e.get("id").and_then(|id| id.as_u64()).unwrap_or(u64::MAX));
}
let mut out = String::new();
pretty(&world, 0, &mut out);
out.push('\n');
out
}
pub fn manifest_text_of(manifest: &crate::WorldManifest) -> String {
let text = serde_json::to_string(manifest).expect("a manifest serializes");
manifest_text(&serde_json::from_str(&text).expect("its own JSON parses"))
}
fn pretty(value: &serde_json::Value, depth: usize, out: &mut String) {
use serde_json::Value;
let pad = " ".repeat(depth);
let inner = " ".repeat(depth + 1);
match value {
Value::Array(items) if items.is_empty() => out.push_str("[]"),
Value::Array(items) if items.iter().all(|x| !x.is_array() && !x.is_object()) => {
out.push('[');
for (i, item) in items.iter().enumerate() {
if i > 0 {
out.push_str(", ");
}
scalar(item, out);
}
out.push(']');
}
Value::Array(items) => {
out.push_str("[\n");
for (i, item) in items.iter().enumerate() {
if i > 0 {
out.push_str(",\n");
}
out.push_str(&inner);
pretty(item, depth + 1, out);
}
out.push('\n');
out.push_str(&pad);
out.push(']');
}
Value::Object(map) => {
let mut keys: Vec<&String> = map
.iter()
.filter(|(_, v)| !v.is_null())
.map(|(k, _)| k)
.collect();
if keys.is_empty() {
out.push_str("{}");
return;
}
keys.sort();
out.push_str("{\n");
for (i, key) in keys.iter().enumerate() {
if i > 0 {
out.push_str(",\n");
}
out.push_str(&inner);
out.push_str(&serde_json::to_string(key).expect("a string serializes"));
out.push_str(": ");
pretty(&map[key.as_str()], depth + 1, out);
}
out.push('\n');
out.push_str(&pad);
out.push('}');
}
other => scalar(other, out),
}
}
fn scalar(value: &serde_json::Value, out: &mut String) {
if let Some(f) = value.as_f64()
&& value.is_f64()
&& f.fract() == 0.0
&& f.abs() <= crate::identity::MAX_ENTITY_ID as f64
{
out.push_str(&format!("{}", f as i64));
return;
}
out.push_str(&serde_json::to_string(value).expect("a value serializes"));
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn snapshots_name_by_entry_when_there_is_one() {
assert_eq!(
snapshot_filename(Some("e42"), 7),
"snapshots/entry-e42.json"
);
assert_eq!(
snapshot_filename(Some("sha256:4b754af+/odd"), 7),
"snapshots/entry-sha256_4b754af__odd.json"
);
assert_eq!(snapshot_filename(None, 7), "snapshots/rev-7.json");
}
#[test]
fn compaction_moves_the_base_to_head() {
let meta = SessionMeta {
base_revision: 0,
head_revision: 41,
..SessionMeta::new("castle")
};
let plan = compact_plan(&meta, 41);
assert_eq!(plan.base_revision, 41);
assert_eq!(plan.head_revision, 41);
assert_eq!(plan.name, "castle");
}
}