1use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5
6pub const DESCRIPTOR_FUNCTION_ID: &str = "60000000-0000-0000-0000-000000000001";
8
9#[derive(Clone, Debug, Serialize, Deserialize, Default)]
11pub struct StaticResourceDescriptors {
12 #[serde(skip_serializing_if = "Option::is_none")]
13 pub name: Option<String>,
14 #[serde(skip_serializing_if = "Option::is_none")]
15 pub map_popup: Option<String>,
16 #[serde(skip_serializing_if = "Option::is_none")]
17 pub description: Option<String>,
18 #[serde(skip_serializing_if = "Option::is_none")]
19 pub slug: Option<String>,
20}
21
22impl StaticResourceDescriptors {
23 pub fn is_empty(&self) -> bool {
25 self.name.is_none()
26 && self.map_popup.is_none()
27 && self.description.is_none()
28 && self.slug.is_none()
29 }
30
31 pub fn empty() -> Self {
33 Self::default()
34 }
35}
36
37#[derive(Clone, Debug, Serialize, Deserialize)]
44pub struct DescriptorTypeConfig {
45 #[serde(default, skip_serializing_if = "Option::is_none")]
47 pub nodegroup_id: Option<String>,
48 #[serde(default, skip_serializing_if = "Option::is_none")]
50 pub nodegroup_ids: Option<Vec<String>>,
51 pub string_template: String,
52}
53
54impl DescriptorTypeConfig {
55 pub fn all_nodegroup_ids(&self) -> Vec<&str> {
57 if let Some(ids) = &self.nodegroup_ids {
58 ids.iter().map(|s| s.as_str()).collect()
59 } else if let Some(id) = &self.nodegroup_id {
60 vec![id.as_str()]
61 } else {
62 vec![]
63 }
64 }
65
66 pub fn is_multi_nodegroup(&self) -> bool {
68 self.nodegroup_ids.as_ref().is_some_and(|ids| ids.len() > 1)
69 }
70}
71
72#[derive(Clone, Debug, Serialize, Deserialize)]
74pub struct DescriptorConfig {
75 pub descriptor_types: HashMap<String, DescriptorTypeConfig>,
76}
77
78#[cfg(test)]
79mod tests {
80 use super::*;
81 use crate::graph::{IndexedGraph, StaticGraph, StaticTile};
82
83 fn build_test_graph(descriptor_types: Vec<(&str, &str, &str)>) -> IndexedGraph {
85 let dt: HashMap<String, serde_json::Value> = descriptor_types
86 .into_iter()
87 .map(|(dtype, ng_id, template)| {
88 (
89 dtype.to_string(),
90 serde_json::json!({
91 "nodegroup_id": ng_id,
92 "string_template": template,
93 }),
94 )
95 })
96 .collect();
97
98 let graph_json = serde_json::json!({
99 "graphid": "test-graph",
100 "name": {"en": "Test Graph"},
101 "root": {
102 "nodeid": "root-id",
103 "name": "Root",
104 "datatype": "semantic",
105 "graph_id": "test-graph"
106 },
107 "nodes": [
108 {
109 "nodeid": "root-id",
110 "name": "Root",
111 "datatype": "semantic",
112 "graph_id": "test-graph"
113 },
114 {
115 "nodeid": "name-node-id",
116 "name": "Name",
117 "alias": "name",
118 "datatype": "string",
119 "nodegroup_id": "name-ng",
120 "graph_id": "test-graph"
121 },
122 {
123 "nodeid": "slug-node-id",
124 "name": "Slug",
125 "alias": "slug",
126 "datatype": "string",
127 "nodegroup_id": "slug-ng",
128 "graph_id": "test-graph"
129 }
130 ],
131 "nodegroups": [
132 { "nodegroupid": "name-ng", "cardinality": "1" },
133 { "nodegroupid": "slug-ng", "cardinality": "1" }
134 ],
135 "edges": [
136 { "domainnode_id": "root-id", "rangenode_id": "name-node-id" },
137 { "domainnode_id": "root-id", "rangenode_id": "slug-node-id" }
138 ],
139 "functions_x_graphs": [
140 {
141 "config": { "descriptor_types": dt },
142 "function_id": DESCRIPTOR_FUNCTION_ID,
143 "graph_id": "test-graph",
144 "id": "fxg-1"
145 }
146 ]
147 });
148
149 let graph: StaticGraph = serde_json::from_value(graph_json).expect("test graph JSON");
150 IndexedGraph::new(graph)
151 }
152
153 fn make_tile(nodegroup_id: &str, node_id: &str, value: &str) -> StaticTile {
154 let mut tile = StaticTile::new_empty(nodegroup_id.to_string());
155 tile.resourceinstance_id = "res-1".to_string();
156 tile.tileid = Some("tile-1".to_string());
157 tile.data.insert(
158 node_id.to_string(),
159 serde_json::json!({"en": {"value": value, "direction": "ltr"}}),
160 );
161 tile
162 }
163
164 #[test]
165 fn test_build_descriptors_slug() {
166 let indexed = build_test_graph(vec![
167 ("name", "name-ng", "<Name>"),
168 ("slug", "slug-ng", "<Slug>"),
169 ]);
170
171 let tiles = vec![
172 make_tile("name-ng", "name-node-id", "My Resource"),
173 make_tile("slug-ng", "slug-node-id", "My Resource"),
174 ];
175
176 let descriptors = indexed.build_descriptors(&tiles);
177
178 assert_eq!(descriptors.name.as_deref(), Some("My Resource"));
179 assert_eq!(descriptors.slug.as_deref(), Some("my-resource"));
180 assert_eq!(descriptors.description, None);
181 assert_eq!(descriptors.map_popup, None);
182 }
183
184 #[test]
185 fn test_build_descriptors_slug_absent_is_none() {
186 let indexed = build_test_graph(vec![("name", "name-ng", "<Name>")]);
187
188 let tiles = vec![make_tile("name-ng", "name-node-id", "My Resource")];
189
190 let descriptors = indexed.build_descriptors(&tiles);
191
192 assert_eq!(descriptors.name.as_deref(), Some("My Resource"));
193 assert!(descriptors.slug.is_none());
194 }
195
196 #[test]
197 fn test_descriptors_is_empty() {
198 let d = StaticResourceDescriptors::default();
199 assert!(d.is_empty());
200
201 let d = StaticResourceDescriptors {
202 slug: Some("test".to_string()),
203 ..Default::default()
204 };
205 assert!(!d.is_empty());
206 }
207
208 #[test]
209 fn test_descriptors_serde_roundtrip_with_slug() {
210 let d = StaticResourceDescriptors {
211 name: Some("Test".to_string()),
212 slug: Some("test-slug".to_string()),
213 description: None,
214 map_popup: None,
215 };
216 let json = serde_json::to_string(&d).unwrap();
217 let parsed: StaticResourceDescriptors = serde_json::from_str(&json).unwrap();
218 assert_eq!(parsed.slug.as_deref(), Some("test-slug"));
219 assert_eq!(parsed.name.as_deref(), Some("Test"));
220 }
221
222 #[test]
223 fn test_descriptors_deserialize_without_slug_is_backwards_compatible() {
224 let json = r#"{"name": "Test"}"#;
225 let parsed: StaticResourceDescriptors = serde_json::from_str(json).unwrap();
226 assert_eq!(parsed.name.as_deref(), Some("Test"));
227 assert!(parsed.slug.is_none());
228 }
229
230 #[test]
231 fn test_build_descriptors_multi_nodegroup() {
232 let graph_json = serde_json::json!({
234 "graphid": "test-graph",
235 "name": {"en": "Test Graph"},
236 "root": {
237 "nodeid": "root-id",
238 "name": "Root",
239 "datatype": "semantic",
240 "graph_id": "test-graph"
241 },
242 "nodes": [
243 {
244 "nodeid": "root-id",
245 "name": "Root",
246 "datatype": "semantic",
247 "graph_id": "test-graph"
248 },
249 {
250 "nodeid": "name-node-id",
251 "name": "Name",
252 "alias": "name",
253 "datatype": "string",
254 "nodegroup_id": "name-ng",
255 "graph_id": "test-graph"
256 },
257 {
258 "nodeid": "desc-node-id",
259 "name": "Description",
260 "alias": "description",
261 "datatype": "string",
262 "nodegroup_id": "desc-ng",
263 "graph_id": "test-graph"
264 }
265 ],
266 "nodegroups": [
267 { "nodegroupid": "name-ng", "cardinality": "1" },
268 { "nodegroupid": "desc-ng", "cardinality": "1" }
269 ],
270 "edges": [
271 { "domainnode_id": "root-id", "rangenode_id": "name-node-id" },
272 { "domainnode_id": "root-id", "rangenode_id": "desc-node-id" }
273 ],
274 "functions_x_graphs": [
275 {
276 "config": {
277 "descriptor_types": {
278 "name": {
279 "nodegroup_ids": ["name-ng", "desc-ng"],
280 "string_template": "<Name> - <Description>"
281 }
282 }
283 },
284 "function_id": "00b2d15a-fda0-4578-b79a-784e4138664b",
285 "graph_id": "test-graph",
286 "id": "fxg-1"
287 }
288 ]
289 });
290
291 let graph: StaticGraph = serde_json::from_value(graph_json).expect("test graph JSON");
292 let indexed = IndexedGraph::new(graph);
293
294 let tiles = vec![
295 make_tile("name-ng", "name-node-id", "Heritage Item 42"),
296 make_tile("desc-ng", "desc-node-id", "A fine building"),
297 ];
298
299 let descriptors = indexed.build_descriptors(&tiles);
300 assert_eq!(
301 descriptors.name.as_deref(),
302 Some("Heritage Item 42 - A fine building")
303 );
304 }
305}