152 lines
5.1 KiB
Rust
152 lines
5.1 KiB
Rust
// json-archive is a tool for tracking JSON file changes over time
|
|
// Copyright (C) 2025 Peoples Grocers LLC
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU Affero General Public License as published
|
|
// by the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU Affero General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Affero General Public License
|
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
//
|
|
// To purchase a license under different terms contact admin@peoplesgrocers.com
|
|
// To request changes, report bugs, or give user feedback contact
|
|
// marxism@peoplesgrocers.com
|
|
//
|
|
|
|
use json_archive::{
|
|
append_to_archive, create_archive_from_files, default_output_filename, is_json_archive, Diagnostic,
|
|
DiagnosticCode, DiagnosticLevel,
|
|
};
|
|
use std::path::Path;
|
|
use std::process;
|
|
|
|
mod cmd;
|
|
mod flags;
|
|
|
|
fn main() {
|
|
let flags = flags::JsonArchive::from_env_or_exit();
|
|
|
|
let diagnostics = run(flags);
|
|
|
|
for diagnostic in &diagnostics {
|
|
eprintln!("{}", diagnostic);
|
|
}
|
|
|
|
let has_fatal = diagnostics.iter().any(|d| d.is_fatal());
|
|
if has_fatal {
|
|
process::exit(1);
|
|
}
|
|
}
|
|
|
|
fn run(flags: flags::JsonArchive) -> Vec<Diagnostic> {
|
|
match flags.subcommand {
|
|
flags::JsonArchiveCmd::Create(create_flags) => create_archive(&create_flags),
|
|
flags::JsonArchiveCmd::Info(info_flags) => cmd::info::run(&info_flags),
|
|
flags::JsonArchiveCmd::State(state_flags) => cmd::state::run(&state_flags),
|
|
}
|
|
}
|
|
|
|
fn create_archive(flags: &flags::Create) -> Vec<Diagnostic> {
|
|
if flags.inputs.is_empty() {
|
|
return vec![Diagnostic::new(
|
|
DiagnosticLevel::Fatal,
|
|
DiagnosticCode::MissingHeaderField,
|
|
"I need at least one JSON file to create an archive, but you didn't provide any."
|
|
.to_string(),
|
|
)
|
|
.with_advice(
|
|
"Usage: json-archive <file1.json> [file2.json ...]\n\n\
|
|
The first file will be used as the initial state, and subsequent files \
|
|
will be compared to generate change events."
|
|
.to_string(),
|
|
)];
|
|
}
|
|
|
|
let output_path = match &flags.output {
|
|
Some(path) => path.clone(),
|
|
None => default_output_filename(&flags.inputs[0]),
|
|
};
|
|
|
|
let mut diagnostics = Vec::new();
|
|
for input_path in &flags.inputs {
|
|
if !Path::new(input_path).exists() {
|
|
diagnostics.push(
|
|
Diagnostic::new(
|
|
DiagnosticLevel::Fatal,
|
|
DiagnosticCode::PathNotFound,
|
|
format!("I couldn't find the input file: {}", input_path.display()),
|
|
)
|
|
.with_advice(
|
|
"Make sure the file path is correct and the file exists. \
|
|
Check for typos in the filename."
|
|
.to_string(),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
if !diagnostics.is_empty() {
|
|
return diagnostics;
|
|
}
|
|
|
|
let first_is_archive = match is_json_archive(&flags.inputs[0]) {
|
|
Ok(is_archive) => is_archive,
|
|
Err(e) => {
|
|
return vec![Diagnostic::new(
|
|
DiagnosticLevel::Fatal,
|
|
DiagnosticCode::PathNotFound,
|
|
format!("I couldn't check if the first file is an archive: {}", e),
|
|
)];
|
|
}
|
|
};
|
|
|
|
if first_is_archive {
|
|
println!("First input appears to be a JSON archive file");
|
|
if flags.inputs.len() == 1 {
|
|
return vec![
|
|
Diagnostic::new(
|
|
DiagnosticLevel::Fatal,
|
|
DiagnosticCode::MissingHeaderField,
|
|
"I found that the first input is already an archive file, but you didn't provide any additional JSON files to append.".to_string()
|
|
)
|
|
.with_advice(
|
|
"If you want to append to an archive, provide additional JSON files:\n\
|
|
json-archive existing.json.archive new1.json new2.json"
|
|
.to_string()
|
|
)
|
|
];
|
|
}
|
|
|
|
return append_to_archive(&flags.inputs[0], &flags.inputs[1..], &output_path, flags.source.clone(), flags.snapshot_interval);
|
|
}
|
|
|
|
println!("Creating archive: {}", output_path.display());
|
|
println!("Input files: {:?}", flags.inputs);
|
|
|
|
if let Some(interval) = flags.snapshot_interval {
|
|
println!("Snapshot interval: every {} observations", interval);
|
|
}
|
|
|
|
if let Some(ref source) = flags.source {
|
|
println!("Source: {}", source);
|
|
}
|
|
|
|
match create_archive_from_files(
|
|
&flags.inputs,
|
|
output_path.clone(),
|
|
flags.source.clone(),
|
|
flags.snapshot_interval,
|
|
) {
|
|
Ok(()) => {
|
|
println!("Archive created successfully: {}", output_path.display());
|
|
Vec::new()
|
|
}
|
|
Err(diagnostics) => diagnostics,
|
|
}
|
|
}
|