summaryrefslogtreecommitdiff
path: root/server/src/util.rs
diff options
context:
space:
mode:
authorSomhairle H. Marisol <[email protected]>2026-09-17 14:32:37 +0800
committerSomhairle H. Marisol <[email protected]>2026-09-17 14:32:37 +0800
commit5c0ba37eda80d39e6ceca59bb1d5f4942f858995 (patch)
tree948723f9cedf7ccb0707fa6ee516bd30fe20fd10 /server/src/util.rs
downloadstrategy-lab-5c0ba37eda80d39e6ceca59bb1d5f4942f858995.tar.gz
chore: establish Strategy Lab source baseline (development, not release)
Diffstat (limited to 'server/src/util.rs')
-rw-r--r--server/src/util.rs138
1 files changed, 138 insertions, 0 deletions
diff --git a/server/src/util.rs b/server/src/util.rs
new file mode 100644
index 0000000..5a82806
--- /dev/null
+++ b/server/src/util.rs
@@ -0,0 +1,138 @@
+use sha2::{Digest, Sha256};
+
+pub fn sha256_hex(data: &[u8]) -> String {
+ let mut h = Sha256::new();
+ h.update(data);
+ hex::encode(h.finalize())
+}
+
+/// Synchronous UTC timestamp helper, ISO 8601 with millisecond precision.
+pub fn now_iso() -> String {
+ chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true)
+}
+
+pub fn plus_hours(hours: i64) -> String {
+ (chrono::Utc::now() + chrono::Duration::hours(hours))
+ .to_rfc3339_opts(chrono::SecondsFormat::Millis, true)
+}
+
+pub fn new_id() -> String {
+ uuid::Uuid::new_v4().to_string()
+}
+
+pub fn gen_token() -> String {
+ use rand::RngCore;
+ let mut buf = [0u8; 32];
+ rand::rngs::OsRng.fill_bytes(&mut buf);
+ hex::encode(buf)
+}
+
+/// Line based unified diff (full document, no hunks) used by the AI draft diff.
+pub fn unified_diff(a: &str, b: &str) -> String {
+ let x: Vec<&str> = a.lines().collect();
+ let y: Vec<&str> = b.lines().collect();
+ let n = x.len();
+ let m = y.len();
+ // LCS table
+ let mut dp = vec![vec![0usize; m + 1]; n + 1];
+ for i in (0..n).rev() {
+ for j in (0..m).rev() {
+ dp[i][j] = if x[i] == y[j] {
+ dp[i + 1][j + 1] + 1
+ } else {
+ dp[i + 1][j].max(dp[i][j + 1])
+ };
+ }
+ }
+ let mut out = String::new();
+ let mut ctx = std::collections::VecDeque::<usize>::new();
+ let (mut i, mut j) = (0usize, 0usize);
+ while i < n || j < m {
+ if i < n && j < m && x[i] == y[j] {
+ ctx.push_back(i);
+ if ctx.len() > 2 {
+ let keep = ctx.pop_front().unwrap();
+ out.push_str(&format!(" {}\n", x[keep]));
+ }
+ i += 1;
+ j += 1;
+ } else {
+ // entering a change: emit queued context lines first
+ for k in ctx.drain(..) {
+ out.push_str(&format!(" {}\n", x[k]));
+ }
+ if i < n && (j >= m || dp[i + 1][j] >= dp[i][j + 1]) {
+ out.push_str(&format!("-{}\n", x[i]));
+ i += 1;
+ } else {
+ out.push_str(&format!("+{}\n", y[j]));
+ j += 1;
+ }
+ }
+ }
+ out
+}
+
+/// Deterministic canonical JSON string (recursively sorted keys) for hashing.
+pub fn canonical_json(v: &serde_json::Value) -> String {
+ fn sort(v: &serde_json::Value) -> serde_json::Value {
+ match v {
+ serde_json::Value::Array(a) => serde_json::Value::Array(a.iter().map(sort).collect()),
+ serde_json::Value::Object(o) => {
+ let mut keys: Vec<(String, serde_json::Value)> =
+ o.iter().map(|(k, v)| (k.clone(), sort(v))).collect();
+ keys.sort_by(|a, b| a.0.cmp(&b.0));
+ serde_json::Value::Object(keys.into_iter().collect())
+ }
+ other => other.clone(),
+ }
+ }
+ sort(v).to_string()
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn now_iso_is_synchronous_iso8601() {
+ let t = now_iso();
+ let parsed = chrono::DateTime::parse_from_rfc3339(&t).expect("ISO timestamp");
+ assert!(parsed.timestamp() > 1_700_000_000);
+ assert!(t.ends_with('Z'));
+ }
+
+ #[test]
+ fn plus_hours_and_tokens() {
+ let e = chrono::DateTime::parse_from_rfc3339(&plus_hours(1)).unwrap();
+ let n = chrono::DateTime::parse_from_rfc3339(&now_iso()).unwrap();
+ assert!((e - n).num_minutes() >= 59);
+ let a = gen_token();
+ assert_eq!(a.len(), 64);
+ assert_ne!(a, gen_token());
+ assert_eq!(new_id().len(), 36);
+ }
+
+ #[test]
+ fn sha256_is_stable() {
+ assert_eq!(sha256_hex(b"abc"), "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad");
+ }
+
+ #[test]
+ fn unified_diff_marks_changes_with_context() {
+ let a = "x\nkeep\ny";
+ let b = "x\nkeep\nz";
+ let d = unified_diff(a, b);
+ assert!(d.contains("-y\n+z\n"), "diff: {d}");
+ assert!(d.contains(" keep"));
+ let same = unified_diff(a, "x\nkeep\ny");
+ assert!(!same.contains('+') && !same.contains('-'), "no diff: {same}");
+ }
+
+ #[test]
+ fn canonical_json_sorts_recursively() {
+ let v: serde_json::Value =
+ serde_json::from_str(r#"{"b":1,"a":{"y":[3,2],"x":true}}"#).unwrap();
+ assert_eq!(canonical_json(&v), r#"{"a":{"x":true,"y":[3,2]},"b":1}"#);
+ }
+}