Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add yaml support #1

Merged
merged 5 commits into from
Jan 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ version = "0.1.0"
edition = "2021"

[dependencies]
anyhow = "1.0.95"
clap = { version = "4.5.21", features = ["derive"] }
md5 = "0.7.0"
serde_json = "1.0.134"
serde_yaml = "0.9.34"
20 changes: 19 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,20 @@
# checksum
A simple binary that computes checksums for JSON files regardless of key ordering.

A simple binary that computes checksums for
JSON and YAML files regardless of key ordering.

## Usage

To install run: `cargo install --path .`

```sh
checksum -p <path>
```

```sh
checksum '{"a": 1, "b": 2}' -t json
```

TODOS:

- [ ] Add workflow to handle versioned releases (dutchiebot-releases)
88 changes: 42 additions & 46 deletions src/digest.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
use anyhow::Result;
use md5::Digest;
use serde_json::Value;
use serde_yaml;
use std::collections::BTreeMap;

fn sort_json(value: &mut Value) {
fn sort(value: &mut Value) {
match value {
Value::Array(arr) => {
for val in arr.iter_mut() {
sort_json(val);
sort(val);
}
arr.sort_by_key(|a| a.to_string());
}
Value::Object(map) => {
let mut sorted_map = BTreeMap::new();
for (key, val) in map.iter_mut() {
let mut val = val.take();
sort_json(&mut val);
sort(&mut val);
sorted_map.insert(key.clone(), val);
}
*map = sorted_map.into_iter().collect();
Expand All @@ -24,53 +26,47 @@ fn sort_json(value: &mut Value) {
}
}

fn compute_json_digest(s: &str) -> Digest {
let mut json: Value = serde_json::from_str(s).unwrap();
sort_json(&mut json);
md5::compute(json.to_string())
fn compute_json_digest(s: &str) -> Result<Digest> {
let json: Result<Value, serde_json::Error> = serde_json::from_str(s);
match json {
Ok(mut data) => {
sort(&mut data);
Ok(md5::compute(data.to_string()))
}
Err(_) => Err(anyhow::anyhow!("Unable to parse json contents"))?,
}
}

pub fn compute_digest(s: &str, ext: Option<&str>) -> Digest {
match ext {
Some("json") => compute_json_digest(s),
_ => panic!("Unsupported file extension: {:?}", ext),
fn compute_yaml_digest(s: &str) -> Result<Digest> {
let yaml: Result<Value, serde_yaml::Error> = serde_yaml::from_str(s);
match yaml {
Ok(mut data) => {
sort(&mut data);
Ok(md5::compute(data.to_string()))
}
Err(_) => Err(anyhow::anyhow!("Unable to parse yaml contents"))?,
}
}
#[cfg(test)]
mod tests {
use super::*;

#[test]
fn it_works() {
let a = compute_json_digest(
r#"
{"a": 1, "b": 2}
"#,
);
let b = compute_json_digest(
r#"
{"b": 2, "a": 1}
"#,
);
assert_eq!(a, b);
let a = compute_json_digest(
r#"
[
{"foo": 1, "bar": 2},
{"baz": 3, "bop": 4},
{"zip": 5, "zap": 6}
]
"#,
);
let b = compute_json_digest(
r#"
[
{"baz": 3, "bop": 4},
{"zip": 5, "zap": 6},
{"foo": 1, "bar": 2}
]
"#,
);
assert_eq!(a, b);
/// # Example
/// ```
/// use checksum::digest::compute_digest;
/// use md5::Digest;
/// // regardless of the order of the keys, the digest should be the same
/// let digest_a: Digest = compute_digest("{\"a\": 1, \"b\": 2}", Some("json")).unwrap();
/// let digest_b: Digest = compute_digest("{\"b\": 2, \"a\": 1}", Some("json")).unwrap();
/// assert_eq!(digest_a, digest_b);
/// ```
pub fn compute_digest(s: &str, ext: Option<&str>) -> Result<Digest> {
match ext {
Some("json") => compute_json_digest(s),
Some("yaml") | Some("yml") => compute_yaml_digest(s),
_ => {
if let Some(ext) = ext {
panic!("Unsupported file type, {:?}", ext);
} else {
panic!("No file extension provided");
}
}
}
}
57 changes: 49 additions & 8 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,21 +1,62 @@
use anyhow::Result;
use checksum::digest::compute_digest;
use clap::Parser;
use std::path::PathBuf;
use std::io::{self, IsTerminal, Read};
use std::path::{Path, PathBuf};

#[derive(Parser)]
#[derive(Debug, Parser)]
#[command(version, about, long_about = None)]
struct Cli {
/// Path to file to compute digest for
#[arg(short, long, value_name = "FILE")]
path: Option<PathBuf>,
/// Input string to compute digest for
#[arg(short, long, value_name = "INPUT")]
input: Option<String>,
/// File type of supplied input. Supported file extensions: json, yaml, yml
#[arg(short, long, value_name = "FILETYPE")]
r#type: Option<String>,
}

fn main() {
let cli = Cli::parse();
fn handle_path(path: &Path) -> Result<(), anyhow::Error> {
if let Some(path_ext) = path.extension() {
let file_contents = std::fs::read_to_string(path)?;
let digest = compute_digest(&file_contents, path_ext.to_str())?;
println!("{:?}", digest);
Ok(())
} else {
Err(anyhow::anyhow!(
"Unable to extract file extension for {:?}",
path
))?
}
}

fn handle_input(input: &str, filetype: Option<&str>) -> Result<(), anyhow::Error> {
let digest = compute_digest(input, filetype)?;
println!("{:?}", digest);
Ok(())
}

fn handle_terminal(filetype: Option<&str>) -> Result<(), anyhow::Error> {
let mut buffer = String::new();
io::stdin().read_to_string(&mut buffer)?;
let digest = compute_digest(buffer.trim(), filetype)?;
println!("{:?}", digest);
Ok(())
}

fn main() -> Result<()> {
let cli = Cli::parse();
if let Some(path) = cli.path.as_deref() {
let path_ext = path.extension().unwrap();
let file_contents = std::fs::read_to_string(path).unwrap();
let digest = compute_digest(&file_contents, path_ext.to_str());
println!("{:?}", digest);
handle_path(path)
} else if let Some(input) = cli.input.as_deref() {
handle_input(input, cli.r#type.as_deref())
} else if !io::stdin().is_terminal() {
handle_terminal(cli.r#type.as_deref())
} else {
Err(anyhow::anyhow!(
"Unable to output digest with supplied command line args"
))?
}
}
File renamed without changes.
27 changes: 27 additions & 0 deletions tests/data.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
up:
- uv venv && uv sync && echo "$(pwd)/.venv/bin" >> .bolt/.path
- echo "KUBECONFIG=$(pwd)/configs/kubeconfig" >> .bolt/.env

cmds:
ci:
desc: Bolt steps to run in CI
steps:
- cmd: verify
- cmd: lint

lint:
desc: Lint all Python scripts
steps:
- cmd: lint.yaml
- cmd: lint.python
cmds:
yaml:
steps:
- cmd: format.yaml
vars: { lint: true }
python:
steps:
- uv run ruff check scripts/
- cmd: format.python
vars: { lint: true }
98 changes: 98 additions & 0 deletions tests/unit.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
#[cfg(test)]
mod tests {
use checksum::digest::compute_digest;

#[test]
fn compute_json_digest_tests() {
let a = compute_digest(
r#"
{"a": 1, "b": 2}
"#,
Some("json"),
)
.unwrap();
let b = compute_digest(
r#"
{"b": 2, "a": 1}
"#,
Some("json"),
)
.unwrap();
assert_eq!(a, b);
let a = compute_digest(
r#"
[
{"foo": 1, "bar": 2},
{"baz": 3, "bop": 4},
{"zip": 5, "zap": 6}
]
"#,
Some("json"),
)
.unwrap();
let b = compute_digest(
r#"
[
{"baz": 3, "bop": 4},
{"zip": 5, "zap": 6},
{"foo": 1, "bar": 2}
]
"#,
Some("json"),
)
.unwrap();
assert_eq!(a, b);
}

#[test]
fn compute_yaml_digest_tests() {
let a = compute_digest(
"apiVersion: v1\nkind: Pod\nmetadata:\n name: my-pod\nspec:\n containers:\n - name: my-container\n image: nginx\n"
, Some("yaml")).unwrap();
let b = compute_digest(
"spec:\n containers:\n - name: my-container\n image: nginx\nkind: Pod\napiVersion: v1\nmetadata:\n name: my-pod\n", Some("yaml")
).unwrap();
assert_eq!(a, b);
let a = compute_digest(
r#"
apiVersion: v1
kind: Pod
metadata:
name: example-pod
labels:
app: example
spec:
containers:
- name: example-container
image: nginx:latest
command: |
echo 'Starting...'
echo 'Hello, Kubernetes!'
echo 'Pod initialization complete.'
"#,
Some("yaml"),
)
.unwrap();
let b = compute_digest(
r#"
kind: Pod
apiVersion: v1
spec:
containers:
- name: example-container
command: |
echo 'Starting...'
echo 'Hello, Kubernetes!'
echo 'Pod initialization complete.'
image: nginx:latest
metadata:
name: example-pod
labels:
app: example
"#,
Some("yaml"),
)
.unwrap();
assert_eq!(a, b);
}
}