Skip to main content

git_meta_lib/
list_value.rs

1//! Encoding and parsing helpers for timestamped list values.
2
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5use sha1::{Digest, Sha1};
6
7use crate::error::{Error, Result};
8
9/// A single timestamped entry in a metadata list value.
10#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
11pub struct ListEntry {
12    /// The string payload stored in this list entry.
13    pub value: String,
14    /// The entry timestamp used for deterministic ordering and tree serialization.
15    pub timestamp: i64,
16}
17
18/// Parse a stored list JSON blob into timestamped entries.
19/// Legacy string arrays are assigned deterministic timestamps based on order.
20pub fn parse_entries(raw: &str) -> Result<Vec<ListEntry>> {
21    let values: Vec<Value> = serde_json::from_str(raw)?;
22    let mut entries = Vec::with_capacity(values.len());
23
24    for (idx, value) in values.into_iter().enumerate() {
25        match value {
26            Value::String(s) => {
27                entries.push(ListEntry {
28                    value: s,
29                    timestamp: legacy_timestamp(idx),
30                });
31            }
32            Value::Object(mut map) => {
33                let val_field = map.remove("value").ok_or_else(|| {
34                    Error::InvalidValue("list entry missing 'value' field".into())
35                })?;
36                let value = val_field
37                    .as_str()
38                    .ok_or_else(|| Error::InvalidValue("list entry 'value' must be string".into()))?
39                    .to_string();
40                let timestamp = match map.remove("timestamp") {
41                    Some(Value::Number(num)) => num.as_i64().ok_or_else(|| {
42                        Error::InvalidValue("list entry 'timestamp' must be integer".into())
43                    })?,
44                    Some(Value::String(s)) => s.parse::<i64>().map_err(|_| {
45                        Error::InvalidValue("list entry 'timestamp' must be integer".into())
46                    })?,
47                    None => legacy_timestamp(idx),
48                    Some(other) => {
49                        return Err(Error::InvalidValue(format!(
50                            "list entry 'timestamp' must be integer, got {other:?}"
51                        )))
52                    }
53                };
54                entries.push(ListEntry { value, timestamp });
55            }
56            other => {
57                return Err(Error::InvalidValue(format!(
58                    "invalid list entry type: expected string or object, got {other:?}"
59                )));
60            }
61        }
62    }
63
64    Ok(entries)
65}
66
67/// Serialize list entries back to JSON objects.
68pub fn encode_entries(entries: &[ListEntry]) -> Result<String> {
69    Ok(serde_json::to_string(entries)?)
70}
71
72/// Extract just the string values from a stored list JSON blob.
73#[cfg_attr(not(feature = "internal"), allow(dead_code))]
74pub fn list_values_from_json(raw: &str) -> Result<Vec<String>> {
75    Ok(parse_entries(raw)?
76        .into_iter()
77        .map(|entry| entry.value)
78        .collect())
79}
80
81/// Build a deterministic entry name used for Git tree serialization.
82#[must_use]
83pub fn make_entry_name(entry: &ListEntry) -> String {
84    make_entry_name_from_parts(entry.timestamp, &entry.value)
85}
86
87/// Build a deterministic entry name from a timestamp and value content hash.
88pub(crate) fn make_entry_name_from_parts(timestamp: i64, value: &str) -> String {
89    let mut hasher = Sha1::new();
90    hasher.update(value.as_bytes());
91    let hash = format!("{:x}", hasher.finalize());
92    format!("{}-{}", timestamp, &hash[..5])
93}
94
95/// Extract the timestamp from a list entry name (format: `<timestamp>-<hash>`).
96#[must_use]
97pub fn parse_timestamp_from_entry_name(name: &str) -> Option<i64> {
98    let idx = name.find('-')?;
99    name[..idx].parse().ok()
100}
101
102fn legacy_timestamp(idx: usize) -> i64 {
103    idx as i64
104}