// Copyright (c) 2024 xmpp-rs contributors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
//! XEP-0122: Data Forms Validation
//! https://xmpp.org/extensions/xep-0122.html#usecases-datatypes
use std::fmt::{Display, Formatter};
use std::str::FromStr;
use minidom::{Element, IntoAttributeValue};
use xso::error::FromElementError;
use crate::ns::XDATA_VALIDATE;
use crate::Error;
/// Validation Method
#[derive(Debug, Clone, PartialEq)]
pub enum Method {
/// … to indicate that the value(s) should simply match the field type and datatype constraints,
/// the element shall contain a child element. Using validation,
/// the form interpreter MUST follow the validation rules of the datatype (if understood) and
/// the field type.
///
/// https://xmpp.org/extensions/xep-0122.html#usercases-validation.basic
Basic,
/// For "list-single" or "list-multi", to indicate that the user may enter a custom value
/// (matching the datatype constraints) or choose from the predefined values, the
/// element shall contain an child element. The validation method applies to
/// "text-multi" differently; it hints that each value for a "text-multi" field shall be
/// validated separately. This effectively turns "text-multi" fields into an open-ended
/// "list-multi", with no options and all values automatically selected.
///
/// https://xmpp.org/extensions/xep-0122.html#usercases-validation.open
Open,
/// To indicate that the value should fall within a certain range, the element shall
/// contain a child element. The 'min' and 'max' attributes of the element
/// specify the minimum and maximum values allowed, respectively.
///
/// The 'max' attribute specifies the maximum allowable value. This attribute is OPTIONAL.
/// The value depends on the datatype in use.
///
/// The 'min' attribute specifies the minimum allowable value. This attribute is OPTIONAL.
/// The value depends on the datatype in use.
///
/// The element SHOULD possess either a 'min' or 'max' attribute, and MAY possess both.
/// If neither attribute is included, the processor MUST assume that there are no range
/// constraints.
///
/// https://xmpp.org/extensions/xep-0122.html#usercases-validation.range
Range {
/// The 'min' attribute specifies the minimum allowable value.
min: Option,
/// The 'max' attribute specifies the maximum allowable value.
max: Option,
},
/// To indicate that the value should be restricted to a regular expression, the
/// element shall contain a child element. The XML character data of this element is
/// the pattern to apply. The syntax of this content MUST be that defined for POSIX extended
/// regular expressions, including support for Unicode. The element MUST contain
/// character data only.
///
/// https://xmpp.org/extensions/xep-0122.html#usercases-validatoin.regex
Regex(String),
}
generate_element!(
/// Selection Ranges in "list-multi"
ListRange, "list-range", XDATA_VALIDATE,
attributes: [
/// The 'min' attribute specifies the minimum allowable number of selected/entered values.
min: Option = "min",
/// The 'max' attribute specifies the maximum allowable number of selected/entered values.
max: Option = "max",
]
);
/// Data Forms Validation Datatypes
///
/// https://xmpp.org/registrar/xdv-datatypes.html
#[derive(Debug, Clone, PartialEq)]
pub enum Datatype {
/// A Uniform Resource Identifier Reference (URI)
AnyUri,
/// An integer with the specified min/max
/// Min: -128, Max: 127
Byte,
/// A calendar date
Date,
/// A specific instant of time
DateTime,
/// An arbitrary-precision decimal number
Decimal,
/// An IEEE double-precision 64-bit floating point type
Double,
/// An integer with the specified min/max
/// Min: -2147483648, Max: 2147483647
Int,
/// A decimal number with no fraction digits
Integer,
/// A language identifier as defined by RFC 1766
Language,
/// An integer with the specified min/max
/// Min: -9223372036854775808, Max: 9223372036854775807
Long,
/// An integer with the specified min/max
/// Min: -32768, Max: 32767
Short,
/// A character strings in XML
String,
/// An instant of time that recurs every day
Time,
/// A user-defined datatype
UserDefined(String),
/// A non-standard datatype
Other {
/// The prefix of the specified datatype. Should be registered with the XMPP Registrar.
prefix: String,
/// The actual value of the specified datatype. E.g. "lat" in the case of "geo:lat".
value: String,
},
}
/// Validation rules for a DataForms Field.
#[derive(Debug, Clone, PartialEq)]
pub struct Validate {
/// The 'datatype' attribute specifies the datatype. This attribute is OPTIONAL, and defaults
/// to "xs:string". It MUST meet one of the following conditions:
///
/// - Start with "xs:", and be one of the "built-in" datatypes defined in XML Schema Part 2
/// - Start with a prefix registered with the XMPP Registrar
/// - Start with "x:", and specify a user-defined datatype.
///
/// Note that while "x:" allows for ad-hoc definitions, its use is NOT RECOMMENDED.
pub datatype: Option,
/// The validation method. If no validation method is specified, form processors MUST
/// assume validation. The element SHOULD include one of the above
/// validation method elements, and MUST NOT include more than one.
///
/// Any validation method applied to a field of type "list-multi", "list-single", or "text-multi"
/// (other than ) MUST imply the same behavior as , with the additional constraints
/// defined by that method.
///
/// https://xmpp.org/extensions/xep-0122.html#usecases-validation
pub method: Option,
/// For "list-multi", validation can indicate (via the element) that a minimum
/// and maximum number of options should be selected and/or entered. This selection range
/// MAY be combined with the other methods to provide more flexibility.
/// The element SHOULD be included only when the is of type "list-multi"
/// and SHOULD be ignored otherwise.
///
/// The element SHOULD possess either a 'min' or 'max' attribute, and MAY possess
/// both. If neither attribute is included, the processor MUST assume that there are no
/// selection constraints.
///
/// https://xmpp.org/extensions/xep-0122.html#usecases-ranges
pub list_range: Option,
}
impl TryFrom for Validate {
type Error = FromElementError;
fn try_from(elem: Element) -> Result {
check_self!(elem, "validate", XDATA_VALIDATE);
check_no_unknown_attributes!(elem, "item", ["datatype"]);
let mut validate = Validate {
datatype: elem.attr("datatype").map(Datatype::from_str).transpose()?,
method: None,
list_range: None,
};
for child in elem.children() {
match child {
_ if child.is("list-range", XDATA_VALIDATE) => {
let list_range = ListRange::try_from(child.clone())?;
if validate.list_range.is_some() {
return Err(Error::Other(
"Encountered unsupported number (n > 1) of list-range in validate element.",
).into());
}
validate.list_range = Some(list_range);
}
_ => {
let method = Method::try_from(child.clone())?;
if validate.method.is_some() {
return Err(Error::Other(
"Encountered unsupported number (n > 1) of validation methods in validate element.",
).into());
}
validate.method = Some(method);
}
}
}
Ok(validate)
}
}
impl From for Element {
fn from(value: Validate) -> Self {
Element::builder("validate", XDATA_VALIDATE)
.attr("datatype", value.datatype)
.append_all(value.method)
.append_all(value.list_range)
.build()
}
}
impl TryFrom for Method {
type Error = Error;
fn try_from(elem: Element) -> Result {
let method = match elem {
_ if elem.is("basic", XDATA_VALIDATE) => {
check_no_attributes!(elem, "basic");
Method::Basic
}
_ if elem.is("open", XDATA_VALIDATE) => {
check_no_attributes!(elem, "open");
Method::Open
}
_ if elem.is("range", XDATA_VALIDATE) => {
check_no_unknown_attributes!(elem, "range", ["min", "max"]);
Method::Range {
min: elem.attr("min").map(ToString::to_string),
max: elem.attr("max").map(ToString::to_string),
}
}
_ if elem.is("regex", XDATA_VALIDATE) => {
check_no_attributes!(elem, "regex");
check_no_children!(elem, "regex");
Method::Regex(elem.text())
}
_ => return Err(Error::Other("Encountered invalid validation method.").into()),
};
Ok(method)
}
}
impl From for Element {
fn from(value: Method) -> Self {
match value {
Method::Basic => Element::builder("basic", XDATA_VALIDATE),
Method::Open => Element::builder("open", XDATA_VALIDATE),
Method::Range { min, max } => Element::builder("range", XDATA_VALIDATE)
.attr("min", min)
.attr("max", max),
Method::Regex(regex) => Element::builder("regex", XDATA_VALIDATE).append(regex),
}
.build()
}
}
impl FromStr for Datatype {
type Err = Error;
fn from_str(s: &str) -> Result {
let mut parts = s.splitn(2, ":");
let Some(prefix) = parts.next() else {
return Err(Error::Other(
"Encountered invalid validation datatype which is missing a prefix.",
)
.into());
};
match prefix {
"xs" => (),
"x" => {
return Ok(Datatype::UserDefined(
parts.next().unwrap_or_default().to_string(),
))
}
_ => {
return Ok(Datatype::Other {
prefix: prefix.to_string(),
value: parts.next().unwrap_or_default().to_string(),
})
}
}
let Some(datatype) = parts.next() else {
return Err(Error::Other("Encountered invalid validation datatype.").into());
};
let parsed_datatype = match datatype {
"anyURI" => Datatype::AnyUri,
"byte" => Datatype::Byte,
"date" => Datatype::Date,
"dateTime" => Datatype::DateTime,
"decimal" => Datatype::Decimal,
"double" => Datatype::Double,
"int" => Datatype::Int,
"integer" => Datatype::Integer,
"language" => Datatype::Language,
"long" => Datatype::Long,
"short" => Datatype::Short,
"string" => Datatype::String,
"time" => Datatype::Time,
_ => return Err(Error::Other("Encountered invalid validation datatype.").into()),
};
Ok(parsed_datatype)
}
}
impl Display for Datatype {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let value = match self {
Datatype::AnyUri => "xs:anyURI",
Datatype::Byte => "xs:byte",
Datatype::Date => "xs:date",
Datatype::DateTime => "xs:dateTime",
Datatype::Decimal => "xs:decimal",
Datatype::Double => "xs:double",
Datatype::Int => "xs:int",
Datatype::Integer => "xs:integer",
Datatype::Language => "xs:language",
Datatype::Long => "xs:long",
Datatype::Short => "xs:short",
Datatype::String => "xs:string",
Datatype::Time => "xs:time",
Datatype::UserDefined(value) => &format!("x:{value}"),
Datatype::Other { prefix, value } => &format!("{prefix}:{value}"),
};
write!(f, "{value}")
}
}
impl IntoAttributeValue for Datatype {
fn into_attribute_value(self) -> Option {
Some(self.to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_datatype() -> Result<(), Error> {
assert_eq!(Datatype::AnyUri, "xs:anyURI".parse()?);
assert!("xs:anyuri".parse::().is_err());
assert!("xs:".parse::().is_err());
assert_eq!(
Datatype::AnyUri.into_attribute_value(),
Some("xs:anyURI".to_string())
);
assert_eq!(Datatype::UserDefined("id".to_string()), "x:id".parse()?);
assert_eq!(Datatype::UserDefined("".to_string()), "x:".parse()?);
assert_eq!(
Datatype::UserDefined("id".to_string()).into_attribute_value(),
Some("x:id".to_string())
);
assert_eq!(
Datatype::Other {
prefix: "geo".to_string(),
value: "lat".to_string()
},
"geo:lat".parse()?
);
assert_eq!(
Datatype::Other {
prefix: "geo".to_string(),
value: "".to_string()
},
"geo:".parse()?
);
assert_eq!(
Datatype::Other {
prefix: "geo".to_string(),
value: "lat".to_string()
}
.into_attribute_value(),
Some("geo:lat".to_string())
);
Ok(())
}
#[test]
fn test_parse_validate_element() -> Result<(), Error> {
let cases = [
(
r#""#,
Validate {
datatype: None,
method: None,
list_range: None,
},
),
(
r#""#,
Validate {
datatype: Some(Datatype::String),
method: Some(Method::Basic),
list_range: Some(ListRange {
min: Some(1),
max: Some(3),
}),
},
),
(
r#"([0-9]{3})-([0-9]{2})-([0-9]{4})"#,
Validate {
datatype: Some(Datatype::String),
method: Some(Method::Regex(
"([0-9]{3})-([0-9]{2})-([0-9]{4})".to_string(),
)),
list_range: None,
},
),
(
r#""#,
Validate {
datatype: Some(Datatype::DateTime),
method: Some(Method::Range {
min: Some("2003-10-05T00:00:00-07:00".to_string()),
max: Some("2003-10-24T23:59:59-07:00".to_string()),
}),
list_range: None,
},
),
];
for case in cases {
let parsed_element: Validate = case
.0
.parse::()
.expect(&format!("Failed to parse {}", case.0))
.try_into()?;
assert_eq!(parsed_element, case.1);
let xml = String::from(&Element::from(parsed_element));
assert_eq!(xml, case.0);
}
Ok(())
}
#[test]
fn test_fails_with_invalid_children() -> Result<(), Error> {
let cases = [
r#""#,
r#""#,
];
for case in cases {
let element = case
.parse::()
.expect(&format!("Failed to parse {}", case));
assert!(Validate::try_from(element).is_err());
}
Ok(())
}
}