use serde::{Deserialize, Serialize};
use crate::layout::{Point, Rect};
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct NodeId(String);
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct ClaimId(String);
impl NodeId {
pub fn new(id: impl Into<String>) -> Self {
Self(id.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn is_canonical(&self) -> bool {
is_canonical_id(&self.0, 'N')
}
}
impl ClaimId {
pub fn new(id: impl Into<String>) -> Self {
Self(id.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn is_canonical(&self) -> bool {
is_canonical_id(&self.0, 'C')
}
}
impl std::fmt::Display for NodeId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl std::fmt::Display for ClaimId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
pub(crate) fn is_canonical_id(s: &str, prefix: char) -> bool {
let mut chars = s.chars();
match chars.next() {
Some(c) if c == prefix => {}
_ => return false,
}
let rest = chars.as_str();
!rest.is_empty() && rest.bytes().all(|b| b.is_ascii_digit())
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Manifest {
pub nodes: Vec<Node>,
pub links: Vec<Link>,
pub bindings: Vec<Binding>,
pub claims: Vec<Claim>,
#[serde(skip_serializing_if = "Option::is_none")]
pub bounds: Option<Rect>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Node {
pub id: NodeId,
pub kind: NodeKind,
pub label: Option<String>,
pub support_level: Option<String>,
pub source_refs: Vec<String>,
pub description: Option<String>,
pub fields: NodeFields,
pub evidence_notes: Vec<String>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub isolated: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub pos: Option<Point>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum NodeKind {
Question,
Experiment,
Decision,
DeadEnd,
Insight,
Other(String),
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum NodeFields {
Question,
Experiment {
result: Option<String>,
},
Decision {
choice: Option<String>,
alternatives: Vec<String>,
rationale: Option<String>,
},
DeadEnd {
why_failed: Option<String>,
},
Insight,
Other,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Link {
pub from: NodeId,
pub to: NodeId,
pub kind: LinkKind,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum LinkKind {
Child,
DependsOn,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Binding {
pub node: NodeId,
pub claim: ClaimId,
pub role: BindingRole,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum BindingRole {
Evidence,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Claim {
pub id: ClaimId,
pub title: String,
pub statement: Option<String>,
pub status: Option<String>,
pub proof: Vec<String>,
pub deps: Vec<ClaimId>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn canonical_id_grammar() {
assert!(is_canonical_id("N01", 'N'));
assert!(is_canonical_id("N7", 'N'));
assert!(is_canonical_id("C123", 'C'));
assert!(!is_canonical_id("N", 'N')); assert!(!is_canonical_id("n01", 'N')); assert!(!is_canonical_id("C01", 'N')); assert!(!is_canonical_id("N01a", 'N')); assert!(!is_canonical_id("", 'N'));
}
#[test]
fn id_accessors_and_display() {
let n = NodeId::new("N01");
assert_eq!(n.as_str(), "N01");
assert_eq!(n.to_string(), "N01");
assert!(n.is_canonical());
assert!(!NodeId::new("nope").is_canonical());
assert!(ClaimId::new("C02").is_canonical());
}
}