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