1use egml_core::Error;
2use egml_core::model::base::AssociationAttributes;
3use egml_core::model::xlink::{ActuateType, HRef, ShowType};
4use serde::{Deserialize, Serialize};
5
6pub 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}