xmpp_parsers/
data_forms_validate.rs

1// Copyright (c) 2024 xmpp-rs contributors.
2//
3// This Source Code Form is subject to the terms of the Mozilla Public
4// License, v. 2.0. If a copy of the MPL was not distributed with this
5// file, You can obtain one at http://mozilla.org/MPL/2.0/.
6
7use alloc::borrow::Cow;
8use core::fmt;
9use core::str::FromStr;
10
11use minidom::IntoAttributeValue;
12use xso::{error::Error, AsXml, AsXmlText, FromXml, FromXmlText};
13
14use crate::ns;
15
16/// Validation Method
17#[derive(FromXml, AsXml, Debug, Clone, PartialEq)]
18#[xml(namespace = ns::XDATA_VALIDATE)]
19pub enum Method {
20    /// … to indicate that the value(s) should simply match the field type and datatype constraints,
21    /// the `<validate/>` element shall contain a `<basic/>` child element. Using `<basic/>` validation,
22    /// the form interpreter MUST follow the validation rules of the datatype (if understood) and
23    /// the field type.
24    ///
25    /// <https://xmpp.org/extensions/xep-0122.html#usercases-validation.basic>
26    #[xml(name = "basic")]
27    Basic,
28
29    /// For "list-single" or "list-multi", to indicate that the user may enter a custom value
30    /// (matching the datatype constraints) or choose from the predefined values, the `<validate/>`
31    /// element shall contain an `<open/>` child element. The `<open/>` validation method applies to
32    /// "text-multi" differently; it hints that each value for a "text-multi" field shall be
33    /// validated separately. This effectively turns "text-multi" fields into an open-ended
34    /// "list-multi", with no options and all values automatically selected.
35    ///
36    /// <https://xmpp.org/extensions/xep-0122.html#usercases-validation.open>
37    #[xml(name = "open")]
38    Open,
39
40    /// To indicate that the value should fall within a certain range, the `<validate/>` element shall
41    /// contain a `<range/>` child element. The 'min' and 'max' attributes of the `<range/>` element
42    /// specify the minimum and maximum values allowed, respectively.
43    ///
44    /// The 'max' attribute specifies the maximum allowable value. This attribute is OPTIONAL.
45    /// The value depends on the datatype in use.
46    ///
47    /// The 'min' attribute specifies the minimum allowable value. This attribute is OPTIONAL.
48    /// The value depends on the datatype in use.
49    ///
50    /// The `<range/>` element SHOULD possess either a 'min' or 'max' attribute, and MAY possess both.
51    /// If neither attribute is included, the processor MUST assume that there are no range
52    /// constraints.
53    ///
54    /// <https://xmpp.org/extensions/xep-0122.html#usercases-validation.range>
55    #[xml(name = "range")]
56    Range {
57        /// The 'min' attribute specifies the minimum allowable value.
58        #[xml(attribute(default))]
59        min: Option<String>,
60
61        /// The 'max' attribute specifies the maximum allowable value.
62        #[xml(attribute(default))]
63        max: Option<String>,
64    },
65
66    /// To indicate that the value should be restricted to a regular expression, the `<validate/>`
67    /// element shall contain a `<regex/>` child element. The XML character data of this element is
68    /// the pattern to apply. The syntax of this content MUST be that defined for POSIX extended
69    /// regular expressions, including support for Unicode. The `<regex/>` element MUST contain
70    /// character data only.
71    ///
72    /// <https://xmpp.org/extensions/xep-0122.html#usercases-validatoin.regex>
73    #[xml(name = "regex")]
74    Regex(#[xml(text)] String),
75}
76
77/// Selection Ranges in "list-multi"
78#[derive(FromXml, AsXml, PartialEq, Debug, Clone)]
79#[xml(namespace = ns::XDATA_VALIDATE, name = "list-range")]
80pub struct ListRange {
81    /// The 'min' attribute specifies the minimum allowable number of selected/entered values.
82    #[xml(attribute(default))]
83    pub min: Option<u32>,
84
85    /// The 'max' attribute specifies the maximum allowable number of selected/entered values.
86    #[xml(attribute(default))]
87    pub max: Option<u32>,
88}
89
90/// Enum representing errors that can occur while parsing a `Datatype`.
91#[derive(Debug, Clone, PartialEq)]
92pub enum DatatypeError {
93    /// Error indicating that a prefix is missing in the validation datatype.
94    MissingPrefix {
95        /// The invalid string that caused this error.
96        input: String,
97    },
98
99    /// Error indicating that the validation datatype is invalid.
100    InvalidType {
101        /// The invalid string that caused this error.
102        input: String,
103    },
104
105    /// Error indicating that the validation datatype is unknown.
106    UnknownType {
107        /// The invalid string that caused this error.
108        input: String,
109    },
110}
111
112impl core::error::Error for DatatypeError {}
113
114impl fmt::Display for DatatypeError {
115    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
116        match self {
117            DatatypeError::MissingPrefix { input } => {
118                write!(f, "Missing prefix in validation datatype {input:?}.")
119            }
120            DatatypeError::InvalidType { input } => {
121                write!(f, "Invalid validation datatype {input:?}.")
122            }
123            DatatypeError::UnknownType { input } => {
124                write!(f, "Unknown validation datatype {input:?}.")
125            }
126        }
127    }
128}
129
130/// Data Forms Validation Datatypes
131///
132/// <https://xmpp.org/registrar/xdv-datatypes.html>
133#[derive(Debug, Clone, PartialEq)]
134pub enum Datatype {
135    /// A Uniform Resource Identifier Reference (URI)
136    AnyUri,
137
138    /// An integer with the specified min/max
139    /// Min: -128, Max: 127
140    Byte,
141
142    /// A calendar date
143    Date,
144
145    /// A specific instant of time
146    DateTime,
147
148    /// An arbitrary-precision decimal number
149    Decimal,
150
151    /// An IEEE double-precision 64-bit floating point type
152    Double,
153
154    /// An integer with the specified min/max
155    /// Min: -2147483648, Max: 2147483647
156    Int,
157
158    /// A decimal number with no fraction digits
159    Integer,
160
161    /// A language identifier as defined by RFC 1766
162    Language,
163
164    /// An integer with the specified min/max
165    /// Min: -9223372036854775808, Max: 9223372036854775807
166    Long,
167
168    /// An integer with the specified min/max
169    /// Min: -32768, Max: 32767
170    Short,
171
172    /// A character strings in XML
173    String,
174
175    /// An instant of time that recurs every day
176    Time,
177
178    /// A user-defined datatype
179    UserDefined(String),
180
181    /// A non-standard datatype
182    Other {
183        /// The prefix of the specified datatype. Should be registered with the XMPP Registrar.
184        prefix: String,
185        /// The actual value of the specified datatype. E.g. "lat" in the case of "geo:lat".
186        value: String,
187    },
188}
189
190/// Validation rules for a DataForms Field.
191#[derive(FromXml, AsXml, Debug, Clone, PartialEq)]
192#[xml(namespace = ns::XDATA_VALIDATE, name = "validate")]
193pub struct Validate {
194    /// The 'datatype' attribute specifies the datatype. This attribute is OPTIONAL, and defaults
195    /// to "xs:string". It MUST meet one of the following conditions:
196    ///
197    /// - Start with "xs:", and be one of the "built-in" datatypes defined in XML Schema Part 2
198    /// - Start with a prefix registered with the XMPP Registrar
199    /// - Start with "x:", and specify a user-defined datatype.
200    ///
201    /// Note that while "x:" allows for ad-hoc definitions, its use is NOT RECOMMENDED.
202    #[xml(attribute(default))]
203    pub datatype: Option<Datatype>,
204
205    /// The validation method. If no validation method is specified, form processors MUST
206    /// assume `<basic/>` validation. The `<validate/>` element SHOULD include one of the above
207    /// validation method elements, and MUST NOT include more than one.
208    ///
209    /// Any validation method applied to a field of type "list-multi", "list-single", or "text-multi"
210    /// (other than `<basic/>`) MUST imply the same behavior as `<open/>`, with the additional constraints
211    /// defined by that method.
212    ///
213    /// <https://xmpp.org/extensions/xep-0122.html#usecases-validation>
214    #[xml(child(default))]
215    pub method: Option<Method>,
216
217    /// For "list-multi", validation can indicate (via the `<list-range/>` element) that a minimum
218    /// and maximum number of options should be selected and/or entered. This selection range
219    /// MAY be combined with the other methods to provide more flexibility.
220    /// The `<list-range/>` element SHOULD be included only when the `<field/>` is of type "list-multi"
221    /// and SHOULD be ignored otherwise.
222    ///
223    /// The `<list-range/>` element SHOULD possess either a 'min' or 'max' attribute, and MAY possess
224    /// both. If neither attribute is included, the processor MUST assume that there are no
225    /// selection constraints.
226    ///
227    /// <https://xmpp.org/extensions/xep-0122.html#usecases-ranges>
228    #[xml(child(default))]
229    pub list_range: Option<ListRange>,
230}
231
232impl FromStr for Datatype {
233    type Err = DatatypeError;
234
235    fn from_str(s: &str) -> Result<Self, Self::Err> {
236        let mut parts = s.splitn(2, ":");
237
238        let Some(prefix) = parts.next() else {
239            return Err(DatatypeError::MissingPrefix {
240                input: s.to_string(),
241            });
242        };
243
244        match prefix {
245            "xs" => (),
246            "x" => {
247                return Ok(Datatype::UserDefined(
248                    parts.next().unwrap_or_default().to_string(),
249                ))
250            }
251            _ => {
252                return Ok(Datatype::Other {
253                    prefix: prefix.to_string(),
254                    value: parts.next().unwrap_or_default().to_string(),
255                })
256            }
257        }
258
259        let Some(datatype) = parts.next() else {
260            return Err(DatatypeError::InvalidType {
261                input: s.to_string(),
262            });
263        };
264
265        let parsed_datatype = match datatype {
266            "anyURI" => Datatype::AnyUri,
267            "byte" => Datatype::Byte,
268            "date" => Datatype::Date,
269            "dateTime" => Datatype::DateTime,
270            "decimal" => Datatype::Decimal,
271            "double" => Datatype::Double,
272            "int" => Datatype::Int,
273            "integer" => Datatype::Integer,
274            "language" => Datatype::Language,
275            "long" => Datatype::Long,
276            "short" => Datatype::Short,
277            "string" => Datatype::String,
278            "time" => Datatype::Time,
279            _ => {
280                return Err(DatatypeError::UnknownType {
281                    input: s.to_string(),
282                })
283            }
284        };
285
286        Ok(parsed_datatype)
287    }
288}
289
290impl fmt::Display for Datatype {
291    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
292        let value = match self {
293            Datatype::AnyUri => "xs:anyURI",
294            Datatype::Byte => "xs:byte",
295            Datatype::Date => "xs:date",
296            Datatype::DateTime => "xs:dateTime",
297            Datatype::Decimal => "xs:decimal",
298            Datatype::Double => "xs:double",
299            Datatype::Int => "xs:int",
300            Datatype::Integer => "xs:integer",
301            Datatype::Language => "xs:language",
302            Datatype::Long => "xs:long",
303            Datatype::Short => "xs:short",
304            Datatype::String => "xs:string",
305            Datatype::Time => "xs:time",
306            Datatype::UserDefined(value) => return write!(f, "x:{value}"),
307            Datatype::Other { prefix, value } => return write!(f, "{prefix}:{value}"),
308        };
309        f.write_str(value)
310    }
311}
312
313impl IntoAttributeValue for Datatype {
314    fn into_attribute_value(self) -> Option<String> {
315        Some(self.to_string())
316    }
317}
318
319impl FromXmlText for Datatype {
320    fn from_xml_text(s: String) -> Result<Datatype, Error> {
321        s.parse().map_err(Error::text_parse_error)
322    }
323}
324
325impl AsXmlText for Datatype {
326    fn as_xml_text(&self) -> Result<Cow<'_, str>, Error> {
327        Ok(Cow::Owned(self.to_string()))
328    }
329}
330
331#[cfg(test)]
332mod tests {
333    use super::*;
334    use minidom::Element;
335
336    #[test]
337    fn test_parse_datatype() -> Result<(), DatatypeError> {
338        assert_eq!(Datatype::AnyUri, "xs:anyURI".parse()?);
339        assert_eq!(
340            Err(DatatypeError::UnknownType {
341                input: "xs:anyuri".to_string()
342            }),
343            "xs:anyuri".parse::<Datatype>(),
344        );
345        assert_eq!(
346            "xs:".parse::<Datatype>(),
347            Err(DatatypeError::UnknownType {
348                input: "xs:".to_string()
349            })
350        );
351        assert_eq!(
352            Datatype::AnyUri.into_attribute_value(),
353            Some("xs:anyURI".to_string())
354        );
355
356        assert_eq!(Datatype::UserDefined("id".to_string()), "x:id".parse()?);
357        assert_eq!(Datatype::UserDefined("".to_string()), "x:".parse()?);
358        assert_eq!(
359            Datatype::UserDefined("id".to_string()).into_attribute_value(),
360            Some("x:id".to_string())
361        );
362
363        assert_eq!(
364            Datatype::Other {
365                prefix: "geo".to_string(),
366                value: "lat".to_string()
367            },
368            "geo:lat".parse()?
369        );
370        assert_eq!(
371            Datatype::Other {
372                prefix: "geo".to_string(),
373                value: "".to_string()
374            },
375            "geo:".parse()?
376        );
377        assert_eq!(
378            Datatype::Other {
379                prefix: "geo".to_string(),
380                value: "lat".to_string()
381            }
382            .into_attribute_value(),
383            Some("geo:lat".to_string())
384        );
385
386        Ok(())
387    }
388
389    #[test]
390    fn test_parse_validate_element() -> Result<(), Error> {
391        let cases = [
392            (
393                r#"<validate xmlns='http://jabber.org/protocol/xdata-validate'/>"#,
394                Validate {
395                    datatype: None,
396                    method: None,
397                    list_range: None,
398                },
399            ),
400            (
401                r#"<validate xmlns='http://jabber.org/protocol/xdata-validate' datatype="xs:string"><basic/><list-range max="3" min="1"/></validate>"#,
402                Validate {
403                    datatype: Some(Datatype::String),
404                    method: Some(Method::Basic),
405                    list_range: Some(ListRange {
406                        min: Some(1),
407                        max: Some(3),
408                    }),
409                },
410            ),
411            (
412                r#"<validate xmlns='http://jabber.org/protocol/xdata-validate' datatype="xs:string"><regex>([0-9]{3})-([0-9]{2})-([0-9]{4})</regex></validate>"#,
413                Validate {
414                    datatype: Some(Datatype::String),
415                    method: Some(Method::Regex(
416                        "([0-9]{3})-([0-9]{2})-([0-9]{4})".to_string(),
417                    )),
418                    list_range: None,
419                },
420            ),
421            (
422                r#"<validate xmlns='http://jabber.org/protocol/xdata-validate' datatype="xs:dateTime"><range max="2003-10-24T23:59:59-07:00" min="2003-10-05T00:00:00-07:00"/></validate>"#,
423                Validate {
424                    datatype: Some(Datatype::DateTime),
425                    method: Some(Method::Range {
426                        min: Some("2003-10-05T00:00:00-07:00".to_string()),
427                        max: Some("2003-10-24T23:59:59-07:00".to_string()),
428                    }),
429                    list_range: None,
430                },
431            ),
432        ];
433
434        for case in cases {
435            let parsed_element: Validate = case
436                .0
437                .parse::<Element>()
438                .expect(&format!("Failed to parse {}", case.0))
439                .try_into()?;
440
441            assert_eq!(parsed_element, case.1);
442
443            let xml = String::from(&Element::from(parsed_element));
444            assert_eq!(xml, case.0);
445        }
446
447        Ok(())
448    }
449
450    #[test]
451    #[cfg_attr(
452        feature = "disable-validation",
453        should_panic = "Validate::try_from(element).is_err()"
454    )]
455    fn test_fails_with_invalid_children() {
456        let cases = [
457            r#"<validate xmlns='http://jabber.org/protocol/xdata-validate'><basic /><open /></validate>"#,
458            r#"<validate xmlns='http://jabber.org/protocol/xdata-validate'><unknown /></validate>"#,
459        ];
460
461        for case in cases {
462            let element = case
463                .parse::<Element>()
464                .expect(&format!("Failed to parse {}", case));
465            assert!(Validate::try_from(element).is_err());
466        }
467    }
468}