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