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(default, skip_serializing_if = "std::ops::Not::not")]
124 pub isolated: bool,
125 #[serde(skip_serializing_if = "Option::is_none")]
127 pub pos: Option<Point>,
128}
129
130#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
132#[serde(rename_all = "snake_case")]
133pub enum NodeKind {
134 Question,
135 Experiment,
136 Decision,
137 DeadEnd,
138 Insight,
139 Other(String),
141}
142
143#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
145#[serde(rename_all = "snake_case")]
146pub enum NodeFields {
147 Question,
148 Experiment {
149 result: Option<String>,
150 },
151 Decision {
152 choice: Option<String>,
153 alternatives: Vec<String>,
154 rationale: Option<String>,
155 },
156 DeadEnd {
157 why_failed: Option<String>,
158 },
159 Insight,
160 Other,
162}
163
164#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
166pub struct Link {
167 pub from: NodeId,
168 pub to: NodeId,
169 pub kind: LinkKind,
170}
171
172#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
174#[serde(rename_all = "snake_case")]
175pub enum LinkKind {
176 Child,
178 DependsOn,
180}
181
182#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
184pub struct Binding {
185 pub node: NodeId,
186 pub claim: ClaimId,
187 pub role: BindingRole,
188}
189
190#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
195#[serde(rename_all = "snake_case")]
196#[non_exhaustive]
197pub enum BindingRole {
198 Evidence,
200}
201
202#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
204pub struct Claim {
205 pub id: ClaimId,
206 pub title: String,
207 pub statement: Option<String>,
208 pub status: Option<String>,
209 pub proof: Vec<String>,
211 pub deps: Vec<ClaimId>,
213}
214
215#[cfg(test)]
216mod tests {
217 use super::*;
218
219 #[test]
220 fn canonical_id_grammar() {
221 assert!(is_canonical_id("N01", 'N'));
222 assert!(is_canonical_id("N7", 'N'));
223 assert!(is_canonical_id("C123", 'C'));
224 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'));
229 }
230
231 #[test]
232 fn id_accessors_and_display() {
233 let n = NodeId::new("N01");
234 assert_eq!(n.as_str(), "N01");
235 assert_eq!(n.to_string(), "N01");
236 assert!(n.is_canonical());
237 assert!(!NodeId::new("nope").is_canonical());
238 assert!(ClaimId::new("C02").is_canonical());
239 }
240}