use argh::FromArgs;
use calcit::detailed_snapshot::{
DetailCirru, DetailedCodeEntry, DetailedFileInSnapshot, DetailedNsEntry, DetailedSnapshot, load_detailed_snapshot_data,
};
use calcit::snapshot::{CodeEntry, FileInSnapShot, NsEntry, Snapshot, load_snapshot_data};
use cirru_edn::Edn;
use cirru_parser::Cirru;
use std::fmt::Debug;
use std::fs;
use std::path::PathBuf;
use std::sync::Arc;
#[derive(FromArgs)]
struct Args {
#[argh(option, short = 'c', default = "PathBuf::from(\"compact.cirru\")")]
compact_path: PathBuf,
#[argh(option, short = 'f', default = "PathBuf::from(\"calcit.cirru\")")]
calcit_path: PathBuf,
#[argh(switch, short = 'd')]
dry_run: bool,
#[argh(switch, short = 'v')]
verbose: bool,
}
fn print_code_change(from: &Cirru, to: &Cirru) {
println!(" Code change:");
println!(" From: {from:?}");
println!(" To: {to:?}");
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let args: Args = argh::from_env();
if args.verbose {
println!(
"Syncing changes from {} to {}",
args.compact_path.display(),
args.calcit_path.display()
);
}
let compact_content = fs::read_to_string(&args.compact_path).map_err(|e| format!("Failed to read compact.cirru: {e}"))?;
let calcit_content = fs::read_to_string(&args.calcit_path).map_err(|e| format!("Failed to read calcit.cirru: {e}"))?;
let compact_data = cirru_edn::parse(&compact_content).map_err(|e| format!("Failed to parse compact file: {e}"))?;
let compact_snapshot = load_snapshot_data(&compact_data, &args.compact_path.to_string_lossy())
.map_err(|e| format!("Failed to load compact snapshot: {e}"))?;
let calcit_data = cirru_edn::parse(&calcit_content).map_err(|e| format!("Failed to parse calcit file: {e}"))?;
let mut detailed_snapshot = load_detailed_snapshot_data(&calcit_data, &args.calcit_path.to_string_lossy())
.map_err(|e| format!("Failed to load detailed snapshot: {e}"))?;
if args.verbose {
println!("Loaded compact snapshot with {} files", compact_snapshot.files.len());
println!("Loaded calcit snapshot with {} files", detailed_snapshot.files.len());
}
let changes = detect_snapshot_changes(&compact_snapshot, &detailed_snapshot);
let configs_changed = has_configs_changed(&compact_snapshot, &detailed_snapshot);
let entries_changed = has_entries_changed(&compact_snapshot, &detailed_snapshot);
if changes.is_empty() && !configs_changed && !entries_changed {
println!("No changes detected between compact and calcit files.");
return Ok(());
}
if args.verbose {
if configs_changed {
println!("Configs changed");
}
if entries_changed {
println!("Entries changed");
}
}
if args.verbose {
println!("Detected {} changes:", changes.len());
for change in &changes {
println!(" {change}");
if let (Some(SnapshotEntry::Def(new_entry)), ChangePath::FunctionDefinition { file_name, def_name }) =
(&change.new_entry, &change.path)
{
if let Some(detailed_file) = detailed_snapshot.files.get(file_name) {
if let Some(detailed_entry) = detailed_file.defs.get(def_name) {
match change.change_type {
ChangeType::ModifiedCode => {
let compact_cirru: Cirru = new_entry.code.clone();
let detailed_cirru: Cirru = detailed_entry.code.clone().into();
print_code_change(&detailed_cirru, &compact_cirru);
}
ChangeType::ModifiedDoc => {
println!(" Doc change: from \"{}\" to \"{}\"", detailed_entry.doc, new_entry.doc);
}
ChangeType::Modified => {
let compact_cirru: Cirru = new_entry.code.clone();
let detailed_cirru: Cirru = detailed_entry.code.clone().into();
print_code_change(&detailed_cirru, &compact_cirru);
println!(" Doc change: from \"{}\" to \"{}\"", detailed_entry.doc, new_entry.doc);
}
_ => {}
}
}
}
} else if let (Some(SnapshotEntry::Ns(new_entry)), ChangePath::NamespaceDefinition { file_name }) =
(&change.new_entry, &change.path)
{
if let Some(detailed_file) = detailed_snapshot.files.get(file_name) {
match change.change_type {
ChangeType::ModifiedCode => {
let compact_cirru: Cirru = new_entry.code.clone();
let detailed_cirru: Cirru = detailed_file.ns.code.clone().into();
print_code_change(&detailed_cirru, &compact_cirru);
}
ChangeType::ModifiedDoc => {
println!(" Doc change: from \"{}\" to \"{}\"", detailed_file.ns.doc, new_entry.doc);
}
ChangeType::Modified => {
let compact_cirru: Cirru = new_entry.code.clone();
let detailed_cirru: Cirru = detailed_file.ns.code.clone().into();
print_code_change(&detailed_cirru, &compact_cirru);
println!(" Doc change: from \"{}\" to \"{}\"", detailed_file.ns.doc, new_entry.doc);
}
_ => {}
}
}
}
}
}
if args.dry_run {
println!("Dry run mode: would apply {} changes", changes.len());
if configs_changed {
println!("Would update configs");
}
if entries_changed {
println!("Would update entries");
}
return Ok(());
}
apply_snapshot_changes(&mut detailed_snapshot, &changes);
if configs_changed || entries_changed {
merge_configs_and_entries(&mut detailed_snapshot, &compact_snapshot);
}
let updated_edn = detailed_snapshot_to_edn(&detailed_snapshot);
let formatted_content = cirru_edn::format(&updated_edn, true).map_err(|e| format!("Failed to format updated calcit.cirru: {e}"))?;
fs::write(&args.calcit_path, formatted_content).map_err(|e| format!("Failed to write calcit.cirru: {e}"))?;
println!("Successfully applied {} changes to {}", changes.len(), args.calcit_path.display());
Ok(())
}
#[derive(Debug, Clone)]
enum ChangePath {
NamespaceDefinition { file_name: String },
FunctionDefinition { file_name: String, def_name: String },
}
impl std::fmt::Display for ChangePath {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ChangePath::NamespaceDefinition { file_name } => write!(f, "{file_name}.ns"),
ChangePath::FunctionDefinition { file_name, def_name } => write!(f, "{file_name}.defs.{def_name}"),
}
}
}
#[derive(Debug, Clone)]
enum SnapshotEntry {
Ns(NsEntry),
Def(CodeEntry),
}
#[derive(Debug, Clone)]
struct SnapshotChange {
path: ChangePath,
change_type: ChangeType,
new_entry: Option<SnapshotEntry>,
}
#[derive(Debug, Clone, PartialEq)]
enum ChangeType {
Added,
Modified,
ModifiedCode,
ModifiedDoc,
Removed,
}
impl std::fmt::Display for SnapshotChange {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match &self.change_type {
ChangeType::Added => write!(f, "Added: {}", self.path),
ChangeType::Modified => write!(f, "Modified: {}", self.path),
ChangeType::ModifiedCode => write!(f, "Modified code: {}", self.path),
ChangeType::ModifiedDoc => write!(f, "Modified doc: {}", self.path),
ChangeType::Removed => write!(f, "Removed: {}", self.path),
}
}
}
fn detailed_snapshot_to_edn(snapshot: &DetailedSnapshot) -> Edn {
let mut modified_snapshot = snapshot.clone();
modified_snapshot.files.retain(|k, _| !k.ends_with(".$meta"));
for (file_name, file) in &mut modified_snapshot.files {
let ns_cirru: Cirru = file.ns.code.clone().into();
if ns_cirru == vec!["ns", "unknown"].into() || ns_cirru.is_empty() {
let fixed_cirru: Cirru = vec!["ns", file_name.as_str()].into();
file.ns.code = fixed_cirru.into();
}
}
let mut edn_map = cirru_edn::EdnMapView::default();
edn_map.insert_key("package", Edn::Str(modified_snapshot.package.as_str().into()));
edn_map.insert_key("configs", modified_snapshot.configs.clone());
edn_map.insert_key("entries", modified_snapshot.entries.clone());
let mut files_map = cirru_edn::EdnMapView::default();
for (k, v) in &modified_snapshot.files {
if k.ends_with(".$meta") {
continue;
}
let mut file_record = cirru_edn::EdnRecordView {
tag: cirru_edn::EdnTag::new("FileEntry"),
pairs: Vec::new(),
};
let mut defs_map = cirru_edn::EdnMapView::default();
for (def_name, def_entry) in &v.defs {
defs_map.insert(Edn::str(def_name.as_str()), detailed_code_entry_to_edn(def_entry));
}
file_record.pairs.push(("defs".into(), Edn::from(defs_map)));
file_record.pairs.push(("ns".into(), detailed_ns_entry_to_edn(&v.ns)));
files_map.insert(Edn::str(k.as_str()), Edn::Record(file_record));
}
edn_map.insert_key("files", files_map.into());
if !modified_snapshot.users.is_nil() {
edn_map.insert_key("users", modified_snapshot.users.clone());
}
Edn::from(edn_map)
}
fn detailed_code_entry_to_edn(entry: &DetailedCodeEntry) -> Edn {
let mut record = cirru_edn::EdnRecordView {
tag: cirru_edn::EdnTag::new("CodeEntry"),
pairs: Vec::new(),
};
record.pairs.push(("code".into(), detailed_cirru_to_edn(&entry.code)));
record.pairs.push(("doc".into(), Edn::Str(entry.doc.as_str().into())));
let examples_list: Vec<Edn> = entry
.examples
.iter()
.map(|e| {
let simple_cirru: Cirru = e.clone().into();
simple_cirru.into()
})
.collect();
record.pairs.push(("examples".into(), Edn::List(examples_list.into())));
Edn::Record(record)
}
fn detailed_ns_entry_to_edn(entry: &DetailedNsEntry) -> Edn {
let mut record = cirru_edn::EdnRecordView {
tag: cirru_edn::EdnTag::new("NsEntry"),
pairs: Vec::new(),
};
record.pairs.push(("code".into(), detailed_cirru_to_edn(&entry.code)));
record.pairs.push(("doc".into(), Edn::Str(entry.doc.as_str().into())));
Edn::Record(record)
}
fn detailed_cirru_to_edn(cirru: &DetailCirru) -> Edn {
match cirru {
DetailCirru::Leaf { at, by, text } => {
let mut record = cirru_edn::EdnRecordView {
tag: cirru_edn::EdnTag::new("Leaf"),
pairs: Vec::new(),
};
record.pairs.push(("at".into(), Edn::Number(*at as f64)));
record.pairs.push(("by".into(), Edn::Str(by.as_str().into())));
if let Some(t) = text {
record.pairs.push(("text".into(), Edn::Str(t.as_str().into())));
}
Edn::Record(record)
}
DetailCirru::List { data, at, by } => {
let mut record = cirru_edn::EdnRecordView {
tag: cirru_edn::EdnTag::new("Expr"), pairs: Vec::new(),
};
record.pairs.push(("at".into(), Edn::Number(*at as f64)));
record.pairs.push(("by".into(), Edn::Str(by.as_str().into())));
let mut data_map = cirru_edn::EdnMapView::default();
for (k, v) in data {
data_map.insert(Edn::str(k.as_str()), detailed_cirru_to_edn(v));
}
record.pairs.push(("data".into(), Edn::from(data_map)));
Edn::Record(record)
}
}
}
fn merge_configs_and_entries(detailed: &mut DetailedSnapshot, compact: &Snapshot) {
let mut configs_map = cirru_edn::EdnMapView::default();
configs_map.insert_key("init-fn", Edn::Str(compact.configs.init_fn.as_str().into()));
configs_map.insert_key("reload-fn", Edn::Str(compact.configs.reload_fn.as_str().into()));
configs_map.insert_key("version", Edn::Str(compact.configs.version.as_str().into()));
configs_map.insert_key(
"modules",
Edn::List(
compact
.configs
.modules
.iter()
.map(|m| Edn::Str(m.as_str().into()))
.collect::<Vec<_>>()
.into(),
),
);
detailed.configs = Edn::from(configs_map);
let mut entries_map = cirru_edn::EdnMapView::default();
for (key, entry_config) in &compact.entries {
let mut entry_map = cirru_edn::EdnMapView::default();
entry_map.insert_key("init-fn", Edn::Str(entry_config.init_fn.as_str().into()));
entry_map.insert_key("reload-fn", Edn::Str(entry_config.reload_fn.as_str().into()));
entry_map.insert_key("version", Edn::Str(entry_config.version.as_str().into()));
entry_map.insert_key(
"modules",
Edn::List(
entry_config
.modules
.iter()
.map(|m| Edn::Str(m.as_str().into()))
.collect::<Vec<_>>()
.into(),
),
);
entries_map.insert_key(key.as_str(), Edn::from(entry_map));
}
detailed.entries = Edn::from(entries_map);
}
fn has_configs_changed(compact: &Snapshot, detailed: &DetailedSnapshot) -> bool {
let detailed_map = match detailed.configs.view_map() {
Ok(m) => m,
Err(_) => return true, };
if let Ok(init_fn) = detailed_map.get_or_nil("init-fn").try_into() {
let init_fn: Arc<str> = init_fn;
if init_fn.as_ref() != compact.configs.init_fn.as_str() {
return true;
}
} else {
return true;
}
if let Ok(reload_fn) = detailed_map.get_or_nil("reload-fn").try_into() {
let reload_fn: Arc<str> = reload_fn;
if reload_fn.as_ref() != compact.configs.reload_fn.as_str() {
return true;
}
} else {
return true;
}
if let Ok(version) = detailed_map.get_or_nil("version").try_into() {
let version: Arc<str> = version;
if version.as_ref() != compact.configs.version.as_str() {
return true;
}
} else {
return true;
}
if let Ok(modules_edn) = detailed_map.get_or_nil("modules").view_list() {
let detailed_modules: Vec<String> = modules_edn
.iter()
.filter_map(|e| {
let s: Result<Arc<str>, _> = e.to_owned().try_into();
s.ok().map(|a| a.to_string())
})
.collect();
if detailed_modules != compact.configs.modules {
return true;
}
} else if !compact.configs.modules.is_empty() {
return true;
}
false
}
fn has_entries_changed(compact: &Snapshot, detailed: &DetailedSnapshot) -> bool {
let detailed_entries_map = match detailed.entries.view_map() {
Ok(m) => m,
Err(_) => {
return !compact.entries.is_empty();
}
};
let cirru_edn::EdnMapView(detailed_map) = &detailed_entries_map;
let detailed_keys: std::collections::HashSet<String> = detailed_map
.keys()
.filter_map(|k| {
let s: Result<std::sync::Arc<str>, _> = k.to_owned().try_into();
s.ok().map(|a| a.to_string())
})
.collect();
let compact_keys: std::collections::HashSet<String> = compact.entries.keys().cloned().collect();
if detailed_keys != compact_keys {
return true;
}
for (key, compact_entry) in &compact.entries {
if let Ok(detailed_entry_edn) = detailed_entries_map.get_or_nil(key.as_str()).view_map() {
if let Ok(init_fn) = detailed_entry_edn.get_or_nil("init-fn").try_into() {
let init_fn: std::sync::Arc<str> = init_fn;
if init_fn.as_ref() != compact_entry.init_fn.as_str() {
return true;
}
} else {
return true;
}
if let Ok(reload_fn) = detailed_entry_edn.get_or_nil("reload-fn").try_into() {
let reload_fn: std::sync::Arc<str> = reload_fn;
if reload_fn.as_ref() != compact_entry.reload_fn.as_str() {
return true;
}
} else {
return true;
}
if let Ok(version) = detailed_entry_edn.get_or_nil("version").try_into() {
let version: std::sync::Arc<str> = version;
if version.as_ref() != compact_entry.version.as_str() {
return true;
}
} else {
return true;
}
} else {
return true;
}
}
false
}
fn detect_snapshot_changes(compact: &Snapshot, detailed: &DetailedSnapshot) -> Vec<SnapshotChange> {
let mut changes = Vec::new();
for (file_name, compact_file) in &compact.files {
if file_name.ends_with(".$meta") {
continue;
}
match detailed.files.get(file_name) {
Some(detailed_file) => {
compare_file_definitions(file_name, compact_file, detailed_file, &mut changes);
}
None => {
changes.push(SnapshotChange {
path: ChangePath::NamespaceDefinition {
file_name: file_name.clone(),
},
change_type: ChangeType::Added,
new_entry: Some(SnapshotEntry::Ns(compact_file.ns.clone())),
});
for (def_name, code_entry) in &compact_file.defs {
changes.push(SnapshotChange {
path: ChangePath::FunctionDefinition {
file_name: file_name.clone(),
def_name: def_name.clone(),
},
change_type: ChangeType::Added,
new_entry: Some(SnapshotEntry::Def(code_entry.clone())),
});
}
}
}
}
for (file_name, detailed_file) in &detailed.files {
if !compact.files.contains_key(file_name) {
for def_name in detailed_file.defs.keys() {
changes.push(SnapshotChange {
path: ChangePath::FunctionDefinition {
file_name: file_name.clone(),
def_name: def_name.clone(),
},
change_type: ChangeType::Removed,
new_entry: None,
});
}
}
}
changes
}
fn compare_file_definitions(
file_name: &str,
compact_file: &FileInSnapShot,
detailed_file: &DetailedFileInSnapshot,
changes: &mut Vec<SnapshotChange>,
) {
let compact_ns_cirru: Cirru = compact_file.ns.code.clone();
let detailed_ns_cirru: Cirru = detailed_file.ns.code.clone().into();
let code_changed = compact_ns_cirru != detailed_ns_cirru;
let doc_changed = compact_file.ns.doc != detailed_file.ns.doc;
if code_changed || doc_changed {
let change_type = if code_changed && doc_changed {
ChangeType::Modified
} else if code_changed {
ChangeType::ModifiedCode
} else {
ChangeType::ModifiedDoc
};
changes.push(SnapshotChange {
path: ChangePath::NamespaceDefinition {
file_name: file_name.to_string(),
},
change_type,
new_entry: Some(SnapshotEntry::Ns(compact_file.ns.clone())),
});
}
for (def_name, compact_entry) in &compact_file.defs {
match detailed_file.defs.get(def_name) {
Some(detailed_entry) => {
let compact_cirru: Cirru = compact_entry.code.clone();
let detailed_cirru: Cirru = detailed_entry.code.clone().into();
let code_changed = compact_cirru != detailed_cirru;
let doc_changed = compact_entry.doc != detailed_entry.doc;
let compact_examples: Vec<Cirru> = compact_entry.examples.clone();
let detailed_examples: Vec<Cirru> = detailed_entry.examples.iter().map(|e| e.clone().into()).collect();
let examples_changed = compact_examples != detailed_examples;
if code_changed || doc_changed || examples_changed {
let change_type = if code_changed && doc_changed {
ChangeType::Modified
} else if code_changed {
ChangeType::ModifiedCode
} else {
ChangeType::ModifiedDoc
};
changes.push(SnapshotChange {
path: ChangePath::FunctionDefinition {
file_name: file_name.to_string(),
def_name: def_name.clone(),
},
change_type,
new_entry: Some(SnapshotEntry::Def(compact_entry.clone())),
});
}
}
None => {
changes.push(SnapshotChange {
path: ChangePath::FunctionDefinition {
file_name: file_name.to_string(),
def_name: def_name.clone(),
},
change_type: ChangeType::Added,
new_entry: Some(SnapshotEntry::Def(compact_entry.clone())),
});
}
}
}
for def_name in detailed_file.defs.keys() {
if !compact_file.defs.contains_key(def_name) {
changes.push(SnapshotChange {
path: ChangePath::FunctionDefinition {
file_name: file_name.to_string(),
def_name: def_name.clone(),
},
change_type: ChangeType::Removed,
new_entry: None,
});
}
}
}
fn apply_snapshot_changes(detailed: &mut DetailedSnapshot, changes: &[SnapshotChange]) {
for change in changes {
match change.change_type {
ChangeType::Added => {
if let Some(new_entry) = &change.new_entry {
apply_add_change(detailed, &change.path, new_entry);
}
}
ChangeType::Modified | ChangeType::ModifiedCode | ChangeType::ModifiedDoc => {
if let Some(new_entry) = &change.new_entry {
apply_modify_change(detailed, &change.path, new_entry, &change.change_type);
}
}
ChangeType::Removed => {
apply_remove_change(detailed, &change.path);
}
}
}
}
fn apply_add_change(detailed: &mut DetailedSnapshot, path: &ChangePath, new_entry: &SnapshotEntry) {
match path {
ChangePath::FunctionDefinition { file_name, def_name } => {
if !detailed.files.contains_key(file_name) {
use calcit::detailed_snapshot::{DetailedFileInSnapshot, DetailedNsEntry};
use std::collections::HashMap;
let empty_ns = DetailedNsEntry {
doc: String::new(),
code: cirru_parser::Cirru::Leaf("".into()).into(),
};
detailed.files.insert(
file_name.clone(),
DetailedFileInSnapshot {
ns: empty_ns,
defs: HashMap::new(),
},
);
}
if let (Some(file), SnapshotEntry::Def(new_entry)) = (detailed.files.get_mut(file_name), new_entry) {
file.defs.insert(def_name.clone(), new_entry.clone().into());
}
}
ChangePath::NamespaceDefinition { file_name } => {
if !detailed.files.contains_key(file_name) {
use calcit::detailed_snapshot::DetailedFileInSnapshot;
use std::collections::HashMap;
detailed.files.insert(
file_name.clone(),
DetailedFileInSnapshot {
ns: match new_entry {
SnapshotEntry::Ns(entry) => entry.clone().into(),
SnapshotEntry::Def(_) => DetailedNsEntry {
doc: String::new(),
code: cirru_parser::Cirru::Leaf("".into()).into(),
},
},
defs: HashMap::new(),
},
);
} else if let (Some(file), SnapshotEntry::Ns(new_entry)) = (detailed.files.get_mut(file_name), new_entry) {
file.ns = new_entry.clone().into();
}
}
}
}
fn apply_modify_change(detailed: &mut DetailedSnapshot, path: &ChangePath, new_entry: &SnapshotEntry, change_type: &ChangeType) {
match path {
ChangePath::FunctionDefinition { file_name, def_name } => {
if let (Some(file), SnapshotEntry::Def(new_entry)) = (detailed.files.get_mut(file_name), new_entry) {
if let Some(existing_def) = file.defs.get_mut(def_name) {
existing_def.doc = new_entry.doc.clone();
existing_def.examples = new_entry.examples.iter().map(|e| e.clone().into()).collect();
if *change_type != ChangeType::ModifiedDoc {
existing_def.code = new_entry.code.clone().into();
}
}
}
}
ChangePath::NamespaceDefinition { file_name } => {
if let (Some(file), SnapshotEntry::Ns(new_entry)) = (detailed.files.get_mut(file_name), new_entry) {
file.ns.doc = new_entry.doc.clone();
if *change_type != ChangeType::ModifiedDoc {
file.ns.code = new_entry.code.clone().into();
}
}
}
}
}
fn apply_remove_change(detailed: &mut DetailedSnapshot, path: &ChangePath) {
match path {
ChangePath::FunctionDefinition { file_name, def_name } => {
if let Some(file) = detailed.files.get_mut(file_name) {
file.defs.remove(def_name);
}
}
ChangePath::NamespaceDefinition { .. } => {
}
}
}