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>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub paper: Option<PaperMeta>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub related_work: Vec<RelatedWork>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub concepts: Vec<Concept>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub problem: Option<Problem>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub recipes: Vec<Recipe>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub exhibits: Vec<Exhibit>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub built_on: Vec<BuiltOn>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub node_exhibits: Vec<NodeExhibit>,
}
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
pub struct PaperMeta {
pub title: Option<String>,
pub authors: Vec<String>,
pub year: Option<String>,
pub venue: Option<String>,
pub doi: Option<String>,
#[serde(rename = "abstract")]
pub abstract_: Option<String>,
pub keywords: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RelatedWork {
pub id: String,
pub cite: String,
pub doi: Option<String>,
pub kind: Option<String>,
pub what_changed: Option<String>,
pub why: Option<String>,
pub adopted: Option<String>,
pub claims_affected: Vec<ClaimId>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Concept {
pub term: String,
pub notation: Option<String>,
pub definition: Option<String>,
pub boundary: Option<String>,
pub related: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Problem {
pub statement: Option<String>,
pub observations: Vec<String>,
pub gaps: Vec<String>,
pub insights: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Recipe {
pub name: String,
pub title: Option<String>,
pub body: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ExhibitKind {
Figure,
Table,
Other,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Exhibit {
pub id: String,
pub file: String,
pub kind: ExhibitKind,
pub source: Option<String>,
pub description: Option<String>,
pub claims: Vec<ClaimId>,
pub body: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BuiltOn {
pub node: NodeId,
pub related_work: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct NodeExhibit {
pub node: NodeId,
pub exhibit: String,
}
#[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,
Pivot,
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 {
hypothesis: Option<String>,
failure_mode: Option<String>,
lesson: Option<String>,
why_failed: Option<String>,
},
Insight,
Pivot {
from: Option<String>,
to: Option<String>,
trigger: Option<String>,
},
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());
}
}