Skip to content

Commit 0293b12

Browse files
authored
Merge pull request #1 from GetDutchie/add-yaml-support
add yaml support
2 parents d0e572b + ac41a1c commit 0293b12

File tree

7 files changed

+237
-55
lines changed

7 files changed

+237
-55
lines changed

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ version = "0.1.0"
44
edition = "2021"
55

66
[dependencies]
7+
anyhow = "1.0.95"
78
clap = { version = "4.5.21", features = ["derive"] }
89
md5 = "0.7.0"
910
serde_json = "1.0.134"
11+
serde_yaml = "0.9.34"

README.md

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,20 @@
11
# checksum
2-
A simple binary that computes checksums for JSON files regardless of key ordering.
2+
3+
A simple binary that computes checksums for
4+
JSON and YAML files regardless of key ordering.
5+
6+
## Usage
7+
8+
To install run: `cargo install --path .`
9+
10+
```sh
11+
checksum -p <path>
12+
```
13+
14+
```sh
15+
checksum '{"a": 1, "b": 2}' -t json
16+
```
17+
18+
TODOS:
19+
20+
- [ ] Add workflow to handle versioned releases (dutchiebot-releases)

src/digest.rs

Lines changed: 42 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,22 @@
1+
use anyhow::Result;
12
use md5::Digest;
23
use serde_json::Value;
4+
use serde_yaml;
35
use std::collections::BTreeMap;
46

