git_meta_lib/
list_value.rs1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3use sha1::{Digest, Sha1};
4
5use crate::error::{Error, Result};
6
7#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
9pub struct ListEntry {
10 pub value: String,
11 pub timestamp: i64,
12}
13
14pub fn parse_entries(raw: &str) -> Result<Vec<ListEntry>> {
17 let values: Vec<Value> = serde_json::from_str(raw)?;
18 let mut entries = Vec::with_capacity(values.len());
19
20 for (idx, value) in values.into_iter().enumerate() {
21 match value {
22 Value::String(s) => {
23 entries.push(ListEntry {
24 value: s,
25 timestamp: legacy_timestamp(idx),
26 });
27 }
28 Value::Object(mut map) => {
29 let val_field = map.remove("value").ok_or_else(|| {
30 Error::InvalidValue("list entry missing 'value' field".into())
31 })?;
32 let value = val_field
33 .as_str()
34 .ok_or_else(|| Error::InvalidValue("list entry 'value' must be string".into()))?
35 .to_string();
36 let timestamp = match map.remove("timestamp") {
37 Some(Value::Number(num)) => num.as_i64().ok_or_else(|| {
38 Error::InvalidValue("list entry 'timestamp' must be integer".into())
39 })?,
40 Some(Value::String(s)) => s.parse::<i64>().map_err(|_| {
41 Error::InvalidValue("list entry 'timestamp' must be integer".into())
42 })?,
43 None => legacy_timestamp(idx),
44 Some(other) => {
45 return Err(Error::InvalidValue(format!(
46 "list entry 'timestamp' must be integer, got {other:?}"
47 )))
48 }
49 };
50 entries.push(ListEntry { value, timestamp });
51 }
52 other => {
53 return Err(Error::InvalidValue(format!(
54 "invalid list entry type: expected string or object, got {other:?}"
55 )));
56 }
57 }
58 }
59
60 Ok(entries)
61}
62
63pub fn encode_entries(entries: &[ListEntry]) -> Result<String> {
65 Ok(serde_json::to_string(entries)?)
66}
67
68#[cfg_attr(not(feature = "internal"), allow(dead_code))]
70pub fn list_values_from_json(raw: &str) -> Result<Vec<String>> {
71 Ok(parse_entries(raw)?
72 .into_iter()
73 .map(|entry| entry.value)
74 .collect())
75}
76
77pub(crate) fn ensure_unique_timestamp(mut timestamp: i64, entries: &[ListEntry]) -> i64 {
79 if let Some(last) = entries.last() {
80 if timestamp <= last.timestamp {
81 timestamp = last.timestamp + 1;
82 }
83 }
84 timestamp
85}
86
87pub fn make_entry_name(entry: &ListEntry) -> String {
89 make_entry_name_from_parts(entry.timestamp, &entry.value)
90}
91
92pub(crate) fn make_entry_name_from_parts(timestamp: i64, value: &str) -> String {
94 let mut hasher = Sha1::new();
95 hasher.update(value.as_bytes());
96 let hash = format!("{:x}", hasher.finalize());
97 format!("{}-{}", timestamp, &hash[..5])
98}
99
100pub fn parse_timestamp_from_entry_name(name: &str) -> Option<i64> {
102 let idx = name.find('-')?;
103 name[..idx].parse().ok()
104}
105
106fn legacy_timestamp(idx: usize) -> i64 {
107 idx as i64
108}