acorn_lib/schema/
validate.rs

1#![deny(missing_docs)]
2//! # Schema validation helpers
3//!
4//! Generic validation functions and custom [validator](https://docs.rs/validator/latest/validator/) functions for validating schema data
5use crate::constants::*;
6use fancy_regex::Regex;
7use validator::ValidationError;
8
9/// Format a phone number into a standard format
10pub fn format_phone_number(value: &str) -> Result<String, ValidationError> {
11    const MESSAGE: &str = "Unable to format telephone number";
12    match RE_PHONE.captures(value) {
13        | Ok(value) => match value {
14            | Some(captures) => {
15                let country_code = match captures.name("country") {
16                    | Some(value) => Some(value.as_str().trim().to_string()),
17                    | None => None,
18                };
19                let area_code = match captures.name("area") {
20                    | Some(value) => Some(value.as_str().replace("(", "").replace(")", "")),
21                    | None => None,
22                };
23                let prefix = match captures.name("prefix") {
24                    | Some(value) => Some(value.as_str().to_string()),
25                    | None => None,
26                };
27                let line = match captures.name("line") {
28                    | Some(value) => Some(value.as_str().to_string()),
29                    | None => None,
30                };
31                Ok([country_code, area_code, prefix, line]
32                    .into_iter()
33                    .flatten()
34                    .collect::<Vec<String>>()
35                    .join("."))
36            }
37            | None => Err(ValidationError::new("telephone").with_message(MESSAGE.into())),
38        },
39        | _ => Err(ValidationError::new("telephone").with_message(MESSAGE.into())),
40    }
41}
42/// Check if a path has a valid image extension
43pub fn has_image_extension(value: &str) -> Result<(), ValidationError> {
44    const MESSAGE: &str = "Please provide a path with a PNG, JPEG, GIF, WEBP, TIFF or SVG extension";
45    match RE_IMAGE_EXTENSION.is_match(value) {
46        | Ok(value) if value => Ok(()),
47        | _ => Err(ValidationError::new("image").with_message(MESSAGE.into())),
48    }
49}
50/// Check if value is a valid DOI
51pub fn is_doi(value: &str) -> Result<(), ValidationError> {
52    const MESSAGE: &str = "Please provide a valid DOI, by itself and without domain or 'doi:' prefix.";
53    match RE_DOI.is_match(value) {
54        | Ok(value) if value => Ok(()),
55        | _ => Err(ValidationError::new("doi").with_message(MESSAGE.into())),
56    }
57}
58/// Check if value is a valid IP6
59pub fn is_ip6(value: &str) -> Result<(), ValidationError> {
60    const MESSAGE: &str = "Please provide a valid IP6 address";
61    match RE_IP6.is_match(value) {
62        | Ok(value) if value => Ok(()),
63        | _ => Err(ValidationError::new("IP6").with_message(MESSAGE.into())),
64    }
65}
66/// Check if value is a valid kebab-case (e.g. 'this-is-kebab-case')
67pub fn is_kebabcase(value: &str) -> Result<(), ValidationError> {
68    const MESSAGE: &str = "Please provide an ID in kebab-case format";
69    let kebab = match Regex::new(r"[ *_./\!@#$%^&(){}]") {
70        | Ok(re) => re.replace_all(value, "").trim().to_string(),
71        | Err(err) => err.to_string(),
72    };
73    match kebab.to_lowercase().eq(&value) {
74        | true => Ok(()),
75        | _ => Err(ValidationError::new("kebabcase").with_message(MESSAGE.into())),
76    }
77}
78/// Check if value is a valid phone number
79///
80/// Uses same regex as `format_phone_number`
81pub fn is_phone_number(value: &str) -> Result<(), ValidationError> {
82    const MESSAGE: &str = "Please provide a valid phone number";
83    let is_fake = match RE_FAKE_PHONE.is_match(value) {
84        | Ok(value) if value => true,
85        | _ => false,
86    };
87    match RE_PHONE.is_match(value) {
88        | Ok(value) if value && !is_fake => Ok(()),
89        | _ => Err(ValidationError::new("phone").with_message(MESSAGE.into())),
90    }
91}
92/// Check if value is a valid RAiD
93pub fn is_raid(value: &str) -> Result<(), ValidationError> {
94    const MESSAGE: &str = "Please provide a valid RAiD";
95    match RE_RAID.is_match(value) {
96        | Ok(value) if value => Ok(()),
97        | _ => Err(ValidationError::new("raid").with_message(MESSAGE.into())),
98    }
99}
100/// Check if value is a valid ROR
101pub fn is_ror(value: &str) -> Result<(), ValidationError> {
102    const MESSAGE: &str = "Please provide a valid ROR";
103    match RE_ROR.is_match(value) {
104        | Ok(value) if value => Ok(()),
105        | _ => Err(ValidationError::new("ror").with_message(MESSAGE.into())),
106    }
107}
108/// Custom validator function for [approach](/acorn_lib/schema/struct.Sections.html#structfield.approach)
109pub fn validate_attribute_approach(value: &[String]) -> Result<(), ValidationError> {
110    const MAX_LENGTH: usize = MAX_LENGTH_APPROACH;
111    let message: String = format!("Each approach statement should be less than {MAX_LENGTH} characters");
112    let is_valid = value.iter().all(|x| x.len() <= MAX_LENGTH);
113    match is_valid {
114        | true => Ok(()),
115        | _ => Err(ValidationError::new("approach").with_message(message.into())),
116    }
117}
118/// Custom validator function for [research areas](/acorn_lib/schema/struct.Research.html#structfield.areas)
119pub fn validate_attribute_areas(value: &[String]) -> Result<(), ValidationError> {
120    const MAX_LENGTH: usize = MAX_LENGTH_RESEARCH_AREA;
121    let is_valid = value.iter().all(|x| x.len() <= MAX_LENGTH);
122    match is_valid {
123        | true => Ok(()),
124        | _ => Err(ValidationError::new("area").with_message(format!("Each area should be less than {MAX_LENGTH} characters").into())),
125    }
126}
127/// Custom validator function for [`ResearchActivity`] [capabilities](/acorn_lib/schema/struct.Sections.html#structfield.capabilities)
128///
129/// [`ResearchActivity`]: ../struct.ResearchActivity.html
130pub fn validate_attribute_capabilities(value: &[String]) -> Result<(), ValidationError> {
131    const MAX_LENGTH: usize = MAX_LENGTH_CAPABILIY;
132    let is_valid = value.iter().all(|x| x.len() <= MAX_LENGTH);
133    match is_valid {
134        | true => Ok(()),
135        | _ => Err(ValidationError::new("capability").with_message(format!("Each capability should be less than {MAX_LENGTH} characters").into())),
136    }
137}
138// TODO: Check that statments start with capital letter (use regex for period and captial?)
139/// Custom validator function for [`ResearchActivity`] [impact](/acorn_lib/schema/struct.Sections.html#structfield.impact)
140///
141/// [`ResearchActivity`]: ../struct.ResearchActivity.html
142pub fn validate_attribute_impact(value: &[String]) -> Result<(), ValidationError> {
143    const MAX_LENGTH: usize = MAX_LENGTH_IMPACT;
144    match value.iter().all(|x| x.len() <= MAX_LENGTH) {
145        | true => {
146            let all_periods = value.iter().all(|x| x.trim().ends_with("."));
147            let no_periods = value.iter().all(|x| !x.trim().ends_with("."));
148            let is_valid = all_periods || no_periods;
149            match is_valid {
150                | true => Ok(()),
151                | _ => Err(ValidationError::new("impact")
152                    .with_message("Impact statements should be all sentences with periods or all phrases without periods".into())),
153            }
154        }
155        | _ => Err(ValidationError::new("impact").with_message(format!("Each impact statement should be less than {MAX_LENGTH} characters").into())),
156    }
157}