use serde_json::{Map, Value};
use crate::domain::entity::wiring::Wiring;
use crate::domain::entity::{PersonaId, Slot, Source};
use crate::domain::error::{DomainError, WireResult};
use crate::domain::graph::Node;
pub const WIRING_TYPE: &str = "outline_node";
pub const META_PERSONA: &str = "persona";
pub const META_SLOT: &str = "axis";
pub const META_SOURCE_URI: &str = "source_uri";
pub const META_MAINTENANCE_EXEMPT: &str = "maintenance_exempt";
pub fn extract_persona(node: &Node) -> Option<&str> {
node.metadata.get(META_PERSONA).and_then(Value::as_str)
}
pub fn extract_slot(node: &Node) -> Option<&str> {
node.metadata.get(META_SLOT).and_then(Value::as_str)
}
pub fn extract_source_uri(node: &Node) -> Option<&str> {
node.metadata.get(META_SOURCE_URI).and_then(Value::as_str)
}
pub fn extract_maintenance_exempt(node: &Node) -> bool {
node.metadata
.get(META_MAINTENANCE_EXEMPT)
.and_then(Value::as_bool)
.unwrap_or(false)
}
pub fn extract_slot_typed(node: &Node) -> WireResult<Option<Slot>> {
match extract_slot(node) {
Some(s) => Slot::new(s.to_string()).map(Some),
None => Ok(None),
}
}
pub fn node_to_wiring(
node: &Node,
projection_ref: Option<crate::domain::entity::ProjectionName>,
) -> WireResult<Wiring> {
let persona = extract_persona(node).ok_or_else(|| {
DomainError::InvalidMetadata(format!(
"wiring node '{}' missing metadata.{META_PERSONA}",
node.id
))
})?;
let slot = extract_slot(node).ok_or_else(|| {
DomainError::InvalidMetadata(format!(
"wiring node '{}' missing metadata.{META_SLOT}",
node.id
))
})?;
let source = extract_source_uri(node).ok_or_else(|| {
DomainError::InvalidMetadata(format!(
"wiring node '{}' missing metadata.{META_SOURCE_URI}",
node.id
))
})?;
Ok(Wiring::new(
PersonaId::new(persona.to_string())?,
Slot::new(slot.to_string())?,
Source::new(source.to_string())?,
projection_ref,
))
}
pub fn wiring_metadata_object(
persona: &PersonaId,
slot: &Slot,
source: &Source,
extras: Option<Map<String, Value>>,
) -> Value {
let mut m = Map::new();
m.insert(
META_PERSONA.to_string(),
Value::String(persona.as_str().into()),
);
m.insert(META_SLOT.to_string(), Value::String(slot.as_str().into()));
m.insert(
META_SOURCE_URI.to_string(),
Value::String(source.as_str().into()),
);
if let Some(extra) = extras {
for (k, v) in extra {
m.insert(k, v);
}
}
Value::Object(m)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::domain::entity::ProjectionName;
use serde_json::json;
fn raw_node(id: &str, metadata: Value) -> Node {
Node {
id: id.into(),
r#type: WIRING_TYPE.into(),
sot_ref: None,
confidence: None,
applicability: None,
last_verified_at: None,
review_due: None,
version: 1,
prev_id: None,
metadata,
}
}
#[test]
fn extract_helpers_read_canonical_keys() {
let n = raw_node(
"shi.mailbox",
json!({
"persona": "shi",
"axis": "mailbox",
"source_uri": "mini-app://mailbox?alias=for_shi",
"maintenance_exempt": true,
}),
);
assert_eq!(extract_persona(&n), Some("shi"));
assert_eq!(extract_slot(&n), Some("mailbox"));
assert_eq!(
extract_source_uri(&n),
Some("mini-app://mailbox?alias=for_shi")
);
assert!(extract_maintenance_exempt(&n));
}
#[test]
fn extract_maintenance_exempt_defaults_false() {
let n = raw_node("a.b", json!({}));
assert!(!extract_maintenance_exempt(&n));
}
#[test]
fn extract_slot_typed_validates_invariants() {
let bad = raw_node("a.b", json!({"axis": "a.b"}));
let err = extract_slot_typed(&bad).expect_err("dot must reject");
assert!(matches!(
err,
crate::domain::error::WireError::Domain(DomainError::InvalidMetadata(_))
));
}
#[test]
fn node_to_wiring_strict_round_trip() {
let persona = PersonaId::new("shi").unwrap();
let slot = Slot::new("mailbox").unwrap();
let source = Source::new("mini-app://mailbox?alias=for_shi").unwrap();
let meta = wiring_metadata_object(&persona, &slot, &source, None);
let node = raw_node("shi.mailbox", meta);
let projection_ref = Some(ProjectionName::new("shi.section.mailbox").unwrap());
let w = node_to_wiring(&node, projection_ref.clone()).unwrap();
assert_eq!(w.persona_id(), &persona);
assert_eq!(w.slot(), &slot);
assert_eq!(w.source(), &source);
assert_eq!(w.projection_ref(), projection_ref.as_ref());
assert_eq!(w.storage_node_id(), "shi.mailbox");
}
#[test]
fn node_to_wiring_rejects_missing_keys() {
let n = raw_node("x.y", json!({"persona": "x"}));
let err = node_to_wiring(&n, None).expect_err("missing axis must reject");
assert!(matches!(
err,
crate::domain::error::WireError::Domain(DomainError::InvalidMetadata(_))
));
}
#[test]
fn wiring_metadata_object_merges_extras() {
let persona = PersonaId::new("p").unwrap();
let slot = Slot::new("s").unwrap();
let source = Source::new("mini-app://x").unwrap();
let mut extras = Map::new();
extras.insert("maintenance_exempt".into(), json!(true));
let v = wiring_metadata_object(&persona, &slot, &source, Some(extras));
assert_eq!(v["persona"], "p");
assert_eq!(v["axis"], "s");
assert_eq!(v["source_uri"], "mini-app://x");
assert_eq!(v["maintenance_exempt"], true);
}
#[test]
fn node_to_wiring_rejects_missing_persona_and_source() {
let no_persona = raw_node(
"x.y",
json!({"axis": "mailbox", "source_uri": "mini-app://x"}),
);
let err = node_to_wiring(&no_persona, None).expect_err("missing persona must reject");
assert!(matches!(
err,
crate::domain::error::WireError::Domain(DomainError::InvalidMetadata(_))
));
assert!(err.to_string().contains("persona"));
let no_source = raw_node("x.y", json!({"persona": "x", "axis": "mailbox"}));
let err = node_to_wiring(&no_source, None).expect_err("missing source must reject");
assert!(matches!(
err,
crate::domain::error::WireError::Domain(DomainError::InvalidMetadata(_))
));
assert!(err.to_string().contains("source_uri"));
}
}