#![forbid(unsafe_code)]
use anyhow::Context as _;
use kcode_kweb_context::{Connection, Context, LoadReport, Node};
use kcode_kweb_db::{Node as KwebNode, NodeId, Owner};
use kcode_kweb_manager::KwebManager;
use serde_json::{Value, json};
pub fn load_durable_batch(
manager: &KwebManager,
context: &mut Context,
durable_ids: &[String],
) -> anyhow::Result<Value> {
load_batch_with(context, durable_ids, |preview, durable_id| {
load_one(manager, preview, durable_id)
})
}
pub fn node_from_value(value: &Value) -> anyhow::Result<Node> {
Node::from_kweb_value(value).map_err(anyhow::Error::new)
}
fn load_batch_with(
context: &mut Context,
durable_ids: &[String],
mut load: impl FnMut(&mut Context, &str) -> anyhow::Result<LoadReport>,
) -> anyhow::Result<Value> {
let mut preview = context.clone();
let mut reports = Vec::with_capacity(durable_ids.len());
for durable_id in durable_ids {
reports.push(load(&mut preview, durable_id)?);
}
*context = preview;
Ok(json!({"loads": reports}))
}
fn load_one(
manager: &KwebManager,
context: &mut Context,
durable_id: &str,
) -> anyhow::Result<LoadReport> {
let requested = read_node(manager, durable_id)?;
anyhow::ensure!(
requested.id.to_string() == durable_id,
"Kweb returned node {} when {durable_id} was requested",
requested.id
);
let fixed_ids = requested.data.fixed_connections.clone();
let requested = node_from_kweb(manager, requested)?;
let fixed = fixed_ids
.into_iter()
.filter(|id| id.to_string() != durable_id)
.map(|id| read_node(manager, &id.to_string()))
.map(|node| node.and_then(|node| node_from_kweb(manager, node)))
.collect::<anyhow::Result<Vec<_>>>()?;
context
.apply_load(requested, fixed)
.map_err(anyhow::Error::new)
}
fn read_node(manager: &KwebManager, durable_id: &str) -> anyhow::Result<KwebNode> {
let node_id = durable_id
.parse::<NodeId>()
.with_context(|| format!("{durable_id:?} is not a canonical node ID"))?;
manager.get_node(node_id).map_err(anyhow::Error::new)
}
fn node_from_kweb(manager: &KwebManager, node: KwebNode) -> anyhow::Result<Node> {
let fixed_connections = connections(manager, &node.data.fixed_connections)?;
let recent_connections = connections(manager, &node.data.recent_connections)?;
let owner = owner_text(node.data.owner, &node.id);
Ok(Node {
id: node.id.to_string(),
short_name: node.data.short_name,
short_description: node.data.short_description,
long_description: node.data.long_description,
owner,
fixed_connections,
recent_connections,
objects: node.data.objects.iter().map(ToString::to_string).collect(),
last_modified_by: node.last_author,
last_modified_at: Some(node.committed_at.to_rfc3339()),
})
}
fn owner_text(owner: Owner, node_id: &NodeId) -> String {
match owner {
Owner::Unowned => "unowned".into(),
Owner::SelfNode => node_id.to_string(),
Owner::Node(id) => id.to_string(),
}
}
fn connections(manager: &KwebManager, ids: &[NodeId]) -> anyhow::Result<Vec<Connection>> {
ids.iter()
.map(|id| read_node(manager, &id.to_string()))
.map(|node| {
node.map(|node| {
connection_from_parts(node.id, node.data.short_name, node.data.short_description)
})
})
.collect()
}
fn connection_from_parts(id: NodeId, short_name: String, short_description: String) -> Connection {
Connection {
id: id.to_string(),
short_name,
short_description,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn context_node(id: &str) -> Node {
Node {
id: id.into(),
short_name: "Node".into(),
short_description: "Description".into(),
long_description: "Long description".into(),
owner: "unowned".into(),
fixed_connections: Vec::new(),
recent_connections: Vec::new(),
objects: Vec::new(),
last_modified_by: "test".into(),
last_modified_at: None,
}
}
#[test]
fn converts_owner_and_connection_projection() {
let node_id = "AAAAAAAE".parse::<NodeId>().unwrap();
let owner_id = "AAAAAAAI".parse::<NodeId>().unwrap();
assert_eq!(owner_text(Owner::Unowned, &node_id), "unowned");
assert_eq!(owner_text(Owner::SelfNode, &node_id), "AAAAAAAE");
assert_eq!(owner_text(Owner::Node(owner_id), &node_id), "AAAAAAAI");
let connection = connection_from_parts(node_id, "Short".into(), "Description".into());
assert_eq!(connection.id, "AAAAAAAE");
assert_eq!(connection.short_name, "Short");
assert_eq!(connection.short_description, "Description");
}
#[test]
fn failed_batch_does_not_partially_update_context() {
let first = "AAAAAAAE".to_owned();
let second = "AAAAAAAI".to_owned();
let mut context = Context::new(vec![first.clone()]).unwrap();
let result = load_batch_with(
&mut context,
&[first.clone(), second.clone()],
|preview, durable_id| {
if durable_id == second {
anyhow::bail!("simulated second read failure");
}
preview
.apply_load(context_node(durable_id), Vec::new())
.map_err(anyhow::Error::new)
},
);
assert!(result.is_err());
assert!(!context.contains_full_node(&first));
}
}