use fhir_types::codec::Value;
use fhir_types::schema::ValueKind;
use regex::bytes::Regex;
use regex::bytes::RegexBuilder;
use crate::issue::IssueType;
use crate::issue::MessageId;
use crate::issue::Severity;
use crate::validate::ValidateError;
use crate::validate::structure::Structure;
use crate::validate::walk::Walk;
const REGEX: &str = "http://hl7.org/fhir/StructureDefinition/regex";
#[derive(Debug)]
pub(crate) struct PrimitiveRule {
lexical: Option<Regex>,
max_length: Option<usize>,
}
impl PrimitiveRule {
pub(crate) fn read(structure: &Structure, code: &str) -> Result<Self, ValidateError> {
let Some(element) = structure.element(&format!("{code}.value")) else {
return Ok(Self {
lexical: None,
max_length: None,
});
};
let pattern = element
.r#type
.iter()
.flat_map(|kind| kind.extension.iter())
.find(|extension| extension.url == REGEX)
.and_then(|extension| match &extension.value {
Some(fhir_types::r4::extension::ExtensionValue::String(text)) => text.value.clone(),
_ => None,
});
let lexical = pattern
.map(|pattern| {
RegexBuilder::new(&format!("^(?:{pattern})$"))
.unicode(false)
.build()
.map_err(|source| ValidateError::Regex {
type_code: code.to_owned(),
source,
})
})
.transpose()?;
let max_length = element
.max_length
.as_ref()
.and_then(|length| length.value)
.and_then(|length| usize::try_from(length).ok());
Ok(Self {
lexical,
max_length,
})
}
}
fn integer_range(code: &str) -> Option<(i64, i64)> {
let max = i64::from(i32::MAX);
match code {
"integer" => Some((i64::from(i32::MIN), max)),
"unsignedInt" => Some((0, max)),
"positiveInt" => Some((1, max)),
_ => None,
}
}
impl Walk<'_, '_, '_> {
pub(crate) fn primitive_value(
&mut self,
value: &Value,
code: &str,
kind: ValueKind,
location: &str,
) -> Result<(), ValidateError> {
let rule = self.validator().primitive(code)?;
let text = match (kind, value) {
(ValueKind::Boolean, Value::Bool(_)) => return Ok(()),
(ValueKind::Integer | ValueKind::Decimal, Value::Number(number)) => number.as_str(),
(ValueKind::Text, Value::String(text)) => text.as_str(),
_ => {
let expected = match kind {
ValueKind::Boolean => "a JSON boolean",
ValueKind::Integer | ValueKind::Decimal => "a JSON number",
ValueKind::Text => "a JSON string",
};
self.out().issue(
Severity::Error,
IssueType::Structure,
MessageId::WrongJsonType,
location,
format!("a {code} is written as {expected}"),
);
return Ok(());
}
};
if text.is_empty() {
self.out().issue(
Severity::Error,
IssueType::Value,
MessageId::EmptyString,
location,
"the value is an empty string".to_owned(),
);
return Ok(());
}
if let Some(lexical) = &rule.lexical
&& !lexical.is_match(text.as_bytes())
{
self.out().issue(
Severity::Error,
IssueType::Value,
MessageId::LexicalForm,
location,
format!("the value is not a valid {code}"),
);
return Ok(());
}
if kind == ValueKind::Integer
&& let Some((min, max)) = integer_range(code)
&& !text
.parse::<i64>()
.is_ok_and(|number| (min..=max).contains(&number))
{
self.out().issue(
Severity::Error,
IssueType::Value,
MessageId::IntegerRange,
location,
format!("the value is outside the range of {code}, {min} to {max}"),
);
}
if let Some(max_length) = rule.max_length
&& text.chars().count() > max_length
{
self.out().issue(
Severity::Error,
IssueType::TooLong,
MessageId::TooLong,
location,
format!("the value is longer than the {max_length} characters a {code} allows"),
);
}
Ok(())
}
pub(crate) fn max_length(
&mut self,
value: Option<&Value>,
max_length: Option<i32>,
element: &str,
location: &str,
) {
let (Some(Value::String(text)), Some(max_length)) = (value, max_length) else {
return;
};
let Ok(limit) = usize::try_from(max_length) else {
return;
};
if text.chars().count() > limit {
self.out().issue(
Severity::Error,
IssueType::TooLong,
MessageId::MaxLength,
location,
format!("the value is longer than the maxLength of {limit} that {element} sets"),
);
}
}
}
#[cfg(test)]
mod tests {
use super::integer_range;
#[test]
fn the_integer_types_carry_the_r4_ranges() {
assert_eq!(
integer_range("integer"),
Some((-2_147_483_648, 2_147_483_647))
);
assert_eq!(integer_range("unsignedInt"), Some((0, 2_147_483_647)));
assert_eq!(integer_range("positiveInt"), Some((1, 2_147_483_647)));
assert_eq!(integer_range("decimal"), None);
}
}