Skip to main content

kcode_kennedy_box_text_objects/
lib.rs

1//! Stages canonical Kennedy session box text as pending objects.
2
3#![forbid(unsafe_code)]
4
5use anyhow::Context as _;
6use kcode_session_history::{
7    Session,
8    chatend::{BoxId, EventId, PendingId},
9};
10use serde_json::{Value, json};
11
12const BOX_TEXT_OBJECT_SOURCE: &str = "kennedy-box-text";
13const BOX_TEXT_MEDIA_TYPE: &str = "text/plain; charset=utf-8";
14
15#[derive(Debug, Eq, PartialEq)]
16struct BoxTextObject {
17    box_id: BoxId,
18    pending_id: PendingId,
19    reused: bool,
20}
21
22pub fn stage_box_text_objects(
23    journal: &mut Session,
24    box_ids: &[BoxId],
25    recorded_at: &str,
26) -> anyhow::Result<String> {
27    let selections = box_ids
28        .iter()
29        .map(|box_id| {
30            let state = journal
31                .state()
32                .box_state(*box_id)
33                .with_context(|| format!("box {box_id} does not exist"))?;
34            anyhow::ensure!(state.active, "box {box_id} is not active");
35            anyhow::ensure!(
36                !state.canonical.content.text.is_empty(),
37                "box {box_id} has no text content"
38            );
39            Ok((
40                *box_id,
41                state.name.clone(),
42                state.canonical.event_id,
43                state.canonical.content.text.clone(),
44            ))
45        })
46        .collect::<anyhow::Result<Vec<_>>>()?;
47
48    let mut objects = Vec::with_capacity(selections.len());
49    for (box_id, box_name, canonical_event_id, text) in selections {
50        if let Some(pending_id) = existing_box_text_object(journal, box_id, canonical_event_id) {
51            objects.push(BoxTextObject {
52                box_id,
53                pending_id,
54                reused: true,
55            });
56            continue;
57        }
58        let pending_id = journal.stage_object(
59            recorded_at,
60            BOX_TEXT_MEDIA_TYPE,
61            Some(format!("box-{box_id}.txt")),
62            json!({
63                "source":BOX_TEXT_OBJECT_SOURCE,
64                "boxId":box_id.0,
65                "boxName":box_name,
66                "canonicalEventId":canonical_event_id.0,
67            }),
68            text.as_bytes(),
69        )?;
70        objects.push(BoxTextObject {
71            box_id,
72            pending_id,
73            reused: false,
74        });
75    }
76    Ok(render_box_text_objects(&objects))
77}
78
79fn existing_box_text_object(
80    journal: &Session,
81    box_id: BoxId,
82    canonical_event_id: EventId,
83) -> Option<PendingId> {
84    journal
85        .objects()
86        .iter()
87        .find(|(_, location)| {
88            let transport = &location.metadata.transport;
89            transport.get("source").and_then(Value::as_str) == Some(BOX_TEXT_OBJECT_SOURCE)
90                && transport.get("boxId").and_then(Value::as_u64) == Some(box_id.0)
91                && transport.get("canonicalEventId").and_then(Value::as_u64)
92                    == Some(canonical_event_id.0)
93        })
94        .map(|(pending_id, _)| pending_id.clone())
95}
96
97fn render_box_text_objects(objects: &[BoxTextObject]) -> String {
98    let mut text = String::from("Box text objects:");
99    for object in objects {
100        text.push_str(&format!("\nBox {}: {}", object.box_id, object.pending_id));
101        if object.reused {
102            text.push_str(" (already staged)");
103        }
104    }
105    text.push_str(
106        "\nThese pending object references resolve to canonical object IDs when the logical session commits.",
107    );
108    text
109}