1use serde::{Deserialize, Serialize};
13
14use crate::layout::{Point, Rect};
15
16#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
18#[serde(transparent)]
19pub struct NodeId(String);
20
21#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
23#[serde(transparent)]
24pub struct ClaimId(String);
25
26impl NodeId {
27 pub fn new(id: impl Into<String>) -> Self {
29 Self(id.into())
30 }
31
32 pub fn as_str(&self) -> &str {
34 &self.0
35 }
36
37 pub fn is_canonical(&self) -> bool {
39 is_canonical_id(&self.0, 'N')
40 }
41}
42
43impl ClaimId {
44 pub fn new(id: impl Into<String>) -> Self {
46 Self(id.into())
47 }
48
49 pub fn as_str(&self) -> &str {
51 &self.0
52 }
53
54 pub fn is_canonical(&self) -> bool {
56 is_canonical_id(&self.0, 'C')
57 }
58}
59
60impl std::fmt::Display for NodeId {
61 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62 f.write_str(&self.0)
63 }
64}
65
66impl std::fmt::Display for ClaimId {
67 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
68 f.write_str(&self.0)
69 }
70}
71
72pub(crate) fn is_canonical_id(s: &str, prefix: char) -> bool {
75 let mut chars = s.chars();
76 match chars.next() {
77 Some(c) if c == prefix => {}
78 _ => return false,
79 }
80 let rest = chars.as_str();
81 !rest.is_empty() && rest.bytes().all(|b| b.is_ascii_digit())
82}
83
84#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
86pub struct Manifest {
87 pub nodes: Vec<Node>,
89 pub links: Vec<Link>,
91 pub bindings: Vec<Binding>,
93 pub claims: Vec<Claim>,
95 #[serde(skip_serializing_if = "Option::is_none")]
97 pub bounds: Option<Rect>,
98}
99
100#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
102pub struct Node {
103 pub id: NodeId,
105 pub kind: NodeKind,
107 pub label: Option<String>,
109 pub support_level: Option<String>,
111 pub source_refs: Vec<String>,
113 pub description: Option<String>,
115 pub fields: NodeFields,
117 pub evidence_notes: Vec<String>,
119 #[serde(skip_serializing_if = "Option::is_none")]
121 pub pos: Option<Point>,
122}
123
124#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
126#[serde(rename_all = "snake_case")]
127pub enum NodeKind {
128 Question,
129 Experiment,
130 Decision,
131 DeadEnd,
132 Insight,
133 Other(String),
135}
136
137#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
139#[serde(rename_all = "snake_case")]
140pub enum NodeFields {
141 Question,
142 Experiment {
143 result: Option<String>,
144 },
145 Decision {
146 choice: Option<String>,
147 alternatives: Vec<String>,
148 rationale: Option<String>,
149 },
150 DeadEnd {
151 why_failed: Option<String>,
152 },
153 Insight,
154 Other,
156}
157
158#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
160pub struct Link {
161 pub from: NodeId,
162 pub to: NodeId,
163 pub kind: LinkKind,
164}
165
166#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
168#[serde(rename_all = "snake_case")]
169pub enum LinkKind {
170 Child,
172 DependsOn,
174}
175
176#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
178pub struct Binding {
179 pub node: NodeId,
180 pub claim: ClaimId,
181 pub role: BindingRole,
182}
183
184#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
189#[serde(rename_all = "snake_case")]
190#[non_exhaustive]
191pub enum BindingRole {
192 Evidence,
194}
195
196#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
198pub struct Claim {
199 pub id: ClaimId,
200 pub title: String,
201 pub statement: Option<String>,
202 pub status: Option<String>,
203 pub proof: Vec<String>,
205 pub deps: Vec<ClaimId>,
207}
208
209#[cfg(test)]
210mod tests {
211 use super::*;
212
213 #[test]
214 fn canonical_id_grammar() {
215 assert!(is_canonical_id("N01", 'N'));
216 assert!(is_canonical_id("N7", 'N'));
217 assert!(is_canonical_id("C123", 'C'));
218 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'));
223 }
224
225 #[test]
226 fn id_accessors_and_display() {
227 let n = NodeId::new("N01");
228 assert_eq!(n.as_str(), "N01");
229 assert_eq!(n.to_string(), "N01");
230 assert!(n.is_canonical());
231 assert!(!NodeId::new("nope").is_canonical());
232 assert!(ClaimId::new("C02").is_canonical());
233 }
234}