kcode_kennedy_box_text_objects/
lib.rs1#![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}