use std::fmt::Display;
use std::ops::{Bound, RangeBounds};
pub trait SdkValidator {
fn validate(&self) -> Vec<ValidationErrorInfo> {
let mut context = ValidationContext::default();
self.validate_into(&mut context);
context.into_errors()
}
#[doc(hidden)]
fn validate_into(&self, _context: &mut ValidationContext) {}
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum ValidationErrorType {
#[default]
Schema,
Semantic,
#[cfg(feature = "parts")]
Package,
#[cfg(feature = "mce")]
MarkupCompatibility,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ValidationErrorInfo {
pub error_type: ValidationErrorType,
pub description: String,
pub id: Option<&'static str>,
pub type_name: Option<&'static str>,
pub field_name: Option<&'static str>,
#[cfg(feature = "parts")]
pub part_uri: Option<String>,
}
impl ValidationErrorInfo {
#[inline]
pub fn from_error(error_type: ValidationErrorType, error: crate::common::SdkError) -> Self {
Self {
error_type,
description: error.to_string(),
id: validation_error_id(&error),
type_name: validation_error_type_name(&error),
field_name: validation_error_field_name(&error),
#[cfg(feature = "parts")]
part_uri: None,
}
}
#[cfg(feature = "parts")]
#[inline]
pub fn with_part_uri(mut self, part_uri: impl Into<String>) -> Self {
self.part_uri = Some(part_uri.into());
self
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ValidationSettings {
#[cfg(feature = "parts")]
pub file_format: crate::sdk::FileFormatVersion,
pub max_number_of_errors: usize,
}
impl Default for ValidationSettings {
#[inline]
fn default() -> Self {
Self {
#[cfg(feature = "parts")]
file_format: crate::sdk::FileFormatVersion::default(),
max_number_of_errors: usize::MAX,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ValidationContext {
settings: ValidationSettings,
errors: Vec<ValidationErrorInfo>,
#[cfg(feature = "parts")]
part_uri: Option<String>,
}
impl ValidationContext {
#[inline]
pub const fn with_settings(settings: ValidationSettings) -> Self {
Self {
settings,
errors: Vec::new(),
#[cfg(feature = "parts")]
part_uri: None,
}
}
#[inline]
pub const fn settings(&self) -> &ValidationSettings {
&self.settings
}
#[inline]
pub fn should_stop(&self) -> bool {
self.errors.len() >= self.settings.max_number_of_errors
}
#[inline]
pub fn check(&mut self, validate: impl FnOnce() -> Result<(), crate::common::SdkError>) {
if self.should_stop() {
return;
}
if let Err(error) = validate() {
self.push_error(error);
}
}
#[inline]
pub fn push_error(&mut self, error: crate::common::SdkError) {
if self.should_stop() {
return;
}
let info = ValidationErrorInfo::from_error(ValidationErrorType::Schema, error);
#[cfg(feature = "parts")]
let info = if let Some(part_uri) = &self.part_uri {
info.with_part_uri(part_uri.clone())
} else {
info
};
self.errors.push(info);
}
#[cfg(feature = "parts")]
pub fn with_part_uri(&mut self, part_uri: impl Into<String>, validate: impl FnOnce(&mut Self)) {
let previous = self.part_uri.replace(part_uri.into());
validate(self);
self.part_uri = previous;
}
#[inline]
pub fn into_errors(self) -> Vec<ValidationErrorInfo> {
self.errors
}
}
impl Default for ValidationContext {
#[inline]
fn default() -> Self {
Self::with_settings(ValidationSettings::default())
}
}
fn validation_error_id(error: &crate::common::SdkError) -> Option<&'static str> {
match error {
crate::common::SdkError::ValidationError { validator, .. } => Some(validator),
crate::common::SdkError::MissingField { .. } => Some("required"),
crate::common::SdkError::InvalidEnumValue { .. } => Some("enum"),
crate::common::SdkError::InvalidFieldValue { .. } => Some("field_value"),
_ => None,
}
}
fn validation_error_type_name(error: &crate::common::SdkError) -> Option<&'static str> {
match error {
crate::common::SdkError::ValidationError { ty, .. }
| crate::common::SdkError::MissingField { ty, .. }
| crate::common::SdkError::InvalidEnumValue { ty, .. }
| crate::common::SdkError::InvalidFieldValue { ty, .. }
| crate::common::SdkError::UnexpectedTag { ty, .. } => Some(ty),
_ => None,
}
}
fn validation_error_field_name(error: &crate::common::SdkError) -> Option<&'static str> {
match error {
crate::common::SdkError::ValidationError { field, .. }
| crate::common::SdkError::MissingField { field, .. }
| crate::common::SdkError::InvalidFieldValue { field, .. } => Some(field),
_ => None,
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum StringFormatKind {
Token,
NcName,
QName,
Uri,
Id,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum NumberSignKind {
NonNegative,
Positive,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum StringLengthKind {
Characters,
HexBinaryBytes,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum BinaryFormatKind {
Hex,
Base64,
}
pub fn validate_pattern<T: Display>(
ty: &'static str,
field: &'static str,
value: &T,
regex: &str,
) -> Result<(), crate::common::SdkError> {
let anchored_regex = format!(r"\A(?:{regex})\z");
let regex = regex::Regex::new(&anchored_regex).map_err(|err| {
crate::common::SdkError::CommonError(format!("invalid validator regex for {ty}.{field}: {err}"))
})?;
validate_pattern_regex(ty, field, value, ®ex)
}
pub fn validate_pattern_regex<T: Display>(
ty: &'static str,
field: &'static str,
value: &T,
regex: ®ex::Regex,
) -> Result<(), crate::common::SdkError> {
let value_string = value.to_string();
if regex.is_match(&value_string) {
Ok(())
} else {
Err(crate::common::validation_error(
ty,
field,
"pattern",
value_string,
format!("value does not match regex {regex}"),
))
}
}
pub fn validate_string_length<T: Display>(
ty: &'static str,
field: &'static str,
value: &T,
min: Option<u32>,
max: Option<u32>,
) -> Result<(), crate::common::SdkError> {
let value_string = value.to_string();
validate_string_length_with_kind(
ty,
field,
value_string,
min,
max,
StringLengthKind::Characters,
)
}
pub fn validate_string_length_with_kind(
ty: &'static str,
field: &'static str,
value_string: String,
min: Option<u32>,
max: Option<u32>,
kind: StringLengthKind,
) -> Result<(), crate::common::SdkError> {
let value_len = match kind {
StringLengthKind::Characters => value_string.chars().count() as u32,
StringLengthKind::HexBinaryBytes => (value_string.chars().count() / 2) as u32,
};
if let Some(min) = min
&& value_len < min
{
return Err(crate::common::validation_error(
ty,
field,
"string_length",
value_string,
format!("length {value_len} is below minimum {min}"),
));
}
if let Some(max) = max
&& value_len > max
{
return Err(crate::common::validation_error(
ty,
field,
"string_length",
value_string,
format!("length {value_len} exceeds maximum {max}"),
));
}
Ok(())
}
pub fn validate_string_format<T: Display>(
ty: &'static str,
field: &'static str,
value: &T,
kind: StringFormatKind,
) -> Result<(), crate::common::SdkError> {
let value_string = value.to_string();
let valid = match kind {
StringFormatKind::Token => is_token(&value_string),
StringFormatKind::NcName | StringFormatKind::Id => is_ncname(&value_string),
StringFormatKind::QName => is_qname(&value_string),
StringFormatKind::Uri => is_uri(&value_string),
};
if valid {
Ok(())
} else {
Err(crate::common::validation_error(
ty,
field,
"string_format",
value_string,
format!("value does not satisfy {kind:?} format"),
))
}
}
pub fn validate_binary_format<T: Display>(
ty: &'static str,
field: &'static str,
value: &T,
kind: BinaryFormatKind,
) -> Result<(), crate::common::SdkError> {
let value_string = value.to_string();
let valid = match kind {
BinaryFormatKind::Hex => crate::simple_type::is_valid_hex_binary(&value_string),
BinaryFormatKind::Base64 => is_base64_binary(&value_string),
};
if valid {
Ok(())
} else {
Err(crate::common::validation_error(
ty,
field,
"binary_format",
value_string,
format!("value does not satisfy {kind:?} binary format"),
))
}
}
pub fn validate_decimal_format<T: Display>(
ty: &'static str,
field: &'static str,
value: &T,
) -> Result<(), crate::common::SdkError> {
let value_string = value.to_string();
if is_decimal(&value_string) {
Ok(())
} else {
Err(crate::common::validation_error(
ty,
field,
"decimal_format",
value_string,
"value does not satisfy decimal format".to_string(),
))
}
}
pub fn validate_datetime_format<T: Display>(
ty: &'static str,
field: &'static str,
value: &T,
) -> Result<(), crate::common::SdkError> {
let value_string = value.to_string();
if is_datetime(&value_string) {
Ok(())
} else {
Err(crate::common::validation_error(
ty,
field,
"datetime_format",
value_string,
"value does not satisfy dateTime format".to_string(),
))
}
}
pub fn validate_number_range<T: Display, R: RangeBounds<f64>>(
ty: &'static str,
field: &'static str,
value: &T,
range: R,
) -> Result<(), crate::common::SdkError> {
let value_string = value.to_string();
let parsed_value = value_string.parse::<f64>().map_err(|err| {
crate::common::SdkError::CommonError(format!(
"failed to parse numeric validator value for {ty}.{field}: {err}"
))
})?;
if parsed_value.is_nan() {
return Err(crate::common::validation_error(
ty,
field,
"number_range",
value_string,
"value must not be NaN".to_string(),
));
}
match range.start_bound() {
Bound::Included(min) if parsed_value < *min => {
return Err(crate::common::validation_error(
ty,
field,
"number_range",
value_string,
format!("value must be >= {min}"),
));
}
Bound::Excluded(min) if parsed_value <= *min => {
return Err(crate::common::validation_error(
ty,
field,
"number_range",
value_string,
format!("value must be > {min}"),
));
}
Bound::Included(_) | Bound::Excluded(_) | Bound::Unbounded => {}
}
match range.end_bound() {
Bound::Included(max) if parsed_value > *max => {
return Err(crate::common::validation_error(
ty,
field,
"number_range",
value_string,
format!("value must be <= {max}"),
));
}
Bound::Excluded(max) if parsed_value >= *max => {
return Err(crate::common::validation_error(
ty,
field,
"number_range",
value_string,
format!("value must be < {max}"),
));
}
Bound::Included(_) | Bound::Excluded(_) | Bound::Unbounded => {}
}
Ok(())
}
pub fn validate_number_sign<T: Display>(
ty: &'static str,
field: &'static str,
value: &T,
kind: NumberSignKind,
) -> Result<(), crate::common::SdkError> {
let value_string = value.to_string();
let parsed_value = value_string.parse::<f64>().map_err(|err| {
crate::common::SdkError::CommonError(format!(
"failed to parse numeric validator value for {ty}.{field}: {err}"
))
})?;
let valid = match kind {
NumberSignKind::NonNegative => parsed_value >= 0.0,
NumberSignKind::Positive => parsed_value > 0.0,
};
if valid && !parsed_value.is_nan() {
Ok(())
} else {
Err(crate::common::validation_error(
ty,
field,
"number_sign",
value_string,
format!("value does not satisfy {kind:?} constraint"),
))
}
}
pub fn validate_number_type<T: Display>(
ty: &'static str,
field: &'static str,
value: &T,
type_name: &str,
) -> Result<(), crate::common::SdkError> {
let value_string = value.to_string();
let valid = match type_name {
"xsd:byte" => value_string.parse::<i8>().is_ok(),
"xsd:short" => value_string.parse::<i16>().is_ok(),
"xsd:int" => value_string.parse::<i32>().is_ok(),
"xsd:long" => value_string.parse::<i64>().is_ok(),
"xsd:unsignedByte" => value_string.parse::<u8>().is_ok(),
"xsd:unsignedShort" => value_string.parse::<u16>().is_ok(),
"xsd:unsignedInt" => value_string.parse::<u32>().is_ok(),
"xsd:unsignedLong" => value_string.parse::<u64>().is_ok(),
"xsd:integer" => value_string.parse::<i128>().is_ok(),
"xsd:nonNegativeInteger" => value_string.parse::<u128>().is_ok(),
"xsd:decimal" | "xsd:double" => value_string.parse::<f64>().is_ok(),
"xsd:float" => value_string.parse::<f32>().is_ok(),
"wp:ST_PositionOffset"
| "xdr:ST_ColID"
| "xdr:ST_RowID"
| "a:ST_Angle"
| "a:ST_Percentage"
| "a:ST_PositivePercentage"
| "a:ST_PositiveFixedPercentage"
| "w:ST_DecimalNumber"
| "w:ST_NonNegativeDecimalNumber"
| "w:ST_SignedDecimalNumberMax-1"
| "w:ST_SignedDecimalNumberMax-2"
| "w:ST_SignedHpsMeasure_O12"
| "w:ST_SignedTwipsMeasure_O12"
| "w:ST_UnsignedDecimalNumberMin1" => value_string.parse::<i32>().is_ok(),
"a:ST_Coordinate" => value_string.parse::<i64>().is_ok(),
"ask:ST_LineSketchSeed"
| "cx:ST_AxisId"
| "a:ST_DrawingElementId"
| "w:ST_HpsMeasure_O12"
| "w:ST_TwipsMeasure_O12"
| "w:ST_UnsignedDecimalNumber" => value_string.parse::<u32>().is_ok(),
_ => false,
};
if valid {
Ok(())
} else {
Err(crate::common::validation_error(
ty,
field,
"number_type",
value_string,
format!("value does not satisfy {type_name} numeric type"),
))
}
}
pub fn validate_string_set<T: Display>(
ty: &'static str,
field: &'static str,
value: &T,
values: &[&str],
) -> Result<(), crate::common::SdkError> {
let value_string = value.to_string();
if values.contains(&value_string.as_str()) {
Ok(())
} else {
Err(crate::common::validation_error(
ty,
field,
"string_set",
value_string,
format!("value is not one of {}", values.join(", ")),
))
}
}
fn is_token(value: &str) -> bool {
value.trim() == value
&& !value.chars().any(|c| matches!(c, '\n' | '\r' | '\t'))
&& !value.contains(" ")
}
fn is_ncname(value: &str) -> bool {
let mut chars = value.chars();
let Some(first) = chars.next() else {
return false;
};
if !(first == '_' || first.is_alphabetic()) {
return false;
}
chars.all(|ch| ch == '_' || ch == '-' || ch == '.' || ch.is_alphanumeric())
}
fn is_qname(value: &str) -> bool {
let mut parts = value.split(':');
let Some(first) = parts.next() else {
return false;
};
let Some(second) = parts.next() else {
return is_ncname(first);
};
parts.next().is_none() && is_ncname(first) && is_ncname(second)
}
fn is_uri(value: &str) -> bool {
if value.is_empty() {
return true;
}
let value = value.trim_matches([' ', '\t', '\n', '\r']);
if value.is_empty() {
return false;
}
if value.chars().any(char::is_whitespace) {
return false;
}
if value.contains("##") {
return false;
}
if let Some(scheme_end) = value.find("://")
&& value[scheme_end + 3..].starts_with('/')
{
return false;
}
true
}
fn is_base64_binary(value: &str) -> bool {
let mut padding_seen = false;
let mut padding_count = 0usize;
let mut len = 0usize;
for ch in value.chars().filter(|ch| !ch.is_ascii_whitespace()) {
len += 1;
match ch {
'A'..='Z' | 'a'..='z' | '0'..='9' | '+' | '/' if !padding_seen => {}
'=' => {
padding_seen = true;
padding_count += 1;
if padding_count > 2 {
return false;
}
}
_ => return false,
}
}
if len == 0 {
return true;
}
len.is_multiple_of(4)
}
fn is_decimal(value: &str) -> bool {
let value = value.strip_prefix(['+', '-']).unwrap_or(value);
if value.is_empty() || value.chars().any(char::is_whitespace) {
return false;
}
let mut dot_seen = false;
let mut digit_seen = false;
for ch in value.chars() {
match ch {
'0'..='9' => digit_seen = true,
'.' if !dot_seen => dot_seen = true,
_ => return false,
}
}
digit_seen
}
fn is_datetime(value: &str) -> bool {
let Some((date, time)) = value.split_once('T') else {
return false;
};
is_date(date) && is_time(time)
}
fn is_date(value: &str) -> bool {
let mut parts = value.split('-');
let (Some(year), Some(month), Some(day), None) =
(parts.next(), parts.next(), parts.next(), parts.next())
else {
return false;
};
if year.len() != 4 || month.len() != 2 || day.len() != 2 {
return false;
}
let Ok(year) = year.parse::<u32>() else {
return false;
};
let Ok(month) = month.parse::<u32>() else {
return false;
};
let Ok(day) = day.parse::<u32>() else {
return false;
};
if year == 0 || !(1..=12).contains(&month) {
return false;
}
(1..=days_in_month(year, month)).contains(&day)
}
fn is_time(value: &str) -> bool {
let (time, zone) = split_timezone(value);
if let Some(zone) = zone
&& !is_timezone(zone)
{
return false;
}
let mut parts = time.split(':');
let (Some(hour), Some(minute), Some(second), None) =
(parts.next(), parts.next(), parts.next(), parts.next())
else {
return false;
};
if hour.len() != 2 || minute.len() != 2 || second.len() < 2 {
return false;
}
let Ok(hour) = hour.parse::<u32>() else {
return false;
};
let Ok(minute) = minute.parse::<u32>() else {
return false;
};
let Some((second, fraction)) = second
.split_once('.')
.map_or(Some((second, None)), |(s, f)| {
if f.is_empty() {
None
} else {
Some((s, Some(f)))
}
})
else {
return false;
};
let Ok(second) = second.parse::<u32>() else {
return false;
};
hour <= 23
&& minute <= 59
&& second <= 59
&& fraction.is_none_or(|fraction| fraction.chars().all(|ch| ch.is_ascii_digit()))
}
fn split_timezone(value: &str) -> (&str, Option<&str>) {
if let Some(time) = value.strip_suffix('Z') {
return (time, Some("Z"));
}
if value.len() > 6 {
let zone_start = value.len() - 6;
let zone = &value[zone_start..];
if matches!(zone.as_bytes().first(), Some(b'+' | b'-')) {
return (&value[..zone_start], Some(zone));
}
}
(value, None)
}
fn is_timezone(value: &str) -> bool {
if value == "Z" {
return true;
}
let Some((hour, minute)) = value[1..].split_once(':') else {
return false;
};
if hour.len() != 2 || minute.len() != 2 {
return false;
}
let Ok(hour) = hour.parse::<u32>() else {
return false;
};
let Ok(minute) = minute.parse::<u32>() else {
return false;
};
(hour < 14 && minute <= 59) || (hour == 14 && minute == 0)
}
fn days_in_month(year: u32, month: u32) -> u32 {
match month {
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
4 | 6 | 9 | 11 => 30,
2 if is_leap_year(year) => 29,
2 => 28,
_ => 0,
}
}
fn is_leap_year(year: u32) -> bool {
year.is_multiple_of(4) && (!year.is_multiple_of(100) || year.is_multiple_of(400))
}