1use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
9#[serde(rename_all = "snake_case")]
10pub enum ProvenanceKind {
11 ReimplementedFromPaper,
13 ReimplementedFromPaperAndCode,
17 UpstreamWeights,
19 PortedFromCode,
21}
22
23#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
25pub struct Paper {
26 pub title: String,
27 pub authors: Vec<String>,
29 pub venue: String,
31 pub year: u16,
32 pub doi: String,
34 #[serde(default)]
36 pub license: Option<String>,
37}
38
39impl Paper {
40 pub fn doi_url(&self) -> String {
42 format!("https://doi.org/{}", self.doi)
43 }
44}
45
46#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
48pub struct UpstreamCode {
49 pub url: String,
50 pub license: Option<String>,
52 pub version: String,
54}
55
56#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
58pub struct ArtifactSource {
59 pub name: String,
61 pub url: String,
63 pub sha256: String,
65 pub size_bytes: u64,
66 #[serde(default)]
68 pub license: Option<String>,
69}
70
71#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
73pub struct Provenance {
74 pub name: String,
76 pub kind: ProvenanceKind,
77 pub paper: Option<Paper>,
79 pub code: UpstreamCode,
80 #[serde(default)]
82 pub artifacts: Vec<ArtifactSource>,
83 #[serde(default)]
85 pub notes: String,
86}
87
88impl Provenance {
89 pub fn citation(&self) -> String {
91 let code = match &self.code.license {
92 Some(l) => format!("Code: {} ({}, {l})", self.code.url, self.code.version),
93 None => format!("Code: {} ({})", self.code.url, self.code.version),
94 };
95 match &self.paper {
96 Some(p) => {
97 let authors = match p.authors.as_slice() {
98 [] => String::new(),
99 [one] => one.clone(),
100 [first, .., last] if p.authors.len() > 2 => format!("{first} … {last}"),
101 [first, second] => format!("{first} & {second}"),
102 _ => unreachable!(),
103 };
104 format!("{authors} ({}). {}. {}. {}. {code}", p.year, p.title, p.venue, p.doi_url())
105 }
106 None => format!("{} (no verified paper). {code}", self.name),
107 }
108 }
109}
110
111pub trait Attributed {
113 fn provenance(&self) -> Provenance;
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119
120 #[test]
121 fn citation_names_paper_doi_and_code() {
122 let p = Provenance {
123 name: "Kilosort4".into(),
124 kind: ProvenanceKind::ReimplementedFromPaper,
125 paper: Some(Paper {
126 title: "Spike sorting with Kilosort4".into(),
127 authors: vec!["Pachitariu".into(), "Sridhar".into(), "Pennington".into(), "Stringer".into()],
128 venue: "Nature Methods".into(),
129 year: 2024,
130 doi: "10.1038/s41592-024-02232-7".into(),
131 license: None,
132 }),
133 code: UpstreamCode { url: "https://github.com/MouseLand/Kilosort".into(), license: Some("GPL-3.0".into()), version: "v4.1.3".into() },
134 artifacts: Vec::new(),
135 notes: String::new(),
136 };
137 let c = p.citation();
138 assert!(c.starts_with("Pachitariu … Stringer (2024). Spike sorting with Kilosort4. Nature Methods. https://doi.org/10.1038/s41592-024-02232-7."), "{c}");
139 assert!(c.ends_with("Code: https://github.com/MouseLand/Kilosort (v4.1.3, GPL-3.0)"), "{c}");
140 }
141}