git_meta_lib/
list_value.rs1use serde::{Deserialize, Serialize};
4use serde_json::Value;
5use sha1::{Digest, Sha1};
6
7use crate::error::{Error, Result};
8
9#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
11pub struct ListEntry {
12 pub value: String,
14 pub timestamp: i64,
16}
17
18pub 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
67pub fn encode_entries(entries: &[ListEntry]) -> Result<String> {
69 Ok(serde_json::to_string(entries)?)
70}
71
72#[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#[must_use]
83pub fn make_entry_name(entry: &ListEntry) -> String {
84 make_entry_name_from_parts(entry.timestamp, &entry.value)
85}
86
87pub(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#[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}