acorn_lib/schema/
validate.rs1use crate::constants::*;
6use crate::schema::pid::PersistentIdentifierConvert;
7use chrono::prelude::*;
8use convert_case::{Case, Casing};
9use uriparse::URI;
10use validator::ValidationError;
11
12pub fn format_phone_number(value: &str) -> Result<String, ValidationError> {
23    const MESSAGE: &str = "Unable to format telephone number";
24    match RE_PHONE.captures(value) {
25        | Ok(value) => match value {
26            | Some(captures) => {
27                let country_code = match captures.name("country") {
28                    | Some(value) => Some(value.as_str().trim().to_string()),
29                    | None => None,
30                };
31                let area_code = match captures.name("area") {
32                    | Some(value) => Some(value.as_str().replace("(", "").replace(")", "")),
33                    | None => None,
34                };
35                let prefix = match captures.name("prefix") {
36                    | Some(value) => Some(value.as_str().to_string()),
37                    | None => None,
38                };
39                let line = match captures.name("line") {
40                    | Some(value) => Some(value.as_str().to_string()),
41                    | None => None,
42                };
43                Ok([country_code, area_code, prefix, line]
44                    .into_iter()
45                    .flatten()
46                    .collect::<Vec<String>>()
47                    .join("."))
48            }
49            | None => Err(ValidationError::new("telephone").with_message(MESSAGE.into())),
50        },
51        | _ => Err(ValidationError::new("telephone").with_message(MESSAGE.into())),
52    }
53}
54pub fn has_image_extension(value: &str) -> Result<(), ValidationError> {
56    const MESSAGE: &str = "Please provide a path with a PNG, JPEG, GIF, WEBP, TIFF or SVG extension";
57    match RE_IMAGE_EXTENSION.is_match(value) {
58        | Ok(value) if value => Ok(()),
59        | _ => Err(ValidationError::new("image").with_message(MESSAGE.into())),
60    }
61}
62pub fn is_ark(value: &str) -> Result<(), ValidationError> {
64    const MESSAGE: &str = "Please provide a valid ARK";
65    match value.is_ark() {
66        | true => Ok(()),
67        | _ => Err(ValidationError::new("ark").with_message(MESSAGE.into())),
68    }
69}
70fn is_current_year(value: String) -> bool {
72    let now: DateTime<Utc> = Utc::now();
73    let year = now.year().to_string().parse::<i32>().unwrap_or_default();
74    value.parse::<i32>().unwrap_or_default() <= year
75}
76pub fn is_doi(value: &str) -> Result<(), ValidationError> {
78    const MESSAGE: &str = "Please provide a valid DOI, by itself and without domain or 'doi:' prefix.";
79    match value.is_doi() {
80        | true => Ok(()),
81        | _ => Err(ValidationError::new("doi").with_message(MESSAGE.into())),
82    }
83}
84pub fn is_ip6(value: &str) -> Result<(), ValidationError> {
86    const MESSAGE: &str = "Please provide a valid IP6 address";
87    match RE_IP6.is_match(value) {
88        | Ok(value) if value => Ok(()),
89        | _ => Err(ValidationError::new("IP6").with_message(MESSAGE.into())),
90    }
91}
92pub fn is_iso8601_date(value: &str) -> Result<(), ValidationError> {
96    const MESSAGE: &str = "Please provide a valid ISO 8601 date (e.g., YYYY-MM-DD)";
97    match RE_ISO_8601_DATE.is_match(value) {
99        | Ok(value) if value => Ok(()),
100        | _ => Err(ValidationError::new("ISO 8601 Date").with_message(MESSAGE.into())),
101    }
102}
103pub fn is_iso8601_year(value: &str) -> Result<(), ValidationError> {
107    const MESSAGE: &str = "Please provide a valid ISO 8601 year (e.g., YYYY)";
108    match RE_ISO_8601_YEAR.is_match(value) {
109        | Ok(x) if x && is_current_year(value.to_string()) => Ok(()),
110        | _ => Err(ValidationError::new("ISO 8601 Date").with_message(MESSAGE.into())),
111    }
112}
113pub fn is_kebabcase(value: &str) -> Result<(), ValidationError> {
115    const MESSAGE: &str = "Please provide an ID in kebab-case format";
116    match value.to_case(Case::Kebab).eq(&value) {
117        | true => Ok(()),
118        | _ => Err(ValidationError::new("kebabcase").with_message(MESSAGE.into())),
119    }
120}
121pub fn is_list_url(value: &[String]) -> Result<(), ValidationError> {
123    let is_valid = value.iter().all(|x| URI::try_from(x.as_str()).is_ok());
124    match is_valid {
125        | true => Ok(()),
126        | _ => Err(ValidationError::new("URLs").with_message("Every URL should be valid".to_string().into())),
127    }
128}
129pub fn is_orcid(value: &str) -> Result<(), ValidationError> {
131    const MESSAGE: &str = "Please provide a valid ORCiD";
132    match value.is_orcid() {
133        | true => Ok(()),
134        | _ => Err(ValidationError::new("orcid").with_message(MESSAGE.into())),
135    }
136}
137pub fn is_phone_number(value: &str) -> Result<(), ValidationError> {
141    const MESSAGE: &str = "Please provide a valid phone number";
142    let is_fake = match RE_FAKE_PHONE.is_match(value) {
143        | Ok(value) if value => true,
144        | _ => false,
145    };
146    match RE_PHONE.is_match(value) {
147        | Ok(value) if value && !is_fake => Ok(()),
148        | _ => Err(ValidationError::new("phone").with_message(MESSAGE.into())),
149    }
150}
151pub fn is_raid(value: &str) -> Result<(), ValidationError> {
153    const MESSAGE: &str = "Please provide a valid RAiD";
154    match RE_RAID.is_match(value) {
155        | Ok(value) if value => Ok(()),
156        | _ => Err(ValidationError::new("raid").with_message(MESSAGE.into())),
157    }
158}
159pub fn is_ror(value: &str) -> Result<(), ValidationError> {
163    const MESSAGE: &str = "Please provide a valid ROR";
164    match RE_ROR.is_match(value) {
165        | Ok(value) if value => Ok(()),
166        | _ => Err(ValidationError::new("ror").with_message(MESSAGE.into())),
167    }
168}
169pub fn is_unix_epoch(value: usize) -> Result<(), ValidationError> {
171    const MESSAGE: &str = "Please provide a valid Unix epoch timestamp";
172    match RE_UNIX_EPOCH.is_match(&value.to_string()) {
173        | Ok(value) if value => Ok(()),
174        | _ => Err(ValidationError::new("unix epoch").with_message(MESSAGE.into())),
175    }
176}
177pub fn validate_attribute_approach(value: &[String]) -> Result<(), ValidationError> {
179    const MAX_LENGTH: usize = MAX_LENGTH_APPROACH;
180    let message: String = format!("Each approach statement should be less than {MAX_LENGTH} characters");
181    let is_valid = value.iter().all(|x| x.len() <= MAX_LENGTH);
182    match is_valid {
183        | true => Ok(()),
184        | _ => Err(ValidationError::new("approach").with_message(message.into())),
185    }
186}
187pub fn validate_attribute_areas(value: &[String]) -> Result<(), ValidationError> {
189    const MAX_LENGTH: usize = MAX_LENGTH_RESEARCH_AREA;
190    let is_valid = value.iter().all(|x| x.len() <= MAX_LENGTH);
191    match is_valid {
192        | true => Ok(()),
193        | _ => Err(ValidationError::new("area").with_message(format!("Each area should be less than {MAX_LENGTH} characters").into())),
194    }
195}
196pub fn validate_attribute_capabilities(value: &[String]) -> Result<(), ValidationError> {
200    const MAX_LENGTH: usize = MAX_LENGTH_CAPABILIY;
201    let is_valid = value.iter().all(|x| x.len() <= MAX_LENGTH);
202    match is_valid {
203        | true => Ok(()),
204        | _ => Err(ValidationError::new("capability").with_message(format!("Each capability should be less than {MAX_LENGTH} characters").into())),
205    }
206}
207pub fn validate_attribute_doi(value: &[String]) -> Result<(), ValidationError> {
211    let is_valid = value.iter().all(|x| is_doi(x).is_ok());
212    match is_valid {
213        | true => Ok(()),
214        | _ => Err(ValidationError::new("DOIs").with_message("Every DOI should be valid".to_string().into())),
215    }
216}
217pub fn validate_attribute_ror(value: &[String]) -> Result<(), ValidationError> {
221    let is_valid = value.iter().all(|x| is_ror(x).is_ok());
222    match is_valid {
223        | true => Ok(()),
224        | _ => Err(ValidationError::new("RORs").with_message("Every ROR should be valid".to_string().into())),
225    }
226}
227pub fn validate_attribute_impact(value: &[String]) -> Result<(), ValidationError> {
232    const MAX_LENGTH: usize = MAX_LENGTH_IMPACT;
233    match value.iter().all(|x| x.len() <= MAX_LENGTH) {
234        | true => {
235            let all_periods = value.iter().all(|x| x.trim().ends_with("."));
236            let no_periods = value.iter().all(|x| !x.trim().ends_with("."));
237            let is_valid = all_periods || no_periods;
238            match is_valid {
239                | true => Ok(()),
240                | _ => Err(ValidationError::new("impact")
241                    .with_message("Impact statements should be all sentences with periods or all phrases without periods".into())),
242            }
243        }
244        | _ => Err(ValidationError::new("impact").with_message(format!("Each impact statement should be less than {MAX_LENGTH} characters").into())),
245    }
246}