5-
fn sort_json(value: &mut Value) {
7+
fn sort(value: &mut Value) {
68
match value {
79
Value::Array(arr) => {
810
for val in arr.iter_mut() {
9-
sort_json(val);
11+
sort(val);
1012
}
1113
arr.sort_by_key(|a| a.to_string());
1214
}
1315
Value::Object(map) => {
1416
let mut sorted_map = BTreeMap::new();
1517
for (key, val) in map.iter_mut() {
1618
let mut val = val.take();
17-
sort_json(&mut val);
19+
sort(&mut val);
1820
sorted_map.insert(key.clone(), val);
1921
}
2022
*map = sorted_map.into_iter().collect();
@@ -24,53 +26,47 @@ fn sort_json(value: &mut Value) {
2426
}
2527
}
2628

27-
fn compute_json_digest(s: &str) -> Digest {
28-
let mut json: Value = serde_json::from_str(s).unwrap();
29-
sort_json(&mut json);
30-
md5::compute(json.to_string())
29+
fn compute_json_digest(s: &str) -> Result<Digest> {
30+
let json: Result<Value, serde_json::Error> = serde_json::from_str(s);
31+
match json {
32+
Ok(mut data) => {
33+
sort(&mut data);
34+
Ok(md5::compute(data.to_string()))
35+
}
36+
Err(_) => Err(anyhow::anyhow!("Unable to parse json contents"))?,
37+
}
3138
}
3239

33-
pub fn compute_digest(s: &str, ext: Option<&str>) -> Digest {
34-
match ext {
35-
Some("json") => compute_json_digest(s),
36-
_ => panic!("Unsupported file extension: {:?}", ext),
40+
fn compute_yaml_digest(s: &str) -> Result<Digest> {
41+
let yaml: Result<Value, serde_yaml::Error> = serde_yaml::from_str(s);
42+
match yaml {
43+
Ok(mut data) => {
44+
sort(&mut data);
45+
Ok(md5::compute(data.to_string()))
46+
}
47+
Err(_) => Err(anyhow::anyhow!("Unable to parse yaml contents"))?,
3748
}
3849
}
39-
#[cfg(test)]
40-
mod tests {
41-
use super::*;
4250

43-
#[test]
44-
fn it_works() {
45-
let a = compute_json_digest(
46-
r#"
47-
{"a": 1, "b": 2}
48-
"#,
49-
);
50-
let b = compute_json_digest(
51-
r#"
52-
{"b": 2, "a": 1}
53-
"#,
54-
);
55-
assert_eq!(a, b);
56-
let a = compute_json_digest(
57-
r#"
58-
[
59-
{"foo": 1, "bar": 2},
60-
{"baz": 3, "bop": 4},
61-
{"zip": 5, "zap": 6}
62-
]
63-
"#,
64-
);
65-
let b = compute_json_digest(
66-
r#"
67-
[
68-
{"baz": 3, "bop": 4},
69-
{"zip": 5, "zap": 6},
70-
{"foo": 1, "bar": 2}
71-
]
72-
"#,
73-
);
74-
assert_eq!(a, b);
51+
/// # Example
52+
/// ```
53+
/// use checksum::digest::compute_digest;
54+
/// use md5::Digest;
55+
/// // regardless of the order of the keys, the digest should be the same
56+
/// let digest_a: Digest = compute_digest("{\"a\": 1, \"b\": 2}", Some("json")).unwrap();
57+
/// let digest_b: Digest = compute_digest("{\"b\": 2, \"a\": 1}", Some("json")).unwrap();
58+
/// assert_eq!(digest_a, digest_b);
59+
/// ```
60+
pub fn compute_digest(s: &str, ext: Option<&str>) -> Result<Digest> {
61+
match ext {
62+
Some("json") => compute_json_digest(s),
63+
Some("yaml") | Some("yml") => compute_yaml_digest(s),
64+
_ => {
65+
if let Some(ext) = ext {
66+
panic!("Unsupported file type, {:?}", ext);
67+
} else {
68+
panic!("No file extension provided");
69+
}
70+
}
7571
}
7672
}

src/main.rs

Lines changed: 49 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,62 @@
1+
use anyhow::Result;
12
use checksum::digest::compute_digest;
23
use clap::Parser;
3-
use std::path::PathBuf;
4+
use std::io::{self, IsTerminal, Read};
5+
use std::path::{Path, PathBuf};
46

5-
#[derive(Parser)]
7+
#[derive(Debug, Parser)]
68
#[command(version, about, long_about = None)]
79
struct Cli {
10+
/// Path to file to compute digest for
811
#[arg(short, long, value_name = "FILE")]
912
path: Option<PathBuf>,
13+
/// Input string to compute digest for
14+
#[arg(short, long, value_name = "INPUT")]
15+
input: Option<String>,
16+
/// File type of supplied input. Supported file extensions: json, yaml, yml
17+
#[arg(short, long, value_name = "FILETYPE")]
18+
r#type: Option<String>,
1019
}
1120

12-
fn main() {
13-
let cli = Cli::parse();
21+
fn handle_path(path: &Path) -> Result<(), anyhow::Error> {
22+
if let Some(path_ext) = path.extension() {
23+
let file_contents = std::fs::read_to_string(path)?;
24+
let digest = compute_digest(&file_contents, path_ext.to_str())?;
25+
println!("{:?}", digest);
26+
Ok(())
27+
} else {
28+
Err(anyhow::anyhow!(
29+
"Unable to extract file extension for {:?}",
30+
path
31+
))?
32+
}
33+
}
34+
35+
fn handle_input(input: &str, filetype: Option<&str>) -> Result<(), anyhow::Error> {
36+
let digest = compute_digest(input, filetype)?;
37+
println!("{:?}", digest);
38+
Ok(())
39+
}
40+
41+
fn handle_terminal(filetype: Option<&str>) -> Result<(), anyhow::Error> {
42+
let mut buffer = String::new();
43+
io::stdin().read_to_string(&mut buffer)?;
44+
let digest = compute_digest(buffer.trim(), filetype)?;
45+
println!("{:?}", digest);
46+
Ok(())
47+
}
1448

49+
fn main() -> Result<()> {
50+
let cli = Cli::parse();
1551
if let Some(path) = cli.path.as_deref() {
16-
let path_ext = path.extension().unwrap();
17-
let file_contents = std::fs::read_to_string(path).unwrap();
18-
let digest = compute_digest(&file_contents, path_ext.to_str());
19-
println!("{:?}", digest);
52+
handle_path(path)
53+
} else if let Some(input) = cli.input.as_deref() {
54+
handle_input(input, cli.r#type.as_deref())
55+
} else if !io::stdin().is_terminal() {
56+
handle_terminal(cli.r#type.as_deref())
57+
} else {
58+
Err(anyhow::anyhow!(
59+
"Unable to output digest with supplied command line args"
60+
))?
2061
}
2162
}
File renamed without changes.

tests/data.yaml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
---
2+
up:
3+
- uv venv && uv sync && echo "$(pwd)/.venv/bin" >> .bolt/.path
4+
- echo "KUBECONFIG=$(pwd)/configs/kubeconfig" >> .bolt/.env
5+
6+
cmds:
7+
ci:
8+
desc: Bolt steps to run in CI
9+
steps:
10+
- cmd: verify
11+
- cmd: lint
12+
13+
lint:
14+
desc: Lint all Python scripts
15+
steps:
16+
- cmd: lint.yaml
17+
- cmd: lint.python
18+
cmds:
19+
yaml:
20+
steps:
21+
- cmd: format.yaml
22+
vars: { lint: true }
23+
python:
24+
steps:
25+
- uv run ruff check scripts/
26+
- cmd: format.python
27+
vars: { lint: true }

tests/unit.rs

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
#[cfg(test)]
2+
mod tests {
3+
use checksum::digest::compute_digest;
4+
5+
#[test]
6+
fn compute_json_digest_tests() {
7+
let a = compute_digest(
8+
r#"
9+
{"a": 1, "b": 2}
10+
"#,
11+
Some("json"),
12+
)
13+
.unwrap();
14+
let b = compute_digest(
15+
r#"
16+
{"b": 2, "a": 1}
17+
"#,
18+
Some("json"),
19+
)
20+
.unwrap();
21+
assert_eq!(a, b);
22+
let a = compute_digest(
23+
r#"
24+
[
25+
{"foo": 1, "bar": 2},
26+
{"baz": 3, "bop": 4},
27+
{"zip": 5, "zap": 6}
28+
]
29+
"#,
30+
Some("json"),
31+
)
32+
.unwrap();
33+
let b = compute_digest(
34+
r#"
35+
[
36+
{"baz": 3, "bop": 4},
37+
{"zip": 5, "zap": 6},
38+
{"foo": 1, "bar": 2}
39+
]
40+
"#,
41+
Some("json"),
42+
)
43+
.unwrap();
44+
assert_eq!(a, b);
45+
}
46+
47+
#[test]
48+
fn compute_yaml_digest_tests() {
49+
let a = compute_digest(
50+
"apiVersion: v1\nkind: Pod\nmetadata:\n name: my-pod\nspec:\n containers:\n - name: my-container\n image: nginx\n"
51+
, Some("yaml")).unwrap();
52+
let b = compute_digest(
53+
"spec:\n containers:\n - name: my-container\n image: nginx\nkind: Pod\napiVersion: v1\nmetadata:\n name: my-pod\n", Some("yaml")
54+
).unwrap();
55+
assert_eq!(a, b);
56+
let a = compute_digest(
57+
r#"
58+
apiVersion: v1
59+
kind: Pod
60+
metadata:
61+
name: example-pod
62+
labels:
63+
app: example
64+
spec:
65+
containers:
66+
- name: example-container
67+
image: nginx:latest
68+
command: |
69+
echo 'Starting...'
70+
echo 'Hello, Kubernetes!'
71+
echo 'Pod initialization complete.'
72+
"#,
73+
Some("yaml"),
74+
)
75+
.unwrap();
76+
let b = compute_digest(
77+
r#"
78+
kind: Pod
79+
apiVersion: v1
80+
spec:
81+
containers:
82+
- name: example-container
83+
command: |
84+
echo 'Starting...'
85+
echo 'Hello, Kubernetes!'
86+
echo 'Pod initialization complete.'
87+
image: nginx:latest
88+
metadata:
89+
name: example-pod
90+
labels:
91+
app: example
92+
"#,
93+
Some("yaml"),
94+
)
95+
.unwrap();
96+
assert_eq!(a, b);
97+
}
98+
}

0 commit comments

Comments
 (0)