1use super::ExperimentError;
7use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
11pub struct Orcid(String);
12
13impl Orcid {
14 pub fn new(orcid: impl Into<String>) -> Result<Self, ExperimentError> {
16 let orcid = orcid.into();
17 if Self::is_valid_orcid(&orcid) {
20 Ok(Self(orcid))
21 } else {
22 Err(ExperimentError::InvalidOrcid(orcid))
23 }
24 }
25
26 fn is_valid_orcid(orcid: &str) -> bool {
28 let parts: Vec<&str> = orcid.split('-').collect();
29 if parts.len() != 4 {
30 return false;
31 }
32 for part in parts.iter().take(3) {
34 if part.len() != 4 || !part.chars().all(|c| c.is_ascii_digit()) {
35 return false;
36 }
37 }
38 let last = parts[3];
40 if last.len() != 4 {
41 return false;
42 }
43 let chars: Vec<char> = last.chars().collect();
44 chars.iter().take(3).all(|c| c.is_ascii_digit())
45 && (chars[3].is_ascii_digit() || chars[3] == 'X')
46 }
47
48 pub fn as_str(&self) -> &str {
50 &self.0
51 }
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
56pub enum CreditRole {
57 Conceptualization,
58 DataCuration,
59 FormalAnalysis,
60 FundingAcquisition,
61 Investigation,
62 Methodology,
63 ProjectAdministration,
64 Resources,
65 Software,
66 Supervision,
67 Validation,
68 Visualization,
69 WritingOriginalDraft,
70 WritingReviewEditing,
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct ResearchContributor {
76 pub name: String,
78 pub orcid: Option<Orcid>,
80 pub affiliation: String,
82 pub roles: Vec<CreditRole>,
84 pub email: Option<String>,
86}
87
88#[derive(Debug, Clone, Serialize, Deserialize)]
90pub struct ResearchArtifact {
91 pub title: String,
93 pub abstract_text: String,
95 pub contributors: Vec<ResearchContributor>,
97 pub keywords: Vec<String>,
99 pub doi: Option<String>,
101 pub arxiv_id: Option<String>,
103 pub license: String,
105 pub created_at: String,
107 pub datasets: Vec<String>,
109 pub code_repositories: Vec<String>,
111 pub pre_registration: Option<PreRegistration>,
113}
114
115#[derive(Debug, Clone, Serialize, Deserialize)]
117pub struct PreRegistration {
118 pub timestamp: String,
120 pub signature: String,
122 pub public_key: String,
124 pub hypotheses_hash: String,
126 pub registry: String,
128 pub registration_id: String,
130}
131
132#[derive(Debug, Clone, Serialize, Deserialize)]
134pub struct CitationMetadata {
135 pub citation_type: CitationType,
137 pub title: String,
139 pub authors: Vec<String>,
141 pub year: u16,
143 pub month: Option<u8>,
145 pub doi: Option<String>,
147 pub url: Option<String>,
149 pub venue: Option<String>,
151 pub volume: Option<String>,
153 pub pages: Option<String>,
155 pub publisher: Option<String>,
157 pub version: Option<String>,
159}
160
161#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
163pub enum CitationType {
164 Article,
165 InProceedings,
166 Book,
167 Software,
168 Dataset,
169 Misc,
170}
171
172impl CitationMetadata {
173 pub fn to_bibtex(&self, key: &str) -> String {
175 let type_str = match self.citation_type {
176 CitationType::Article => "article",
177 CitationType::InProceedings => "inproceedings",
178 CitationType::Book => "book",
179 CitationType::Software => "software",
180 CitationType::Dataset => "dataset",
181 CitationType::Misc => "misc",
182 };
183
184 let mut bibtex = format!("@{}{{{},\n", type_str, key);
185 bibtex.push_str(&format!(" title = {{{}}},\n", self.title));
186 bibtex.push_str(&format!(" author = {{{}}},\n", self.authors.join(" and ")));
187 bibtex.push_str(&format!(" year = {{{}}},\n", self.year));
188
189 if let Some(month) = self.month {
190 bibtex.push_str(&format!(" month = {{{}}},\n", month));
191 }
192 if let Some(ref doi) = self.doi {
193 bibtex.push_str(&format!(" doi = {{{}}},\n", doi));
194 }
195 if let Some(ref url) = self.url {
196 bibtex.push_str(&format!(" url = {{{}}},\n", url));
197 }
198 if let Some(ref venue) = self.venue {
199 let field = match self.citation_type {
200 CitationType::Article => "journal",
201 CitationType::InProceedings => "booktitle",
202 _ => "howpublished",
203 };
204 bibtex.push_str(&format!(" {} = {{{}}},\n", field, venue));
205 }
206 if let Some(ref volume) = self.volume {
207 bibtex.push_str(&format!(" volume = {{{}}},\n", volume));
208 }
209 if let Some(ref pages) = self.pages {
210 bibtex.push_str(&format!(" pages = {{{}}},\n", pages));
211 }
212 if let Some(ref publisher) = self.publisher {
213 bibtex.push_str(&format!(" publisher = {{{}}},\n", publisher));
214 }
215 if let Some(ref version) = self.version {
216 bibtex.push_str(&format!(" version = {{{}}},\n", version));
217 }
218
219 bibtex.push('}');
220 bibtex
221 }
222
223 pub fn to_cff(&self) -> String {
225 let mut cff = String::from("cff-version: 1.2.0\n");
226 cff.push_str(&format!("title: \"{}\"\n", self.title));
227 cff.push_str("authors:\n");
228 for author in &self.authors {
229 cff.push_str(&format!(" - name: \"{}\"\n", author));
230 }
231 cff.push_str(&format!(
232 "date-released: \"{}-{:02}-01\"\n",
233 self.year,
234 self.month.unwrap_or(1)
235 ));
236
237 if let Some(ref version) = self.version {
238 cff.push_str(&format!("version: \"{}\"\n", version));
239 }
240 if let Some(ref doi) = self.doi {
241 cff.push_str(&format!("doi: \"{}\"\n", doi));
242 }
243 if let Some(ref url) = self.url {
244 cff.push_str(&format!("url: \"{}\"\n", url));
245 }
246
247 cff
248 }
249}