1use indexmap::{IndexMap, IndexSet};
8use serde::{Serialize, Deserialize, Serializer, Deserializer};
9use std::fmt::{self, Display, Debug};
10use std::str::FromStr;
11use thiserror::Error;
12
13#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
15pub struct QName {
16 pub local_name: String,
18 pub namespace_uri: Option<String>,
20 pub prefix: Option<String>,
22}
23
24impl QName {
25 pub fn new(local_name: impl Into<String>) -> Self {
27 Self {
28 local_name: local_name.into(),
29 namespace_uri: None,
30 prefix: None,
31 }
32 }
33
34 pub fn with_namespace(local_name: impl Into<String>, namespace_uri: impl Into<String>) -> Self {
36 Self {
37 local_name: local_name.into(),
38 namespace_uri: Some(namespace_uri.into()),
39 prefix: None,
40 }
41 }
42
43 pub fn with_prefix_and_namespace(
45 local_name: impl Into<String>,
46 prefix: impl Into<String>,
47 namespace_uri: impl Into<String>
48 ) -> Self {
49 Self {
50 local_name: local_name.into(),
51 namespace_uri: Some(namespace_uri.into()),
52 prefix: Some(prefix.into()),
53 }
54 }
55
56 pub fn to_xml_name(&self) -> String {
58 match &self.prefix {
59 Some(prefix) if !prefix.is_empty() => format!("{}:{}", prefix, self.local_name),
60 _ => self.local_name.clone(),
61 }
62 }
63
64 pub fn is_namespace_declaration(&self) -> bool {
66 self.local_name == "xmlns" ||
67 (self.prefix.as_deref() == Some("xmlns"))
68 }
69
70 pub fn is_ddex_standard(&self) -> bool {
72 match &self.namespace_uri {
73 Some(uri) => {
74 uri.contains("ddex.net") ||
75 uri.contains("w3.org/2001/XMLSchema") ||
76 self.is_namespace_declaration()
77 },
78 None => {
79 matches!(self.local_name.as_str(),
81 "LanguageAndScriptCode" | "ApplicableTerritoryCode" |
82 "IsDefault" | "SequenceNumber" | "Namespace"
83 )
84 }
85 }
86 }
87
88 pub fn canonical_sort_key(&self) -> String {
90 if self.is_namespace_declaration() {
92 if self.local_name == "xmlns" {
93 "0:xmlns".to_string()
94 } else {
95 format!("0:xmlns:{}", self.local_name)
96 }
97 } else {
98 format!("1:{}", self.to_xml_name())
99 }
100 }
101}
102
103impl Display for QName {
104 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
105 write!(f, "{}", self.to_xml_name())
106 }
107}
108
109impl FromStr for QName {
110 type Err = AttributeError;
111
112 fn from_str(s: &str) -> Result<Self, Self::Err> {
113 if let Some((prefix, local_name)) = s.split_once(':') {
114 Ok(QName {
115 local_name: local_name.to_string(),
116 namespace_uri: None, prefix: Some(prefix.to_string()),
118 })
119 } else {
120 Ok(QName::new(s))
121 }
122 }
123}
124
125impl PartialOrd for QName {
126 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
127 Some(self.cmp(other))
128 }
129}
130
131impl Ord for QName {
132 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
133 self.canonical_sort_key().cmp(&other.canonical_sort_key())
134 }
135}
136
137#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
139pub enum AttributeValue {
140 String(String),
142 Boolean(bool),
144 Integer(i64),
146 Decimal(f64),
148 Date(chrono::NaiveDate),
150 DateTime(chrono::DateTime<chrono::Utc>),
152 Duration(chrono::Duration),
154 Uri(String),
156 Language(String),
158 Token(String),
160 Enum(String, Vec<String>), Raw(String),
164}
165
166impl AttributeValue {
167 pub fn string(value: impl Into<String>) -> Self {
169 Self::String(value.into())
170 }
171
172 pub fn boolean(value: bool) -> Self {
174 Self::Boolean(value)
175 }
176
177 pub fn integer(value: i64) -> Self {
179 Self::Integer(value)
180 }
181
182 pub fn decimal(value: f64) -> Self {
184 Self::Decimal(value)
185 }
186
187 pub fn uri(value: impl Into<String>) -> Self {
189 Self::Uri(value.into())
190 }
191
192 pub fn raw(value: impl Into<String>) -> Self {
194 Self::Raw(value.into())
195 }
196
197 pub fn to_xml_value(&self) -> String {
199 match self {
200 AttributeValue::String(s) => s.clone(),
201 AttributeValue::Boolean(b) => b.to_string(),
202 AttributeValue::Integer(i) => i.to_string(),
203 AttributeValue::Decimal(d) => d.to_string(),
204 AttributeValue::Date(d) => d.format("%Y-%m-%d").to_string(),
205 AttributeValue::DateTime(dt) => dt.to_rfc3339(),
206 AttributeValue::Duration(dur) => {
207 let secs = dur.num_seconds();
209 format!("PT{}S", secs)
210 },
211 AttributeValue::Uri(uri) => uri.clone(),
212 AttributeValue::Language(lang) => lang.clone(),
213 AttributeValue::Token(token) => token.clone(),
214 AttributeValue::Enum(value, _) => value.clone(),
215 AttributeValue::Raw(raw) => raw.clone(),
216 }
217 }
218
219 pub fn parse_with_type(value: &str, type_hint: AttributeType) -> Result<Self, AttributeError> {
221 match type_hint {
222 AttributeType::String => Ok(AttributeValue::String(value.to_string())),
223 AttributeType::Boolean => {
224 match value.to_lowercase().as_str() {
225 "true" | "1" => Ok(AttributeValue::Boolean(true)),
226 "false" | "0" => Ok(AttributeValue::Boolean(false)),
227 _ => Err(AttributeError::InvalidBoolean(value.to_string())),
228 }
229 },
230 AttributeType::Integer => {
231 value.parse::<i64>()
232 .map(AttributeValue::Integer)
233 .map_err(|_| AttributeError::InvalidInteger(value.to_string()))
234 },
235 AttributeType::Decimal => {
236 value.parse::<f64>()
237 .map(AttributeValue::Decimal)
238 .map_err(|_| AttributeError::InvalidDecimal(value.to_string()))
239 },
240 AttributeType::Date => {
241 chrono::NaiveDate::parse_from_str(value, "%Y-%m-%d")
242 .map(AttributeValue::Date)
243 .map_err(|_| AttributeError::InvalidDate(value.to_string()))
244 },
245 AttributeType::DateTime => {
246 chrono::DateTime::parse_from_rfc3339(value)
247 .map(|dt| AttributeValue::DateTime(dt.with_timezone(&chrono::Utc)))
248 .map_err(|_| AttributeError::InvalidDateTime(value.to_string()))
249 },
250 AttributeType::Uri => Ok(AttributeValue::Uri(value.to_string())),
251 AttributeType::Language => Ok(AttributeValue::Language(value.to_string())),
252 AttributeType::Token => Ok(AttributeValue::Token(value.trim().to_string())),
253 AttributeType::Raw => Ok(AttributeValue::Raw(value.to_string())),
254 }
255 }
256
257 pub fn validate(&self) -> Result<(), AttributeError> {
259 match self {
260 AttributeValue::Enum(value, allowed_values) => {
261 if allowed_values.contains(value) {
262 Ok(())
263 } else {
264 Err(AttributeError::InvalidEnumValue {
265 value: value.clone(),
266 allowed: allowed_values.clone(),
267 })
268 }
269 },
270 AttributeValue::Uri(uri) => {
271 if uri.contains(' ') || uri.is_empty() {
273 Err(AttributeError::InvalidUri(uri.clone()))
274 } else {
275 Ok(())
276 }
277 },
278 AttributeValue::Language(lang) => {
279 if lang.len() < 2 || lang.len() > 8 {
281 Err(AttributeError::InvalidLanguage(lang.clone()))
282 } else {
283 Ok(())
284 }
285 },
286 _ => Ok(()),
287 }
288 }
289}
290
291impl Display for AttributeValue {
292 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
293 write!(f, "{}", self.to_xml_value())
294 }
295}
296
297impl From<String> for AttributeValue {
298 fn from(value: String) -> Self {
299 AttributeValue::String(value)
300 }
301}
302
303impl From<&str> for AttributeValue {
304 fn from(value: &str) -> Self {
305 AttributeValue::String(value.to_string())
306 }
307}
308
309impl From<bool> for AttributeValue {
310 fn from(value: bool) -> Self {
311 AttributeValue::Boolean(value)
312 }
313}
314
315impl From<i64> for AttributeValue {
316 fn from(value: i64) -> Self {
317 AttributeValue::Integer(value)
318 }
319}
320
321impl From<f64> for AttributeValue {
322 fn from(value: f64) -> Self {
323 AttributeValue::Decimal(value)
324 }
325}
326
327#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
329pub enum AttributeType {
330 String,
331 Boolean,
332 Integer,
333 Decimal,
334 Date,
335 DateTime,
336 Uri,
337 Language,
338 Token,
339 Raw,
340}
341
342impl std::fmt::Display for AttributeType {
343 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
344 match self {
345 AttributeType::String => write!(f, "string"),
346 AttributeType::Boolean => write!(f, "boolean"),
347 AttributeType::Integer => write!(f, "integer"),
348 AttributeType::Decimal => write!(f, "decimal"),
349 AttributeType::Date => write!(f, "date"),
350 AttributeType::DateTime => write!(f, "dateTime"),
351 AttributeType::Uri => write!(f, "anyURI"),
352 AttributeType::Language => write!(f, "language"),
353 AttributeType::Token => write!(f, "token"),
354 AttributeType::Raw => write!(f, "raw"),
355 }
356 }
357}
358
359#[derive(Debug, Clone, PartialEq)]
361pub struct AttributeMap {
362 attributes: IndexMap<QName, AttributeValue>,
364}
365
366impl AttributeMap {
367 pub fn new() -> Self {
369 Self {
370 attributes: IndexMap::new(),
371 }
372 }
373
374 pub fn insert(&mut self, name: QName, value: AttributeValue) -> Option<AttributeValue> {
376 self.attributes.insert(name, value)
377 }
378
379 pub fn insert_str(&mut self, name: &str, value: impl Into<AttributeValue>) -> Option<AttributeValue> {
381 let qname = QName::from_str(name).unwrap_or_else(|_| QName::new(name));
382 self.insert(qname, value.into())
383 }
384
385 pub fn get(&self, name: &QName) -> Option<&AttributeValue> {
387 self.attributes.get(name)
388 }
389
390 pub fn get_str(&self, name: &str) -> Option<&AttributeValue> {
392 let qname = QName::from_str(name).unwrap_or_else(|_| QName::new(name));
393 self.get(&qname)
394 }
395
396 pub fn remove(&mut self, name: &QName) -> Option<AttributeValue> {
398 self.attributes.shift_remove(name)
399 }
400
401 pub fn contains_key(&self, name: &QName) -> bool {
403 self.attributes.contains_key(name)
404 }
405
406 pub fn iter_canonical(&self) -> impl Iterator<Item = (&QName, &AttributeValue)> {
408 let mut sorted: Vec<_> = self.attributes.iter().collect();
409 sorted.sort_by(|(a, _), (b, _)| a.cmp(b));
410 sorted.into_iter()
411 }
412
413 pub fn iter(&self) -> impl Iterator<Item = (&QName, &AttributeValue)> {
415 self.attributes.iter()
416 }
417
418 pub fn iter_mut(&mut self) -> impl Iterator<Item = (&QName, &mut AttributeValue)> {
420 self.attributes.iter_mut()
421 }
422
423 pub fn len(&self) -> usize {
425 self.attributes.len()
426 }
427
428 pub fn is_empty(&self) -> bool {
430 self.attributes.is_empty()
431 }
432
433 pub fn clear(&mut self) {
435 self.attributes.clear();
436 }
437
438 pub fn standard_attributes(&self) -> IndexMap<QName, AttributeValue> {
440 self.attributes.iter()
441 .filter(|(qname, _)| qname.is_ddex_standard())
442 .map(|(qname, value)| (qname.clone(), value.clone()))
443 .collect()
444 }
445
446 pub fn extension_attributes(&self) -> IndexMap<QName, AttributeValue> {
448 self.attributes.iter()
449 .filter(|(qname, _)| !qname.is_ddex_standard())
450 .map(|(qname, value)| (qname.clone(), value.clone()))
451 .collect()
452 }
453
454 pub fn namespace_declarations(&self) -> IndexMap<QName, AttributeValue> {
456 self.attributes.iter()
457 .filter(|(qname, _)| qname.is_namespace_declaration())
458 .map(|(qname, value)| (qname.clone(), value.clone()))
459 .collect()
460 }
461
462 pub fn merge(&mut self, other: &AttributeMap, strategy: AttributeMergeStrategy) {
464 for (qname, value) in &other.attributes {
465 if let Some(_existing) = self.attributes.get(qname) {
466 match strategy {
467 AttributeMergeStrategy::PreferThis => continue,
468 AttributeMergeStrategy::PreferOther => {
469 self.attributes.insert(qname.clone(), value.clone());
470 },
471 AttributeMergeStrategy::Error => {
472 eprintln!("Attribute conflict: {}", qname);
474 },
475 }
476 } else {
477 self.attributes.insert(qname.clone(), value.clone());
478 }
479 }
480 }
481
482 pub fn validate(&self) -> Vec<AttributeError> {
484 let mut errors = Vec::new();
485 for (_qname, value) in &self.attributes {
486 if let Err(error) = value.validate() {
487 errors.push(error);
488 }
489 }
490 errors
491 }
492
493 pub fn to_string_map(&self) -> IndexMap<String, String> {
495 self.attributes.iter()
496 .map(|(qname, value)| (qname.to_xml_name(), value.to_xml_value()))
497 .collect()
498 }
499
500 pub fn from_string_map(map: IndexMap<String, String>) -> Self {
502 let mut attributes = IndexMap::new();
503 for (name, value) in map {
504 let qname = QName::from_str(&name).unwrap_or_else(|_| QName::new(name));
505 attributes.insert(qname, AttributeValue::String(value));
506 }
507 Self { attributes }
508 }
509
510 pub fn keys(&self) -> indexmap::map::Keys<'_, QName, AttributeValue> {
512 self.attributes.keys()
513 }
514
515 pub fn to_canonical_ordered(&self) -> IndexMap<QName, AttributeValue> {
517 let mut namespace_attrs = IndexMap::new();
518 let mut regular_attrs = IndexMap::new();
519
520 for (qname, value) in &self.attributes {
522 if qname.is_namespace_declaration() {
523 namespace_attrs.insert(qname.clone(), value.clone());
524 } else {
525 regular_attrs.insert(qname.clone(), value.clone());
526 }
527 }
528
529 namespace_attrs.sort_by(|a, _, b, _| a.canonical_sort_key().cmp(&b.canonical_sort_key()));
531 regular_attrs.sort_by(|a, _, b, _| a.canonical_sort_key().cmp(&b.canonical_sort_key()));
532
533 let mut result = IndexMap::new();
535 result.extend(namespace_attrs);
536 result.extend(regular_attrs);
537
538 result
539 }
540
541}
542
543impl Default for AttributeMap {
544 fn default() -> Self {
545 Self::new()
546 }
547}
548
549impl Serialize for AttributeMap {
550 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
551 where
552 S: Serializer,
553 {
554 let string_map = self.to_string_map();
556 string_map.serialize(serializer)
557 }
558}
559
560impl<'de> Deserialize<'de> for AttributeMap {
561 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
562 where
563 D: Deserializer<'de>,
564 {
565 let string_map = IndexMap::<String, String>::deserialize(deserializer)?;
566 Ok(Self::from_string_map(string_map))
567 }
568}
569
570impl<'a> IntoIterator for &'a AttributeMap {
571 type Item = (&'a QName, &'a AttributeValue);
572 type IntoIter = indexmap::map::Iter<'a, QName, AttributeValue>;
573
574 fn into_iter(self) -> Self::IntoIter {
575 self.attributes.iter()
576 }
577}
578
579#[derive(Debug, Clone, Copy, PartialEq, Eq)]
581pub enum AttributeMergeStrategy {
582 PreferThis,
584 PreferOther,
586 Error,
588}
589
590#[derive(Debug, Clone)]
592pub struct AttributeInheritance {
593 inheritable_attributes: IndexSet<QName>,
595 non_inheritable_attributes: IndexSet<QName>,
597}
598
599impl AttributeInheritance {
600 pub fn new() -> Self {
602 let mut inheritable = IndexSet::new();
603 let mut non_inheritable = IndexSet::new();
604
605 inheritable.insert(QName::new("LanguageAndScriptCode"));
607 inheritable.insert(QName::new("ApplicableTerritoryCode"));
608 inheritable.insert(QName::with_namespace("lang", "http://www.w3.org/XML/1998/namespace"));
609
610 non_inheritable.insert(QName::new("SequenceNumber"));
612 non_inheritable.insert(QName::with_prefix_and_namespace("xsi", "type", "http://www.w3.org/2001/XMLSchema-instance"));
613
614 Self {
615 inheritable_attributes: inheritable,
616 non_inheritable_attributes: non_inheritable,
617 }
618 }
619
620 pub fn should_inherit(&self, qname: &QName) -> bool {
622 if self.non_inheritable_attributes.contains(qname) {
623 false
624 } else if self.inheritable_attributes.contains(qname) {
625 true
626 } else {
627 false
629 }
630 }
631
632 pub fn apply_inheritance(&self, parent: &AttributeMap, child: &mut AttributeMap) {
634 for (qname, value) in parent.iter() {
635 if self.should_inherit(qname) && !child.contains_key(qname) {
636 child.insert(qname.clone(), value.clone());
637 }
638 }
639 }
640}
641
642impl Default for AttributeInheritance {
643 fn default() -> Self {
644 Self::new()
645 }
646}
647
648#[derive(Debug, Clone, Error, PartialEq)]
650pub enum AttributeError {
651 #[error("Invalid boolean value: {0}")]
652 InvalidBoolean(String),
653
654 #[error("Invalid integer value: {0}")]
655 InvalidInteger(String),
656
657 #[error("Invalid decimal value: {0}")]
658 InvalidDecimal(String),
659
660 #[error("Invalid date value: {0}")]
661 InvalidDate(String),
662
663 #[error("Invalid datetime value: {0}")]
664 InvalidDateTime(String),
665
666 #[error("Invalid URI value: {0}")]
667 InvalidUri(String),
668
669 #[error("Invalid language code: {0}")]
670 InvalidLanguage(String),
671
672 #[error("Invalid enum value '{value}', allowed values: {}", allowed.join(", "))]
673 InvalidEnumValue {
674 value: String,
675 allowed: Vec<String>,
676 },
677
678 #[error("Missing required attribute: {0}")]
679 MissingRequired(String),
680
681 #[error("Conflicting attribute values for: {0}")]
682 ConflictingValues(String),
683
684 #[error("Invalid QName format: {0}")]
685 InvalidQName(String),
686}
687
688#[cfg(test)]
689mod tests {
690 use super::*;
691
692 #[test]
693 fn test_qname_creation() {
694 let qname = QName::new("title");
695 assert_eq!(qname.local_name, "title");
696 assert_eq!(qname.namespace_uri, None);
697 assert_eq!(qname.prefix, None);
698
699 let qname_ns = QName::with_namespace("title", "http://ddex.net/xml/ern/43");
700 assert_eq!(qname_ns.namespace_uri, Some("http://ddex.net/xml/ern/43".to_string()));
701
702 let qname_prefix = QName::with_prefix_and_namespace("title", "ern", "http://ddex.net/xml/ern/43");
703 assert_eq!(qname_prefix.prefix, Some("ern".to_string()));
704 assert_eq!(qname_prefix.to_xml_name(), "ern:title");
705 }
706
707 #[test]
708 fn test_qname_parsing() {
709 let qname: QName = "ern:title".parse().unwrap();
710 assert_eq!(qname.local_name, "title");
711 assert_eq!(qname.prefix, Some("ern".to_string()));
712
713 let simple_qname: QName = "title".parse().unwrap();
714 assert_eq!(simple_qname.local_name, "title");
715 assert_eq!(simple_qname.prefix, None);
716 }
717
718 #[test]
719 fn test_qname_canonical_ordering() {
720 let xmlns = QName::new("xmlns");
721 let xmlns_ern = QName::from_str("xmlns:ern").unwrap();
722 let regular = QName::new("title");
723 let prefixed = QName::from_str("ern:title").unwrap();
724
725 let mut qnames = vec![®ular, &prefixed, &xmlns_ern, &xmlns];
726 qnames.sort();
727
728 assert_eq!(qnames[0], &xmlns);
730 assert_eq!(qnames[1], &xmlns_ern);
731 }
732
733 #[test]
734 fn test_attribute_value_types() {
735 let string_val = AttributeValue::string("test");
736 assert_eq!(string_val.to_xml_value(), "test");
737
738 let bool_val = AttributeValue::boolean(true);
739 assert_eq!(bool_val.to_xml_value(), "true");
740
741 let int_val = AttributeValue::integer(42);
742 assert_eq!(int_val.to_xml_value(), "42");
743
744 let parsed = AttributeValue::parse_with_type("true", AttributeType::Boolean).unwrap();
746 assert_eq!(parsed, AttributeValue::Boolean(true));
747
748 let parsed_int = AttributeValue::parse_with_type("123", AttributeType::Integer).unwrap();
749 assert_eq!(parsed_int, AttributeValue::Integer(123));
750 }
751
752 #[test]
753 fn test_attribute_map() {
754 let mut map = AttributeMap::new();
755
756 map.insert_str("title", "Test Title");
757 map.insert_str("ern:version", "4.3");
758 map.insert_str("xmlns:ern", "http://ddex.net/xml/ern/43");
759
760 assert_eq!(map.len(), 3);
761 assert_eq!(map.get_str("title").unwrap().to_xml_value(), "Test Title");
762
763 let canonical: Vec<_> = map.iter_canonical().collect();
765 assert_eq!(canonical.len(), 3);
766
767 let first_attr = &canonical[0];
769 assert!(first_attr.0.is_namespace_declaration());
770 }
771
772 #[test]
773 fn test_attribute_inheritance() {
774 let inheritance = AttributeInheritance::new();
775
776 let lang_attr = QName::new("LanguageAndScriptCode");
777 let seq_attr = QName::new("SequenceNumber");
778
779 assert!(inheritance.should_inherit(&lang_attr));
780 assert!(!inheritance.should_inherit(&seq_attr));
781 }
782
783 #[test]
784 fn test_attribute_validation() {
785 let mut enum_val = AttributeValue::Enum(
786 "invalid".to_string(),
787 vec!["valid1".to_string(), "valid2".to_string()]
788 );
789 assert!(enum_val.validate().is_err());
790
791 enum_val = AttributeValue::Enum(
792 "valid1".to_string(),
793 vec!["valid1".to_string(), "valid2".to_string()]
794 );
795 assert!(enum_val.validate().is_ok());
796 }
797
798 #[test]
799 fn test_ddex_standard_detection() {
800 let ddex_attr = QName::with_namespace("title", "http://ddex.net/xml/ern/43");
801 assert!(ddex_attr.is_ddex_standard());
802
803 let xmlns_attr = QName::new("xmlns");
804 assert!(xmlns_attr.is_ddex_standard());
805
806 let custom_attr = QName::with_namespace("custom", "http://example.com/custom");
807 assert!(!custom_attr.is_ddex_standard());
808
809 let lang_attr = QName::new("LanguageAndScriptCode");
810 assert!(lang_attr.is_ddex_standard());
811 }
812
813 #[test]
814 fn test_attribute_map_serialization() {
815 let mut map = AttributeMap::new();
816 map.insert_str("title", "Test Title");
817 map.insert_str("version", "4.3");
818
819 let string_map = map.to_string_map();
821 assert_eq!(string_map.len(), 2);
822 assert_eq!(string_map.get("title"), Some(&"Test Title".to_string()));
823
824 let restored = AttributeMap::from_string_map(string_map);
826 assert_eq!(restored.len(), 2);
827 assert_eq!(restored.get_str("title").unwrap().to_xml_value(), "Test Title");
828 }
829}