use automerge::{ObjId, ObjType, Prop, ReadDoc, Value};
#[cfg(test)]
use automerge::transaction::Transactable;
pub fn dump_json<D: ReadDoc>(doc: &D, obj: &ObjId) -> String {
dump_value(doc, obj, 0)
}
pub fn dump_json_at<D: ReadDoc>(doc: &D, obj: &ObjId, prop: impl Into<Prop>) -> String {
match doc.get(obj, prop) {
Ok(Some((Value::Object(_), child_id))) => dump_value(doc, &child_id, 0),
Ok(Some((value, _))) => format_scalar(&value),
Ok(None) => "null".to_string(),
Err(e) => format!("<error: {}>", e),
}
}
pub fn dump_structure<D: ReadDoc>(doc: &D, obj: &ObjId) -> String {
dump_structure_impl(doc, obj, 0)
}
#[derive(Debug, Clone)]
pub struct DocEntry {
pub path: String,
pub value_type: String,
pub value_str: String,
}
pub struct DocumentInspector<'a, D: ReadDoc> {
doc: &'a D,
stack: Vec<InspectorState>,
}
enum InspectorState {
Map {
obj_id: ObjId,
keys: Vec<String>,
index: usize,
path: String,
},
List {
obj_id: ObjId,
len: usize,
index: usize,
path: String,
},
}
impl<'a, D: ReadDoc> DocumentInspector<'a, D> {
pub fn new(doc: &'a D, obj: &ObjId, path_prefix: &str) -> Self {
let obj_type = doc.object_type(obj).ok();
let initial_state = match obj_type {
Some(ObjType::Map) | Some(ObjType::Table) => {
let keys: Vec<String> = doc.keys(obj).collect();
InspectorState::Map {
obj_id: obj.clone(),
keys,
index: 0,
path: path_prefix.to_string(),
}
}
Some(ObjType::List) | Some(ObjType::Text) => InspectorState::List {
obj_id: obj.clone(),
len: doc.length(obj),
index: 0,
path: path_prefix.to_string(),
},
None => {
return Self { doc, stack: vec![] };
}
};
Self {
doc,
stack: vec![initial_state],
}
}
}
impl<'a, D: ReadDoc> Iterator for DocumentInspector<'a, D> {
type Item = DocEntry;
fn next(&mut self) -> Option<Self::Item> {
loop {
let state = self.stack.last_mut()?;
match state {
InspectorState::Map {
obj_id,
keys,
index,
path,
} => {
if *index >= keys.len() {
self.stack.pop();
continue;
}
let key = &keys[*index];
*index += 1;
let full_path = if path.is_empty() {
key.clone()
} else {
format!("{}.{}", path, key)
};
match self.doc.get(obj_id, key.as_str()) {
Ok(Some((Value::Object(obj_type), child_id))) => {
let child_state = match obj_type {
ObjType::Map | ObjType::Table => {
let child_keys: Vec<String> =
self.doc.keys(&child_id).collect();
InspectorState::Map {
obj_id: child_id.clone(),
keys: child_keys,
index: 0,
path: full_path.clone(),
}
}
ObjType::List | ObjType::Text => InspectorState::List {
obj_id: child_id.clone(),
len: self.doc.length(&child_id),
index: 0,
path: full_path.clone(),
},
};
self.stack.push(child_state);
return Some(DocEntry {
path: full_path,
value_type: format!("{:?}", obj_type),
value_str: format!("({} items)", count_items(self.doc, &child_id)),
});
}
Ok(Some((value, _))) => {
return Some(DocEntry {
path: full_path,
value_type: value_type_name(&value),
value_str: format_scalar(&value),
});
}
Ok(None) => {
return Some(DocEntry {
path: full_path,
value_type: "null".to_string(),
value_str: "null".to_string(),
});
}
Err(e) => {
return Some(DocEntry {
path: full_path,
value_type: "error".to_string(),
value_str: e.to_string(),
});
}
}
}
InspectorState::List {
obj_id,
len,
index,
path,
} => {
if *index >= *len {
self.stack.pop();
continue;
}
let idx = *index;
*index += 1;
let full_path = format!("{}[{}]", path, idx);
match self.doc.get(obj_id, idx) {
Ok(Some((Value::Object(obj_type), child_id))) => {
let child_state = match obj_type {
ObjType::Map | ObjType::Table => {
let child_keys: Vec<String> =
self.doc.keys(&child_id).collect();
InspectorState::Map {
obj_id: child_id.clone(),
keys: child_keys,
index: 0,
path: full_path.clone(),
}
}
ObjType::List | ObjType::Text => InspectorState::List {
obj_id: child_id.clone(),
len: self.doc.length(&child_id),
index: 0,
path: full_path.clone(),
},
};
self.stack.push(child_state);
return Some(DocEntry {
path: full_path,
value_type: format!("{:?}", obj_type),
value_str: format!("({} items)", count_items(self.doc, &child_id)),
});
}
Ok(Some((value, _))) => {
return Some(DocEntry {
path: full_path,
value_type: value_type_name(&value),
value_str: format_scalar(&value),
});
}
Ok(None) => {
return Some(DocEntry {
path: full_path,
value_type: "null".to_string(),
value_str: "null".to_string(),
});
}
Err(e) => {
return Some(DocEntry {
path: full_path,
value_type: "error".to_string(),
value_str: e.to_string(),
});
}
}
}
}
}
}
}
pub fn print_doc<D: ReadDoc>(doc: &D, obj: &ObjId) {
println!("Document contents:");
for entry in DocumentInspector::new(doc, obj, "") {
println!(
" {}: {} = {}",
entry.path, entry.value_type, entry.value_str
);
}
}
fn dump_value<D: ReadDoc>(doc: &D, obj: &ObjId, indent: usize) -> String {
let obj_type = doc.object_type(obj).ok();
match obj_type {
Some(ObjType::Map) | Some(ObjType::Table) => dump_map(doc, obj, indent),
Some(ObjType::List) => dump_list(doc, obj, indent),
Some(ObjType::Text) => dump_text(doc, obj),
None => "null".to_string(),
}
}
fn dump_map<D: ReadDoc>(doc: &D, obj: &ObjId, indent: usize) -> String {
let keys: Vec<String> = doc.keys(obj).collect();
if keys.is_empty() {
return "{}".to_string();
}
let indent_str = " ".repeat(indent);
let inner_indent = " ".repeat(indent + 1);
let entries: Vec<String> = keys
.iter()
.map(|key| {
let value_str = match doc.get(obj, key.as_str()) {
Ok(Some((Value::Object(_), child_id))) => dump_value(doc, &child_id, indent + 1),
Ok(Some((value, _))) => format_scalar(&value),
Ok(None) => "null".to_string(),
Err(_) => "<error>".to_string(),
};
format!("{}\"{}\": {}", inner_indent, key, value_str)
})
.collect();
format!("{{\n{}\n{}}}", entries.join(",\n"), indent_str)
}
fn dump_list<D: ReadDoc>(doc: &D, obj: &ObjId, indent: usize) -> String {
let len = doc.length(obj);
if len == 0 {
return "[]".to_string();
}
let indent_str = " ".repeat(indent);
let inner_indent = " ".repeat(indent + 1);
let entries: Vec<String> = (0..len)
.map(|idx| {
let value_str = match doc.get(obj, idx) {
Ok(Some((Value::Object(_), child_id))) => dump_value(doc, &child_id, indent + 1),
Ok(Some((value, _))) => format_scalar(&value),
Ok(None) => "null".to_string(),
Err(_) => "<error>".to_string(),
};
format!("{}{}", inner_indent, value_str)
})
.collect();
format!("[\n{}\n{}]", entries.join(",\n"), indent_str)
}
fn dump_text<D: ReadDoc>(doc: &D, obj: &ObjId) -> String {
match doc.text(obj) {
Ok(text) => format!("\"{}\"", escape_string(&text)),
Err(_) => "<text error>".to_string(),
}
}
fn format_scalar(value: &Value) -> String {
match value {
Value::Scalar(s) => {
use automerge::ScalarValue;
match s.as_ref() {
ScalarValue::Null => "null".to_string(),
ScalarValue::Boolean(b) => b.to_string(),
ScalarValue::Int(i) => i.to_string(),
ScalarValue::Uint(u) => u.to_string(),
ScalarValue::F64(f) => f.to_string(),
ScalarValue::Str(s) => format!("\"{}\"", escape_string(s)),
ScalarValue::Bytes(b) => format!("<bytes: {} bytes>", b.len()),
ScalarValue::Counter(c) => format!("<counter: {}>", i64::from(c.clone())),
ScalarValue::Timestamp(t) => format!("<timestamp: {}>", t),
ScalarValue::Unknown { type_code, bytes } => {
format!("<unknown type {}: {} bytes>", type_code, bytes.len())
}
}
}
Value::Object(_) => "<object>".to_string(),
}
}
fn value_type_name(value: &Value) -> String {
match value {
Value::Scalar(s) => {
use automerge::ScalarValue;
match s.as_ref() {
ScalarValue::Null => "null",
ScalarValue::Boolean(_) => "Bool",
ScalarValue::Int(_) => "Int",
ScalarValue::Uint(_) => "Uint",
ScalarValue::F64(_) => "Float",
ScalarValue::Str(_) => "String",
ScalarValue::Bytes(_) => "Bytes",
ScalarValue::Counter(_) => "Counter",
ScalarValue::Timestamp(_) => "Timestamp",
ScalarValue::Unknown { .. } => "Unknown",
}
.to_string()
}
Value::Object(obj_type) => format!("{:?}", obj_type),
}
}
fn dump_structure_impl<D: ReadDoc>(doc: &D, obj: &ObjId, indent: usize) -> String {
let obj_type = doc.object_type(obj).ok();
let indent_str = " ".repeat(indent);
let inner_indent = " ".repeat(indent + 1);
match obj_type {
Some(ObjType::Map) | Some(ObjType::Table) => {
let keys: Vec<String> = doc.keys(obj).collect();
if keys.is_empty() {
return "{}".to_string();
}
let entries: Vec<String> = keys
.iter()
.map(|key| {
let type_str = match doc.get(obj, key.as_str()) {
Ok(Some((Value::Object(_), child_id))) => {
dump_structure_impl(doc, &child_id, indent + 1)
}
Ok(Some((value, _))) => value_type_name(&value),
Ok(None) => "null".to_string(),
Err(_) => "<error>".to_string(),
};
format!("{}\"{}\": {}", inner_indent, key, type_str)
})
.collect();
format!("{{\n{}\n{}}}", entries.join(",\n"), indent_str)
}
Some(ObjType::List) => {
match doc.get(obj, 0usize) {
Ok(Some((Value::Object(_), child_id))) => {
format!("[{}]", dump_structure_impl(doc, &child_id, indent))
}
Ok(Some((value, _))) => format!("[{}]", value_type_name(&value)),
_ => "[]".to_string(),
}
}
Some(ObjType::Text) => "Text".to_string(),
None => "null".to_string(),
}
}
fn count_items<D: ReadDoc>(doc: &D, obj: &ObjId) -> usize {
match doc.object_type(obj).ok() {
Some(ObjType::Map) | Some(ObjType::Table) => doc.keys(obj).count(),
Some(ObjType::List) | Some(ObjType::Text) => doc.length(obj),
None => 0,
}
}
fn escape_string(s: &str) -> String {
s.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace('\n', "\\n")
.replace('\r', "\\r")
.replace('\t', "\\t")
}
#[cfg(test)]
mod tests {
use super::*;
use automerge::{AutoCommit, ROOT};
#[test]
fn test_dump_empty_doc() {
let doc = AutoCommit::new();
let json = dump_json(&doc, &ROOT);
assert_eq!(json, "{}");
}
#[test]
fn test_dump_simple_values() {
let mut doc = AutoCommit::new();
doc.put(&ROOT, "name", "Alice").unwrap();
doc.put(&ROOT, "age", 30i64).unwrap();
doc.put(&ROOT, "active", true).unwrap();
let json = dump_json(&doc, &ROOT);
assert!(json.contains("\"name\": \"Alice\""));
assert!(json.contains("\"age\": 30"));
assert!(json.contains("\"active\": true"));
}
#[test]
fn test_dump_nested() {
let mut doc = AutoCommit::new();
let obj = doc.put_object(&ROOT, "nested", ObjType::Map).unwrap();
doc.put(&obj, "inner", "value").unwrap();
let json = dump_json(&doc, &ROOT);
assert!(json.contains("\"nested\":"));
assert!(json.contains("\"inner\": \"value\""));
}
#[test]
fn test_dump_list() {
let mut doc = AutoCommit::new();
let list = doc.put_object(&ROOT, "items", ObjType::List).unwrap();
doc.insert(&list, 0, "a").unwrap();
doc.insert(&list, 1, "b").unwrap();
let json = dump_json(&doc, &ROOT);
assert!(json.contains("\"items\":"));
assert!(json.contains("["));
assert!(json.contains("\"a\""));
assert!(json.contains("\"b\""));
}
#[test]
fn test_inspector() {
let mut doc = AutoCommit::new();
doc.put(&ROOT, "name", "Alice").unwrap();
doc.put(&ROOT, "age", 30i64).unwrap();
let entries: Vec<_> = DocumentInspector::new(&doc, &ROOT, "").collect();
assert_eq!(entries.len(), 2);
let names: Vec<_> = entries.iter().map(|e| e.path.as_str()).collect();
assert!(names.contains(&"name"));
assert!(names.contains(&"age"));
}
}