Skip to main content

egml_io/codec/base/
association_attributes.rs

1use egml_core::Error;
2use egml_core::model::base::AssociationAttributes;
3use egml_core::model::xlink::{ActuateType, HRef, ShowType};
4use serde::{Deserialize, Serialize};
5
6/// Renders the `gml:AssociationAttributeGroup` (`xlink:href`, `xlink:title`, ...) as
7/// `(name, value)` pairs suitable for [`XmlNodeParts::attributes`](crate::util::XmlNodeParts).
8///
9/// Returns a `Vec` rather than a `HashMap` so attribute order is stable and deterministic —
10/// XML serialization output should not vary between runs.
11pub fn serialize_association_attributes(
12    association_attributes: &AssociationAttributes,
13) -> Vec<(String, String)> {
14    let mut attributes = Vec::new();
15
16    if let Some(href) = &association_attributes.href {
17        attributes.push(("xlink:href".to_string(), href.to_string()));
18    }
19    if let Some(title) = &association_attributes.title {
20        attributes.push(("xlink:title".to_string(), title.clone()));
21    }
22    if let Some(role) = &association_attributes.role {
23        attributes.push(("xlink:role".to_string(), role.clone()));
24    }
25    if let Some(arcrole) = &association_attributes.arcrole {
26        attributes.push(("xlink:arcrole".to_string(), arcrole.clone()));
27    }
28    if let Some(show) = &association_attributes.show {
29        attributes.push(("xlink:show".to_string(), show.to_string()));
30    }
31    if let Some(actuate) = &association_attributes.actuate {
32        attributes.push(("xlink:actuate".to_string(), actuate.to_string()));
33    }
34
35    attributes
36}
37
38#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Default)]
39pub struct GmlAssociationAttributes {
40    #[serde(
41        rename(deserialize = "@href", serialize = "@xlink:href"),
42        skip_serializing_if = "Option::is_none"
43    )]
44    pub href: Option<String>,
45
46    #[serde(
47        rename(deserialize = "@title", serialize = "@xlink:title"),
48        skip_serializing_if = "Option::is_none"
49    )]
50    pub title: Option<String>,
51
52    #[serde(
53        rename(deserialize = "@role", serialize = "@xlink:role"),
54        skip_serializing_if = "Option::is_none"
55    )]
56    pub role: Option<String>,
57
58    #[serde(
59        rename(deserialize = "@arcrole", serialize = "@xlink:arcrole"),
60        skip_serializing_if = "Option::is_none"
61    )]
62    pub arcrole: Option<String>,
63
64    #[serde(
65        rename(deserialize = "@show", serialize = "@xlink:show"),
66        skip_serializing_if = "Option::is_none"
67    )]
68    pub show: Option<String>,
69
70    #[serde(
71        rename(deserialize = "@actuate", serialize = "@xlink:actuate"),
72        skip_serializing_if = "Option::is_none"
73    )]
74    pub actuate: Option<String>,
75}
76
77impl TryFrom<GmlAssociationAttributes> for AssociationAttributes {
78    type Error = Error;
79
80    fn try_from(item: GmlAssociationAttributes) -> Result<Self, Self::Error> {
81        let show = item
82            .show
83            .map(|value| {
84                value
85                    .parse::<ShowType>()
86                    .map_err(|_| Error::InvalidAttributeValue {
87                        attribute: "xlink:show",
88                        value,
89                    })
90            })
91            .transpose()?;
92        let actuate = item
93            .actuate
94            .map(|value| {
95                value
96                    .parse::<ActuateType>()
97                    .map_err(|_| Error::InvalidAttributeValue {
98                        attribute: "xlink:actuate",
99                        value,
100                    })
101            })
102            .transpose()?;
103
104        Ok(Self {
105            href: item.href.map(HRef::from),
106            nil_reason: None,
107            title: item.title,
108            role: item.role,
109            arcrole: item.arcrole,
110            show,
111            actuate,
112        })
113    }
114}
115
116impl From<&AssociationAttributes> for GmlAssociationAttributes {
117    fn from(item: &AssociationAttributes) -> Self {
118        Self {
119            href: item.href.as_ref().map(|href| href.to_string()),
120            title: item.title.clone(),
121            role: item.role.clone(),
122            arcrole: item.arcrole.clone(),
123            show: item.show.as_ref().map(|show| show.to_string()),
124            actuate: item.actuate.as_ref().map(|actuate| actuate.to_string()),
125        }
126    }
127}
128
129#[cfg(test)]
130mod tests {
131    use super::GmlAssociationAttributes;
132    use egml_core::Error;
133    use egml_core::model::base::AssociationAttributes;
134    use egml_core::model::xlink::{ActuateType, HRef, ShowType};
135    use quick_xml::de;
136
137    #[test]
138    fn deserializes_full_attribute_group() {
139        let xml = b"<con:relatedTo xlink:href=\"#some-id\" xlink:title=\"Some Title\" \
140            xlink:role=\"http://example.com/role\" xlink:arcrole=\"http://example.com/arcrole\" \
141            xlink:show=\"new\" xlink:actuate=\"onLoad\"/>";
142
143        let parsed: GmlAssociationAttributes = de::from_reader(xml.as_ref()).unwrap();
144
145        assert_eq!(parsed.href.as_deref(), Some("#some-id"));
146        assert_eq!(parsed.title.as_deref(), Some("Some Title"));
147        assert_eq!(parsed.role.as_deref(), Some("http://example.com/role"));
148        assert_eq!(
149            parsed.arcrole.as_deref(),
150            Some("http://example.com/arcrole")
151        );
152        assert_eq!(parsed.show.as_deref(), Some("new"));
153        assert_eq!(parsed.actuate.as_deref(), Some("onLoad"));
154    }
155
156    #[test]
157    fn ignores_unrelated_attributes() {
158        let xml = b"<con:relatedTo gml:id=\"UUID_1\" gml:owns=\"true\" xlink:href=\"#some-id\"/>";
159
160        let parsed: GmlAssociationAttributes = de::from_reader(xml.as_ref()).unwrap();
161
162        assert_eq!(parsed.href.as_deref(), Some("#some-id"));
163    }
164
165    #[test]
166    fn defaults_to_empty() {
167        let xml = b"<con:relatedTo/>";
168
169        let parsed: GmlAssociationAttributes = de::from_reader(xml.as_ref()).unwrap();
170
171        assert_eq!(parsed, GmlAssociationAttributes::default());
172    }
173
174    #[test]
175    fn try_from_converts_full_attribute_group() {
176        let item = GmlAssociationAttributes {
177            href: Some("#some-id".to_string()),
178            title: Some("Some Title".to_string()),
179            role: Some("http://example.com/role".to_string()),
180            arcrole: Some("http://example.com/arcrole".to_string()),
181            show: Some("new".to_string()),
182            actuate: Some("onLoad".to_string()),
183        };
184
185        let association = AssociationAttributes::try_from(item).unwrap();
186
187        assert_eq!(association.href, Some(HRef::from_local("some-id")));
188        assert_eq!(association.title.as_deref(), Some("Some Title"));
189        assert_eq!(association.role.as_deref(), Some("http://example.com/role"));
190        assert_eq!(
191            association.arcrole.as_deref(),
192            Some("http://example.com/arcrole")
193        );
194        assert_eq!(association.show, Some(ShowType::New));
195        assert_eq!(association.actuate, Some(ActuateType::OnLoad));
196    }
197
198    #[test]
199    fn from_converts_full_attribute_group() {
200        let association = AssociationAttributes {
201            href: Some(HRef::from_local("some-id")),
202            nil_reason: None,
203            title: Some("Some Title".to_string()),
204            role: Some("http://example.com/role".to_string()),
205            arcrole: Some("http://example.com/arcrole".to_string()),
206            show: Some(ShowType::New),
207            actuate: Some(ActuateType::OnLoad),
208        };
209
210        let item = GmlAssociationAttributes::from(&association);
211
212        assert_eq!(item.href.as_deref(), Some("#some-id"));
213        assert_eq!(item.title.as_deref(), Some("Some Title"));
214        assert_eq!(item.role.as_deref(), Some("http://example.com/role"));
215        assert_eq!(item.arcrole.as_deref(), Some("http://example.com/arcrole"));
216        assert_eq!(item.show.as_deref(), Some("new"));
217        assert_eq!(item.actuate.as_deref(), Some("onLoad"));
218    }
219
220    #[test]
221    fn round_trip_through_gml_and_back() {
222        let association = AssociationAttributes {
223            href: Some(HRef::from_local("some-id")),
224            nil_reason: None,
225            title: Some("Some Title".to_string()),
226            role: Some("http://example.com/role".to_string()),
227            arcrole: Some("http://example.com/arcrole".to_string()),
228            show: Some(ShowType::New),
229            actuate: Some(ActuateType::OnLoad),
230        };
231
232        let item = GmlAssociationAttributes::from(&association);
233        let recovered = AssociationAttributes::try_from(item).unwrap();
234
235        assert_eq!(recovered, association);
236    }
237
238    #[test]
239    fn try_from_rejects_invalid_show_value() {
240        let item = GmlAssociationAttributes {
241            show: Some("bogus".to_string()),
242            ..Default::default()
243        };
244
245        let result = AssociationAttributes::try_from(item);
246
247        assert!(matches!(
248            result,
249            Err(Error::InvalidAttributeValue {
250                attribute: "xlink:show",
251                ..
252            })
253        ));
254    }
255}