kcode_kennedy_kweb_loader/
lib.rs1#![forbid(unsafe_code)]
4
5use anyhow::Context as _;
6use kcode_kweb_context::{Connection, Context, LoadReport, Node};
7use kcode_kweb_db::{Node as KwebNode, NodeId, Owner};
8use kcode_kweb_manager::KwebManager;
9use serde_json::{Value, json};
10
11pub fn load_durable_batch(
14 manager: &KwebManager,
15 context: &mut Context,
16 durable_ids: &[String],
17) -> anyhow::Result<Value> {
18 load_batch_with(context, durable_ids, |preview, durable_id| {
19 load_one(manager, preview, durable_id)
20 })
21}
22
23pub fn node_from_value(value: &Value) -> anyhow::Result<Node> {
25 Node::from_kweb_value(value).map_err(anyhow::Error::new)
26}
27
28fn load_batch_with(
29 context: &mut Context,
30 durable_ids: &[String],
31 mut load: impl FnMut(&mut Context, &str) -> anyhow::Result<LoadReport>,
32) -> anyhow::Result<Value> {
33 let mut preview = context.clone();
34 let mut reports = Vec::with_capacity(durable_ids.len());
35 for durable_id in durable_ids {
36 reports.push(load(&mut preview, durable_id)?);
37 }
38 *context = preview;
39 Ok(json!({"loads": reports}))
40}
41
42fn load_one(
43 manager: &KwebManager,
44 context: &mut Context,
45 durable_id: &str,
46) -> anyhow::Result<LoadReport> {
47 let requested = read_node(manager, durable_id)?;
48 anyhow::ensure!(
49 requested.id.to_string() == durable_id,
50 "Kweb returned node {} when {durable_id} was requested",
51 requested.id
52 );
53 let fixed_ids = requested.data.fixed_connections.clone();
54 let requested = node_from_kweb(manager, requested)?;
55 let fixed = fixed_ids
56 .into_iter()
57 .filter(|id| id.to_string() != durable_id)
58 .map(|id| read_node(manager, &id.to_string()))
59 .map(|node| node.and_then(|node| node_from_kweb(manager, node)))
60 .collect::<anyhow::Result<Vec<_>>>()?;
61 context
62 .apply_load(requested, fixed)
63 .map_err(anyhow::Error::new)
64}
65
66fn read_node(manager: &KwebManager, durable_id: &str) -> anyhow::Result<KwebNode> {
67 let node_id = durable_id
68 .parse::<NodeId>()
69 .with_context(|| format!("{durable_id:?} is not a canonical node ID"))?;
70 manager.get_node(node_id).map_err(anyhow::Error::new)
71}
72
73fn node_from_kweb(manager: &KwebManager, node: KwebNode) -> anyhow::Result<Node> {
74 let fixed_connections = connections(manager, &node.data.fixed_connections)?;
75 let recent_connections = connections(manager, &node.data.recent_connections)?;
76 let owner = owner_text(node.data.owner, &node.id);
77 Ok(Node {
78 id: node.id.to_string(),
79 short_name: node.data.short_name,
80 short_description: node.data.short_description,
81 long_description: node.data.long_description,
82 owner,
83 fixed_connections,
84 recent_connections,
85 objects: node.data.objects.iter().map(ToString::to_string).collect(),
86 last_modified_by: node.last_author,
87 last_modified_at: Some(node.committed_at.to_rfc3339()),
88 })
89}
90
91fn owner_text(owner: Owner, node_id: &NodeId) -> String {
92 match owner {
93 Owner::Unowned => "unowned".into(),
94 Owner::SelfNode => node_id.to_string(),
95 Owner::Node(id) => id.to_string(),
96 }
97}
98
99fn connections(manager: &KwebManager, ids: &[NodeId]) -> anyhow::Result<Vec<Connection>> {
100 ids.iter()
101 .map(|id| read_node(manager, &id.to_string()))
102 .map(|node| {
103 node.map(|node| {
104 connection_from_parts(node.id, node.data.short_name, node.data.short_description)
105 })
106 })
107 .collect()
108}
109
110fn connection_from_parts(id: NodeId, short_name: String, short_description: String) -> Connection {
111 Connection {
112 id: id.to_string(),
113 short_name,
114 short_description,
115 }
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121
122 fn context_node(id: &str) -> Node {
123 Node {
124 id: id.into(),
125 short_name: "Node".into(),
126 short_description: "Description".into(),
127 long_description: "Long description".into(),
128 owner: "unowned".into(),
129 fixed_connections: Vec::new(),
130 recent_connections: Vec::new(),
131 objects: Vec::new(),
132 last_modified_by: "test".into(),
133 last_modified_at: None,
134 }
135 }
136
137 #[test]
138 fn converts_owner_and_connection_projection() {
139 let node_id = "AAAAAAAE".parse::<NodeId>().unwrap();
140 let owner_id = "AAAAAAAI".parse::<NodeId>().unwrap();
141
142 assert_eq!(owner_text(Owner::Unowned, &node_id), "unowned");
143 assert_eq!(owner_text(Owner::SelfNode, &node_id), "AAAAAAAE");
144 assert_eq!(owner_text(Owner::Node(owner_id), &node_id), "AAAAAAAI");
145
146 let connection = connection_from_parts(node_id, "Short".into(), "Description".into());
147 assert_eq!(connection.id, "AAAAAAAE");
148 assert_eq!(connection.short_name, "Short");
149 assert_eq!(connection.short_description, "Description");
150 }
151
152 #[test]
153 fn failed_batch_does_not_partially_update_context() {
154 let first = "AAAAAAAE".to_owned();
155 let second = "AAAAAAAI".to_owned();
156 let mut context = Context::new(vec![first.clone()]).unwrap();
157
158 let result = load_batch_with(
159 &mut context,
160 &[first.clone(), second.clone()],
161 |preview, durable_id| {
162 if durable_id == second {
163 anyhow::bail!("simulated second read failure");
164 }
165 preview
166 .apply_load(context_node(durable_id), Vec::new())
167 .map_err(anyhow::Error::new)
168 },
169 );
170
171 assert!(result.is_err());
172 assert!(!context.contains_full_node(&first));
173 }
174}