Skip to main content

fhir_core/
validate.rs

1//! The version-independent core of FHIR validation.
2//!
3//! Validation is identical in shape across FHIR releases: a value reports a
4//! list of [`ValidationIssue`]s, and container types (`Option`, `Vec`,
5//! [`Vec1`](::vec1::Vec1), `Box`) delegate to what they hold. Only the
6//! *primitive format constraints* and the `OperationOutcome` bridge are
7//! release-specific, so those live in the per-release modules
8//! ([`crate::r4::validate`] and [`crate::r5::validate`]) which re-export the
9//! [`Validate`] trait and [`ValidationIssue`] type defined here.
10//!
11//! Because a single trait is shared, one `#[derive(Validate)]` implementation
12//! serves every release and a generic helper can validate R4 and R5 values
13//! alike.
14//!
15//! ```
16//! use fhir::validate::{Validate, ValidationIssue};
17//!
18//! // Containers delegate: an empty `Option` has nothing to report.
19//! let nothing: Option<String> = None;
20//! assert!(nothing.validate().is_empty());
21//!
22//! // An issue names what failed and why.
23//! let issue = ValidationIssue { path: "gender".to_string(), message: "bad".to_string() };
24//! assert_eq!(issue.path, "gender");
25//! ```
26
27/// A single validation problem, with the location and a human-readable message.
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct ValidationIssue {
30    /// A path or label identifying what failed (e.g. the datatype name).
31    pub path: String,
32    /// A human-readable description of the problem.
33    pub message: String,
34}
35
36impl ValidationIssue {
37    /// Build an issue at `path`.
38    ///
39    /// Public because the generated release crates construct issues, and they
40    /// are no longer the same crate as this one.
41    #[must_use]
42    pub fn new(path: &str, message: impl Into<String>) -> Self {
43        Self {
44            path: path.to_string(),
45            message: message.into(),
46        }
47    }
48}
49
50/// Types that can validate themselves against FHIR constraints.
51pub trait Validate {
52    /// Return all validation issues; an empty vector means the value is valid.
53    fn validate(&self) -> Vec<ValidationIssue>;
54
55    /// Convenience: `true` when [`Validate::validate`] finds no issues.
56    fn is_valid(&self) -> bool {
57        self.validate().is_empty()
58    }
59}
60
61impl<T: Validate> Validate for Option<T> {
62    fn validate(&self) -> Vec<ValidationIssue> {
63        self.as_ref().map(Validate::validate).unwrap_or_default()
64    }
65}
66
67impl<T: Validate> Validate for Vec<T> {
68    fn validate(&self) -> Vec<ValidationIssue> {
69        self.iter().flat_map(Validate::validate).collect()
70    }
71}
72
73/// A non-empty `Vec1` (used for FHIR `1..*` elements) validates each element.
74impl<T: Validate> Validate for ::vec1::Vec1<T> {
75    fn validate(&self) -> Vec<ValidationIssue> {
76        self.iter().flat_map(Validate::validate).collect()
77    }
78}
79
80impl<T: Validate> Validate for Box<T> {
81    fn validate(&self) -> Vec<ValidationIssue> {
82        (**self).validate()
83    }
84}
85
86/// A zero-sized type marker (e.g. the phantom on `Reference<T>`) has nothing to
87/// validate.
88impl<T: ?Sized> Validate for ::std::marker::PhantomData<T> {
89    fn validate(&self) -> Vec<ValidationIssue> {
90        Vec::new()
91    }
92}
93
94/// Arbitrary embedded JSON (e.g. a `contained` resource) is not structurally
95/// validated here.
96impl Validate for ::serde_json::Value {
97    fn validate(&self) -> Vec<ValidationIssue> {
98        Vec::new()
99    }
100}
101
102/// A bare `String` (used by a few fields) carries no FHIR-level constraint here.
103impl Validate for ::std::string::String {
104    fn validate(&self) -> Vec<ValidationIssue> {
105        Vec::new()
106    }
107}
108
109/// FHIR `code`: non-empty, no leading/trailing whitespace, single internal
110/// spaces (regex `[^\s]+(\s[^\s]+)*`).
111///
112/// The rule is unchanged between R4 and R5, so both releases share it.
113#[must_use]
114pub fn is_valid_code(s: &str) -> bool {
115    !s.is_empty()
116        && s == s.trim()
117        && !s.split(' ').any(str::is_empty)
118        && !s.chars().any(|c| c != ' ' && c.is_whitespace())
119}
120
121/// FHIR `id`: 1..=64 chars from `[A-Za-z0-9-.]`.
122#[must_use]
123pub fn is_valid_id(s: &str) -> bool {
124    (1..=64).contains(&s.len())
125        && s.chars()
126            .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '.')
127}
128
129/// FHIR `uri`/`url`/`canonical`: non-empty and not surrounded by whitespace.
130#[must_use]
131pub fn is_valid_uri_like(s: &str) -> bool {
132    !s.is_empty() && s.trim() == s
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138
139    #[test]
140    fn code_rules() {
141        assert!(is_valid_code("final"));
142        assert!(is_valid_code("entered in error"));
143        assert!(!is_valid_code(" leading"));
144        assert!(!is_valid_code("double  space"));
145        assert!(!is_valid_code(""));
146    }
147
148    #[test]
149    fn id_rules() {
150        assert!(is_valid_id("abc-123.4"));
151        assert!(!is_valid_id("bad id!"));
152        assert!(!is_valid_id(&"x".repeat(65)));
153    }
154
155    #[test]
156    fn uri_rules() {
157        assert!(is_valid_uri_like("http://example.org"));
158        assert!(!is_valid_uri_like(" http://example.org "));
159        assert!(!is_valid_uri_like(""));
160    }
161
162    #[test]
163    fn containers_delegate() {
164        // `String` is always valid, so containers of it report nothing.
165        let some: Option<String> = Some("x".to_string());
166        assert!(some.validate().is_empty());
167        assert!(vec!["a".to_string(), "b".to_string()].validate().is_empty());
168        assert!(Box::new("a".to_string()).validate().is_empty());
169        assert!(::serde_json::Value::Null.validate().is_empty());
170    }
171}