Skip to main content

git_meta_lib/
session_handle.rs

1use crate::error::{Error, Result};
2use crate::session::Session;
3use crate::types::{MetaValue, Target, ValueType};
4use serde::{de::DeserializeOwned, Serialize};
5use serde_json::{Map, Value};
6
7/// A scoped handle for operations on a specific target within a session.
8///
9/// Created via [`Session::target()`]. Carries the target, email, and
10/// timestamp from the session so callers never have to pass them.
11///
12/// # Example
13///
14/// ```ignore
15/// let session = Session::discover()?;
16/// let handle = session.target(&Target::parse("commit:abc123")?);
17/// handle.set("agent:model", "claude")?;
18/// let val = handle.get_value("agent:model")?;
19/// ```
20pub struct SessionTargetHandle<'a> {
21    session: &'a Session,
22    target: Target,
23}
24
25impl<'a> SessionTargetHandle<'a> {
26    pub(crate) fn new(session: &'a Session, target: Target) -> Self {
27        Self { session, target }
28    }
29
30    /// Get a metadata value by key.
31    pub fn get_value(&self, key: &str) -> Result<Option<MetaValue>> {
32        self.session.store.get_value(&self.target, key)
33    }
34
35    /// Set a metadata value with convenience conversion.
36    ///
37    /// Accepts anything that converts to [`MetaValue`]: `&str`, `String`,
38    /// `Vec<ListEntry>`, `BTreeSet<String>`, or `MetaValue` directly.
39    ///
40    /// ```ignore
41    /// handle.set("key", "hello")?;                    // string
42    /// handle.set("key", MetaValue::String("hello".into()))?; // explicit
43    /// ```
44    ///
45    /// Uses the session's email and timestamp automatically.
46    pub fn set(&self, key: &str, value: impl Into<MetaValue>) -> Result<()> {
47        let meta_value = value.into();
48        self.session.store.set_value(
49            &self.target,
50            key,
51            &meta_value,
52            self.session.email(),
53            self.session.now(),
54        )
55    }
56
57    /// Merge string metadata fields under a common key prefix.
58    ///
59    /// The record must serialize to a JSON object. Object field names, including
60    /// `serde` renames, become key suffixes. String values are written as
61    /// `prefix:field`.
62    ///
63    /// This is a partial update, not a replacement operation. Null fields are
64    /// skipped and existing keys are left untouched. This makes `Option<T>`
65    /// fields useful for patch-style records, but callers that need to clear a
66    /// field must remove that key explicitly.
67    ///
68    /// ```ignore
69    /// #[derive(serde::Serialize)]
70    /// #[serde(rename_all = "kebab-case")]
71    /// struct Source<'a> {
72    ///     agent: &'a str,
73    ///     tool_version: Option<&'a str>,
74    /// }
75    ///
76    /// handle.set_record("agent-session:abc:source:def", &Source {
77    ///     agent: "codex",
78    ///     tool_version: Some("1.2.3"),
79    /// })?;
80    ///
81    /// // Later updates that serialize `tool_version` as null do not remove the
82    /// // existing `agent-session:abc:source:def:tool-version` key.
83    /// handle.set_record("agent-session:abc:source:def", &Source {
84    ///     agent: "codex",
85    ///     tool_version: None,
86    /// })?;
87    /// ```
88    pub fn set_record(&self, prefix: &str, record: impl Serialize) -> Result<()> {
89        let Value::Object(fields) = serde_json::to_value(record)? else {
90            return Err(Error::InvalidValue(
91                "record metadata must serialize to a JSON object".to_string(),
92            ));
93        };
94
95        for (field, value) in fields {
96            match value {
97                Value::Null => {}
98                Value::String(value) => self.set(&format!("{prefix}:{field}"), value)?,
99                _ => {
100                    return Err(Error::InvalidValue(format!(
101                        "record metadata field '{field}' must serialize to a string or null"
102                    )));
103                }
104            }
105        }
106
107        Ok(())
108    }
109
110    /// Read string metadata fields under a common key prefix into a record.
111    ///
112    /// This is the read-side pair to [`set_record`](Self::set_record). Immediate
113    /// child keys like `prefix:field` become JSON object fields before being
114    /// deserialized into `T`. Missing records return `Ok(None)`. Nested keys such
115    /// as `prefix:child:field` are ignored because they belong to a deeper
116    /// metadata subtree, not this record.
117    ///
118    /// Because [`set_record`](Self::set_record) leaves null or omitted fields
119    /// untouched, `get_record` reads the current merged field set under the
120    /// prefix, not the exact last value passed to `set_record`.
121    ///
122    /// # Errors
123    ///
124    /// Returns an error if an immediate child field exists but is not a string,
125    /// or if the collected fields do not deserialize into `T`.
126    pub fn get_record<T>(&self, prefix: &str) -> Result<Option<T>>
127    where
128        T: DeserializeOwned,
129    {
130        let field_prefix = format!("{prefix}:");
131        let mut fields = Map::new();
132
133        for (key, value) in self.get_all_values(Some(prefix))? {
134            let Some(field) = key.strip_prefix(&field_prefix) else {
135                continue;
136            };
137            if field.contains(':') {
138                continue;
139            }
140
141            match value {
142                MetaValue::String(value) => {
143                    fields.insert(field.to_string(), Value::String(value));
144                }
145                _ => {
146                    return Err(Error::InvalidValue(format!(
147                        "record metadata field '{field}' must be a string"
148                    )));
149                }
150            }
151        }
152
153        if fields.is_empty() {
154            return Ok(None);
155        }
156
157        serde_json::from_value(Value::Object(fields))
158            .map(Some)
159            .map_err(Into::into)
160    }
161
162    /// Remove a metadata key.
163    ///
164    /// Uses the session's email and timestamp automatically.
165    pub fn remove(&self, key: &str) -> Result<bool> {
166        self.session
167            .store
168            .remove(&self.target, key, self.session.email(), self.session.now())
169    }
170
171    /// Push a value onto a list.
172    ///
173    /// Uses the session's email and timestamp automatically.
174    pub fn list_push(&self, key: &str, value: &str) -> Result<()> {
175        self.session.store.list_push(
176            &self.target,
177            key,
178            value,
179            self.session.email(),
180            self.session.now(),
181        )
182    }
183
184    /// Apply list/set edits in one transaction.
185    ///
186    /// Empty edit batches are no-ops. If any edit fails, none of the batch is
187    /// committed. The session email and timestamp are used for every edit.
188    pub fn apply_edits<'b>(
189        &self,
190        edits: impl IntoIterator<Item = crate::MetaEdit<'b>>,
191    ) -> Result<()> {
192        self.session.store.apply_edits(
193            &self.target,
194            edits,
195            self.session.email(),
196            self.session.now(),
197        )
198    }
199
200    /// Pop a value from a list.
201    ///
202    /// Uses the session's email and timestamp automatically.
203    pub fn list_pop(&self, key: &str, value: &str) -> Result<()> {
204        self.session.store.list_pop(
205            &self.target,
206            key,
207            value,
208            self.session.email(),
209            self.session.now(),
210        )
211    }
212
213    /// Remove a list entry by index.
214    ///
215    /// Uses the session's email and timestamp automatically.
216    pub fn list_remove(&self, key: &str, index: usize) -> Result<()> {
217        self.session.store.list_remove(
218            &self.target,
219            key,
220            index,
221            self.session.email(),
222            self.session.now(),
223        )
224    }
225
226    /// Add a member to a set.
227    ///
228    /// Uses the session's email and timestamp automatically.
229    pub fn set_add(&self, key: &str, value: &str) -> Result<()> {
230        self.session.store.set_add(
231            &self.target,
232            key,
233            value,
234            self.session.email(),
235            self.session.now(),
236        )
237    }
238
239    /// Remove a member from a set.
240    ///
241    /// Uses the session's email and timestamp automatically.
242    pub fn set_remove(&self, key: &str, value: &str) -> Result<()> {
243        self.session.store.set_remove(
244            &self.target,
245            key,
246            value,
247            self.session.email(),
248            self.session.now(),
249        )
250    }
251
252    /// The target this handle is scoped to.
253    pub fn target(&self) -> &Target {
254        &self.target
255    }
256
257    /// Get all metadata for this target as typed (key, value) pairs.
258    ///
259    /// Optionally filters by key prefix (e.g., `Some("agent")` returns
260    /// all keys starting with `agent` or `agent:`).
261    ///
262    /// # Parameters
263    ///
264    /// - `prefix`: optional key prefix to filter by
265    ///
266    /// # Returns
267    ///
268    /// A vector of `(key, MetaValue)` pairs for matching metadata entries.
269    ///
270    /// # Errors
271    ///
272    /// Returns an error if the database read or deserialization fails.
273    pub fn get_all_values(&self, prefix: Option<&str>) -> Result<Vec<(String, MetaValue)>> {
274        let entries = self.session.store.get_all(&self.target, prefix)?;
275        let mut result = Vec::with_capacity(entries.len());
276        for entry in entries {
277            let meta_value = match entry.value_type {
278                ValueType::String => {
279                    let s: String =
280                        serde_json::from_str(&entry.value).unwrap_or_else(|_| entry.value.clone());
281                    MetaValue::String(s)
282                }
283                ValueType::List => {
284                    let entries = crate::list_value::parse_entries(&entry.value)?;
285                    MetaValue::List(entries)
286                }
287                ValueType::Set => {
288                    let members: Vec<String> = serde_json::from_str(&entry.value)?;
289                    MetaValue::Set(members.into_iter().collect())
290                }
291            };
292            result.push((entry.key, meta_value));
293        }
294        Ok(result)
295    }
296
297    /// Get list entries for a key on this target.
298    ///
299    /// # Parameters
300    ///
301    /// - `key`: the metadata key name
302    ///
303    /// # Returns
304    ///
305    /// A vector of [`ListEntry`](crate::list_value::ListEntry) values with
306    /// resolved content and timestamps.
307    ///
308    /// # Errors
309    ///
310    /// Returns an error if the key is missing, the value is not a list, or
311    /// the database read fails.
312    pub fn list_entries(&self, key: &str) -> Result<Vec<crate::list_value::ListEntry>> {
313        self.session.store.list_entries(&self.target, key)
314    }
315
316    /// Get authorship info (last author email and timestamp) for a key on this target.
317    ///
318    /// # Parameters
319    ///
320    /// - `key`: the metadata key name
321    ///
322    /// # Returns
323    ///
324    /// `Some(Authorship)` if the key has been modified at least once,
325    /// `None` otherwise.
326    ///
327    /// # Errors
328    ///
329    /// Returns an error if the database read fails.
330    pub fn get_authorship(&self, key: &str) -> Result<Option<crate::db::types::Authorship>> {
331        self.session.store.get_authorship(&self.target, key)
332    }
333}