1use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5use std::sync::Arc;
6
7use super::cards::{StaticCard, StaticCardsXNodesXWidgets, StaticFunctionsXGraphs};
8use super::descriptors::{DescriptorConfig, StaticResourceDescriptors, DESCRIPTOR_FUNCTION_ID};
9use super::nodes::{StaticEdge, StaticNode, StaticNodegroup};
10use super::tile::StaticTile;
11use super::translatable::StaticTranslatableString;
12
13#[derive(Debug, Deserialize)]
15pub struct GraphWrapper {
16 pub graph: Vec<StaticGraph>,
17}
18
19#[derive(Clone, Debug, Serialize, Deserialize)]
21pub struct StaticGraph {
22 pub graphid: String,
23 pub name: StaticTranslatableString,
24 #[serde(default)]
25 pub author: Option<String>,
26 #[serde(default)]
27 pub subtitle: Option<StaticTranslatableString>,
28 #[serde(default)]
29 pub description: Option<StaticTranslatableString>,
30 pub nodes: Vec<StaticNode>,
31 #[serde(default)]
32 pub nodegroups: Vec<StaticNodegroup>,
33 #[serde(default)]
34 pub edges: Vec<StaticEdge>,
35 pub root: StaticNode,
36 #[serde(default)]
37 pub version: Option<String>,
38 #[serde(default)]
39 pub iconclass: Option<String>,
40 #[serde(default)]
41 pub color: Option<String>,
42 #[serde(default)]
43 pub isresource: Option<bool>,
44 #[serde(default)]
45 pub slug: Option<String>,
46 #[serde(default)]
47 pub is_editable: Option<bool>,
48 #[serde(default, with = "super::serde_helpers::optional_string_or_vec")]
52 pub ontology_id: Option<Vec<String>>,
53 #[serde(default)]
54 pub template_id: Option<String>,
55 #[serde(default)]
56 pub deploymentdate: Option<String>,
57 #[serde(default)]
58 pub deploymentfile: Option<String>,
59 #[serde(default)]
60 pub jsonldcontext: Option<String>,
61 #[serde(default)]
62 pub config: serde_json::Value,
63 #[serde(default)]
64 pub relatable_resource_model_ids: Vec<String>,
65 #[serde(default)]
66 pub publication: Option<serde_json::Value>,
67 #[serde(default)]
68 pub resource_2_resource_constraints: Option<Vec<serde_json::Value>>,
69
70 #[serde(default, skip_serializing_if = "Option::is_none")]
73 pub source_identifier_id: Option<String>,
74 #[serde(default, skip_serializing_if = "Option::is_none")]
76 pub is_active: Option<bool>,
77 #[serde(default, skip_serializing_if = "Option::is_none")]
79 pub has_unpublished_changes: Option<bool>,
80 #[serde(default, skip_serializing_if = "Option::is_none")]
82 pub is_copy_immutable: Option<bool>,
83 #[serde(default, skip_serializing_if = "Option::is_none")]
85 pub resource_instance_lifecycle: Option<serde_json::Value>,
86 #[serde(default, skip_serializing_if = "Option::is_none")]
88 pub spatial_views: Option<serde_json::Value>,
89 #[serde(default, skip_serializing_if = "Option::is_none")]
91 pub group_permissions: Option<serde_json::Value>,
92 #[serde(default, skip_serializing_if = "Option::is_none")]
94 pub user_permissions: Option<serde_json::Value>,
95
96 #[serde(default, skip_serializing_if = "Option::is_none")]
98 pub cards: Option<Vec<StaticCard>>,
99 #[serde(default, skip_serializing_if = "Option::is_none")]
100 pub cards_x_nodes_x_widgets: Option<Vec<StaticCardsXNodesXWidgets>>,
101 #[serde(default, skip_serializing_if = "Option::is_none")]
102 pub functions_x_graphs: Option<Vec<StaticFunctionsXGraphs>>,
103
104 #[serde(skip)]
106 node_by_id: Option<HashMap<String, usize>>,
107 #[serde(skip)]
108 node_by_alias: Option<HashMap<String, usize>>,
109 #[serde(skip)]
110 edges_map: Option<HashMap<String, Vec<String>>>,
111 #[serde(skip)]
112 nodes_by_nodegroup: Option<HashMap<String, Vec<usize>>>,
113 #[serde(skip)]
114 nodegroup_by_id: Option<HashMap<String, usize>>,
115 #[serde(skip)]
117 nodes_by_alias_arc: Option<HashMap<String, Arc<StaticNode>>>,
118 #[serde(skip)]
120 card_index: Option<super::card_index::CardIndex>,
121}
122
123impl StaticGraph {
124 pub fn from_json_string(json_str: &str) -> Result<StaticGraph, String> {
127 if let Ok(mut graph) = serde_json::from_str::<StaticGraph>(json_str) {
129 graph.build_indices();
130 return Ok(graph);
131 }
132
133 let wrapper: GraphWrapper =
135 serde_json::from_str(json_str).map_err(|e| format!("Failed to parse JSON: {}", e))?;
136 let mut graph = wrapper
137 .graph
138 .into_iter()
139 .next()
140 .ok_or_else(|| "No graphs found in JSON".to_string())?;
141 graph.build_indices();
142 Ok(graph)
143 }
144
145 pub fn build_indices(&mut self) {
147 let mut node_by_id = HashMap::new();
148 let mut node_by_alias = HashMap::new();
149 let mut nodes_by_nodegroup: HashMap<String, Vec<usize>> = HashMap::new();
150
151 for (idx, node) in self.nodes.iter().enumerate() {
152 node_by_id.insert(node.nodeid.clone(), idx);
153 if let Some(ref alias) = node.alias {
154 if !alias.is_empty() {
155 node_by_alias.insert(alias.clone(), idx);
156 }
157 }
158 if let Some(ref ng_id) = node.nodegroup_id {
159 if !ng_id.is_empty() {
160 nodes_by_nodegroup
161 .entry(ng_id.clone())
162 .or_default()
163 .push(idx);
164 }
165 }
166 }
167
168 let mut edges_map: HashMap<String, Vec<String>> = HashMap::new();
170 for edge in &self.edges {
171 edges_map
172 .entry(edge.domainnode_id.clone())
173 .or_default()
174 .push(edge.rangenode_id.clone());
175 }
176
177 let mut nodegroup_by_id = HashMap::new();
179 for (idx, ng) in self.nodegroups.iter().enumerate() {
180 nodegroup_by_id.insert(ng.nodegroupid.clone(), idx);
181 }
182
183 let nodes_by_alias_arc: HashMap<String, Arc<StaticNode>> = self
185 .nodes
186 .iter()
187 .filter_map(|n| {
188 n.alias
189 .as_ref()
190 .filter(|a| !a.is_empty())
191 .map(|a| (a.clone(), Arc::new(n.clone())))
192 })
193 .collect();
194
195 self.node_by_id = Some(node_by_id);
196 self.node_by_alias = Some(node_by_alias);
197 self.edges_map = Some(edges_map);
198 self.nodes_by_nodegroup = Some(nodes_by_nodegroup);
199 self.nodegroup_by_id = Some(nodegroup_by_id);
200 self.nodes_by_alias_arc = Some(nodes_by_alias_arc);
201
202 if self.cards.is_some() {
204 self.card_index = Some(super::card_index::build_card_index(
205 self.cards_slice(),
206 self.cards_x_nodes_x_widgets_slice(),
207 &self.nodegroups,
208 &self.nodes,
209 crate::graph_mutator::get_widget_name_by_id,
210 ));
211 }
212 }
213
214 pub fn invalidate_indices(&mut self) {
219 self.node_by_id = None;
220 self.node_by_alias = None;
221 self.edges_map = None;
222 self.nodes_by_nodegroup = None;
223 self.nodegroup_by_id = None;
224 self.nodes_by_alias_arc = None;
225 self.card_index = None;
226 }
227
228 pub fn get_root(&self) -> &StaticNode {
230 &self.root
231 }
232
233 pub fn get_node_by_index(&self, idx: usize) -> Option<&StaticNode> {
235 self.nodes.get(idx)
236 }
237
238 pub fn get_node_by_id(&self, id: &str) -> Option<&StaticNode> {
240 self.node_by_id
241 .as_ref()?
242 .get(id)
243 .and_then(|&idx| self.nodes.get(idx))
244 }
245
246 pub fn get_node_by_alias(&self, alias: &str) -> Option<&StaticNode> {
248 self.node_by_alias
249 .as_ref()?
250 .get(alias)
251 .and_then(|&idx| self.nodes.get(idx))
252 }
253
254 pub fn display_name(&self) -> String {
256 self.name.to_string_default()
257 }
258
259 pub fn display_subtitle(&self) -> String {
261 self.subtitle
262 .as_ref()
263 .map(|s| s.to_string_default())
264 .unwrap_or_default()
265 }
266
267 pub fn display_author(&self) -> String {
269 self.author.clone().unwrap_or_default()
270 }
271
272 pub fn nodes_slice(&self) -> &[StaticNode] {
274 &self.nodes
275 }
276
277 pub fn nodegroups_slice(&self) -> &[StaticNodegroup] {
279 &self.nodegroups
280 }
281
282 pub fn edges_slice(&self) -> &[StaticEdge] {
284 &self.edges
285 }
286
287 pub fn root_node(&self) -> &StaticNode {
289 &self.root
290 }
291
292 pub fn graph_id(&self) -> &str {
294 &self.graphid
295 }
296
297 pub fn get_model_class_name(&self) -> Option<String> {
302 let name = self.name.to_string_default();
303 if name.is_empty() {
304 None
305 } else {
306 Some(name)
307 }
308 }
309
310 pub fn edges_map(&self) -> Option<&HashMap<String, Vec<String>>> {
313 self.edges_map.as_ref()
314 }
315
316 pub fn get_child_ids(&self, node_id: &str) -> Option<&Vec<String>> {
318 self.edges_map.as_ref()?.get(node_id)
319 }
320
321 pub fn nodes_by_nodegroup(&self) -> Option<&HashMap<String, Vec<usize>>> {
324 self.nodes_by_nodegroup.as_ref()
325 }
326
327 pub fn get_nodes_in_nodegroup(&self, nodegroup_id: &str) -> Vec<&StaticNode> {
329 self.nodes_by_nodegroup
330 .as_ref()
331 .and_then(|map| map.get(nodegroup_id))
332 .map(|indices| {
333 indices
334 .iter()
335 .filter_map(|&idx| self.nodes.get(idx))
336 .collect()
337 })
338 .unwrap_or_default()
339 }
340
341 pub fn get_nodegroup_by_id(&self, nodegroup_id: &str) -> Option<&StaticNodegroup> {
343 self.nodegroup_by_id
344 .as_ref()?
345 .get(nodegroup_id)
346 .and_then(|&idx| self.nodegroups.get(idx))
347 }
348
349 pub fn nodes_by_alias_arc(&self) -> Option<&HashMap<String, Arc<StaticNode>>> {
352 self.nodes_by_alias_arc.as_ref()
353 }
354
355 pub fn get_node_arc_by_alias(&self, alias: &str) -> Option<Arc<StaticNode>> {
357 self.nodes_by_alias_arc.as_ref()?.get(alias).cloned()
358 }
359
360 pub fn deep_clone(&self) -> Self {
366 let mut cloned = self.clone();
367 cloned.build_indices();
368 cloned
369 }
370
371 pub fn push_node(&mut self, node: StaticNode) {
375 self.nodes.push(node);
376 self.invalidate_indices();
377 }
378
379 pub fn push_edge(&mut self, edge: StaticEdge) {
381 self.edges.push(edge);
382 self.edges_map = None;
383 }
384
385 pub fn push_nodegroup(&mut self, nodegroup: StaticNodegroup) {
387 self.nodegroups.push(nodegroup);
388 self.nodegroup_by_id = None;
389 }
390
391 pub fn push_card(&mut self, card: StaticCard) {
393 if self.cards.is_none() {
394 self.cards = Some(Vec::new());
395 }
396 if let Some(ref mut cards) = self.cards {
397 cards.push(card);
398 }
399 }
400
401 pub fn push_card_x_node_x_widget(&mut self, cxnxw: StaticCardsXNodesXWidgets) {
403 if self.cards_x_nodes_x_widgets.is_none() {
404 self.cards_x_nodes_x_widgets = Some(Vec::new());
405 }
406 if let Some(ref mut cxnxw_list) = self.cards_x_nodes_x_widgets {
407 cxnxw_list.push(cxnxw);
408 }
409 }
410
411 pub fn cards_slice(&self) -> &[StaticCard] {
413 self.cards.as_deref().unwrap_or(&[])
414 }
415
416 pub fn card_index(&self) -> Option<&super::card_index::CardIndex> {
418 self.card_index.as_ref()
419 }
420
421 pub fn cards_x_nodes_x_widgets_slice(&self) -> &[StaticCardsXNodesXWidgets] {
423 self.cards_x_nodes_x_widgets.as_deref().unwrap_or(&[])
424 }
425
426 pub fn find_card_by_nodegroup(&self, nodegroup_id: &str) -> Option<&StaticCard> {
428 self.cards
429 .as_ref()?
430 .iter()
431 .find(|c| c.nodegroup_id == nodegroup_id)
432 }
433
434 pub fn find_node_by_alias(&self, alias: &str) -> Option<&StaticNode> {
436 if let Some(node) = self.get_node_by_alias(alias) {
438 return Some(node);
439 }
440 self.nodes
442 .iter()
443 .find(|n| n.alias.as_deref() == Some(alias))
444 }
445
446 pub fn get_schema(&self) -> serde_json::Value {
454 let node_map: HashMap<&str, &StaticNode> =
456 self.nodes.iter().map(|n| (n.nodeid.as_str(), n)).collect();
457
458 let mut children_map: HashMap<&str, Vec<&str>> = HashMap::new();
460 for edge in &self.edges {
461 children_map
462 .entry(edge.domainnode_id.as_str())
463 .or_default()
464 .push(&edge.rangenode_id);
465 }
466
467 fn build_node_schema(
469 nodeid: &str,
470 node_map: &HashMap<&str, &StaticNode>,
471 children_map: &HashMap<&str, Vec<&str>>,
472 ) -> serde_json::Value {
473 let node = match node_map.get(nodeid) {
474 Some(n) => n,
475 None => return serde_json::json!({}),
476 };
477
478 let mut schema = serde_json::json!({
479 "datatype": node.datatype,
480 "nodeid": node.nodeid,
481 });
482
483 if node.isrequired {
484 schema["required"] = serde_json::json!(true);
485 }
486
487 if let Some(child_ids) = children_map.get(nodeid) {
489 let mut children = serde_json::Map::new();
490 for child_id in child_ids {
491 if let Some(child_node) = node_map.get(child_id) {
492 let key = child_node.alias.as_deref().unwrap_or(&child_node.nodeid);
493 children.insert(
494 key.to_string(),
495 build_node_schema(child_id, node_map, children_map),
496 );
497 }
498 }
499 if !children.is_empty() {
500 schema["children"] = serde_json::Value::Object(children);
501 }
502 }
503
504 schema
505 }
506
507 let root_id = &self.root.nodeid;
509 let mut root_schema = serde_json::Map::new();
510
511 if let Some(child_ids) = children_map.get(root_id.as_str()) {
512 for child_id in child_ids {
513 if let Some(child_node) = node_map.get(child_id) {
514 let key = child_node.alias.as_deref().unwrap_or(&child_node.nodeid);
515 root_schema.insert(
516 key.to_string(),
517 build_node_schema(child_id, &node_map, &children_map),
518 );
519 }
520 }
521 }
522
523 serde_json::Value::Object(root_schema)
524 }
525
526 pub fn set_descriptor_template(
534 &mut self,
535 descriptor_type: &str,
536 string_template: &str,
537 ) -> Result<(), String> {
538 use crate::graph::descriptors::DESCRIPTOR_FUNCTION_ID;
539 use crate::graph::StaticFunctionsXGraphs;
540 use std::collections::HashSet;
541
542 let fxg = self.functions_x_graphs.get_or_insert_with(Vec::new);
543
544 let idx = fxg
550 .iter()
551 .position(|f| {
552 f.function_id != DESCRIPTOR_FUNCTION_ID
553 && (f.config.is_null() || f.config == serde_json::json!({}))
554 })
555 .or_else(|| {
556 fxg.iter().position(|f| {
557 f.function_id != DESCRIPTOR_FUNCTION_ID
558 && f.config
559 .as_object()
560 .is_some_and(|c| c.contains_key("descriptor_types"))
561 })
562 })
563 .or_else(|| {
564 fxg.iter()
565 .position(|f| f.function_id == DESCRIPTOR_FUNCTION_ID)
566 });
567
568 let is_default_function = idx
569 .map(|i| fxg[i].function_id == DESCRIPTOR_FUNCTION_ID)
570 .unwrap_or(true); let entry = if is_default_function {
577 let placeholders = IndexedGraph::extract_placeholders(string_template);
578 if placeholders.is_empty() {
579 return Err(format!(
580 "Template '{}' has no <Node Name> placeholders",
581 string_template
582 ));
583 }
584
585 let mut nodegroup_ids = HashSet::new();
586 for placeholder in &placeholders {
587 let node_name = placeholder.trim_start_matches('<').trim_end_matches('>');
588 let node = self
589 .nodes
590 .iter()
591 .find(|n| n.name == node_name)
592 .ok_or_else(|| {
593 format!("Node '{}' from template not found in graph", node_name)
594 })?;
595 let ng_id = node
596 .nodegroup_id
597 .as_ref()
598 .ok_or_else(|| format!("Node '{}' has no nodegroup_id", node_name))?;
599 nodegroup_ids.insert(ng_id.clone());
600 }
601
602 if nodegroup_ids.len() != 1 && descriptor_type != "slug" {
603 return Err(format!(
604 "Template placeholders span {} nodegroups ({:?}), expected exactly 1",
605 nodegroup_ids.len(),
606 nodegroup_ids
607 ));
608 }
609
610 let nodegroup_id = nodegroup_ids.into_iter().next().unwrap();
611 serde_json::json!({
612 "nodegroup_id": nodegroup_id,
613 "string_template": string_template,
614 })
615 } else {
616 serde_json::json!({
619 "nodegroup_id": "",
620 "string_template": string_template,
621 })
622 };
623
624 let existing = idx.map(|i| &mut fxg[i]);
625
626 if let Some(func) = existing {
627 let needs_seed = !func.config.is_object()
628 || !func
629 .config
630 .as_object()
631 .is_some_and(|c| c.contains_key("descriptor_types"));
632 if needs_seed && !is_default_function {
633 func.config = serde_json::json!({
635 "descriptor_types": {
636 "name": {"nodegroup_id": "", "string_template": ""},
637 "description": {"nodegroup_id": "", "string_template": ""},
638 "map_popup": {"nodegroup_id": "", "string_template": ""},
639 }
640 });
641 } else if !func.config.is_object() {
642 func.config = serde_json::json!({"descriptor_types": {}});
643 }
644 let config = func.config.as_object_mut().unwrap();
645 let dt = config
646 .entry("descriptor_types")
647 .or_insert_with(|| serde_json::json!({}));
648 if let Some(dt_obj) = dt.as_object_mut() {
649 dt_obj.insert(descriptor_type.to_string(), entry);
650 }
651 } else {
652 let mut dt_map = serde_json::Map::new();
653 dt_map.insert(descriptor_type.to_string(), entry);
654 fxg.push(StaticFunctionsXGraphs {
655 config: serde_json::json!({ "descriptor_types": dt_map }),
656 function_id: DESCRIPTOR_FUNCTION_ID.to_string(),
657 graph_id: self.graphid.clone(),
658 id: crate::graph_mutator::generate_uuid_v5(
659 ("function", Some(&self.graphid)),
660 DESCRIPTOR_FUNCTION_ID,
661 ),
662 });
663 }
664
665 Ok(())
666 }
667}
668
669pub struct IndexedGraph {
671 pub graph: StaticGraph,
672 pub nodes_by_id: HashMap<String, StaticNode>,
674 pub children_by_node: HashMap<String, Vec<String>>,
676 pub nodes_by_alias: HashMap<String, StaticNode>,
678 pub nodegroups_by_id: HashMap<String, StaticNodegroup>,
680}
681
682impl IndexedGraph {
683 pub fn new(graph: StaticGraph) -> Self {
685 let mut nodes_by_id = HashMap::new();
686 let mut nodes_by_alias = HashMap::new();
687 let mut children_by_node: HashMap<String, Vec<String>> = HashMap::new();
688 let mut nodegroups_by_id = HashMap::new();
689
690 for node in &graph.nodes {
692 nodes_by_id.insert(node.nodeid.clone(), node.clone());
693 if let Some(ref alias) = node.alias {
694 if !alias.is_empty() {
695 nodes_by_alias.insert(alias.clone(), node.clone());
696 }
697 }
698 }
699
700 for edge in &graph.edges {
702 children_by_node
703 .entry(edge.domainnode_id.clone())
704 .or_default()
705 .push(edge.rangenode_id.clone());
706 }
707
708 for ng in &graph.nodegroups {
710 nodegroups_by_id.insert(ng.nodegroupid.clone(), ng.clone());
711 }
712
713 IndexedGraph {
714 graph,
715 nodes_by_id,
716 nodes_by_alias,
717 children_by_node,
718 nodegroups_by_id,
719 }
720 }
721
722 pub fn get_root(&self) -> &StaticNode {
724 self.graph.get_root()
725 }
726
727 pub fn get_node(&self, node_id: &str) -> Option<&StaticNode> {
729 self.nodes_by_id.get(node_id)
730 }
731
732 pub fn get_node_by_alias(&self, alias: &str) -> Option<&StaticNode> {
734 self.nodes_by_alias.get(alias)
735 }
736
737 pub fn get_children(&self, node_id: &str) -> Vec<&StaticNode> {
739 self.children_by_node
740 .get(node_id)
741 .map(|ids| {
742 ids.iter()
743 .filter_map(|id| self.nodes_by_id.get(id))
744 .collect()
745 })
746 .unwrap_or_default()
747 }
748
749 pub fn get_child_ids(&self, node_id: &str) -> Vec<&String> {
751 self.children_by_node
752 .get(node_id)
753 .map(|v| v.iter().collect())
754 .unwrap_or_default()
755 }
756
757 pub fn has_children(&self, node_id: &str) -> bool {
759 self.children_by_node
760 .get(node_id)
761 .map(|v| !v.is_empty())
762 .unwrap_or(false)
763 }
764
765 pub fn get_nodegroup(&self, node: &StaticNode) -> Option<&StaticNodegroup> {
767 node.nodegroup_id
768 .as_ref()
769 .and_then(|id| self.nodegroups_by_id.get(id))
770 }
771
772 pub fn build_descriptors(&self, tiles: &[StaticTile]) -> StaticResourceDescriptors {
783 self.build_descriptors_with_context(tiles, &mut Vec::new(), None, None)
784 }
785
786 pub fn build_descriptors_with_diagnostics(
788 &self,
789 tiles: &[StaticTile],
790 warnings: &mut Vec<String>,
791 cache: Option<&super::resources::ResourceCache>,
792 ) -> StaticResourceDescriptors {
793 self.build_descriptors_with_context(tiles, warnings, cache, None)
794 }
795
796 pub fn build_descriptors_with_context(
798 &self,
799 tiles: &[StaticTile],
800 warnings: &mut Vec<String>,
801 cache: Option<&super::resources::ResourceCache>,
802 extension_registry: Option<&crate::extension_type_registry::ExtensionTypeRegistry>,
803 ) -> StaticResourceDescriptors {
804 let config = match self.get_descriptor_config_with_diagnostics(warnings) {
806 Some(c) => c,
807 None => {
808 return StaticResourceDescriptors::default();
810 }
811 };
812
813 let mut descriptors = StaticResourceDescriptors::default();
814
815 for (descriptor_type, type_config) in &config.descriptor_types {
817 let mut template = type_config.string_template.clone();
818
819 let placeholders = Self::extract_placeholders(&template);
821 if placeholders.is_empty() {
822 warnings.push(format!(
823 "Descriptor '{}': No placeholders found in template '{}'",
824 descriptor_type, template
825 ));
826 continue;
827 }
828
829 for placeholder in &placeholders {
831 let node_name = placeholder.trim_start_matches('<').trim_end_matches('>');
832
833 if let Some(node) = self.find_node_by_name(node_name) {
834 let node_ng = match node.nodegroup_id.as_ref() {
835 Some(ng) => ng,
836 None => {
837 warnings.push(format!(
838 "Descriptor '{}': Node '{}' has no nodegroup_id",
839 descriptor_type, node_name
840 ));
841 continue;
842 }
843 };
844 let node_tiles: Vec<&StaticTile> = tiles
845 .iter()
846 .filter(|t| &t.nodegroup_id == node_ng)
847 .collect();
848 if let Some(value) = Self::extract_display_value_from_tiles(
849 &node_tiles,
850 &node.nodeid,
851 &node.datatype,
852 cache,
853 extension_registry,
854 ) {
855 template = template.replace(placeholder, &value);
856 } else {
857 let available_keys: Vec<_> =
858 node_tiles.iter().flat_map(|t| t.data.keys()).collect();
859 warnings.push(format!(
860 "Descriptor '{}': No value found for node '{}' (nodeid '{}') in tiles. Available data keys: {:?}",
861 descriptor_type, node_name, node.nodeid, available_keys
862 ));
863 }
864 } else {
865 warnings.push(format!(
866 "Descriptor '{}': Node '{}' not found in graph",
867 descriptor_type, node_name
868 ));
869 }
870 }
871
872 match descriptor_type.as_str() {
874 "name" => descriptors.name = Some(template),
875 "description" => descriptors.description = Some(template),
876 "map_popup" => descriptors.map_popup = Some(template),
877 "slug" => {
878 descriptors.slug =
879 Some(crate::graph_mutator::slugify(&template).replace('_', "-"))
880 }
881 _ => {} }
883 }
884
885 descriptors
886 }
887
888 fn get_descriptor_config_with_diagnostics(
890 &self,
891 warnings: &mut Vec<String>,
892 ) -> Option<DescriptorConfig> {
893 let functions_x_graphs = match self.graph.functions_x_graphs.as_ref() {
894 Some(fxg) => fxg,
895 None => {
896 warnings.push("Graph has no functions_x_graphs array".to_string());
897 return None;
898 }
899 };
900
901 let descriptor_func = functions_x_graphs
904 .iter()
905 .find(|f| f.function_id == DESCRIPTOR_FUNCTION_ID)
906 .or_else(|| {
907 functions_x_graphs.iter().find(|f| {
908 f.config
909 .as_object()
910 .is_some_and(|c| c.contains_key("descriptor_types"))
911 })
912 });
913
914 if let Some(func) = descriptor_func {
915 match serde_json::from_value::<DescriptorConfig>(func.config.clone()) {
916 Ok(config) => return Some(config),
917 Err(e) => {
918 warnings.push(format!(
919 "Failed to parse descriptor config: {}. Raw config: {}",
920 e,
921 serde_json::to_string(&func.config).unwrap_or_default()
922 ));
923 return None;
924 }
925 }
926 }
927
928 warnings.push(format!(
929 "No descriptor function found in functions_x_graphs (looking for function_id {} or descriptor_types config). Available function_ids: {:?}",
930 DESCRIPTOR_FUNCTION_ID,
931 functions_x_graphs.iter().map(|f| &f.function_id).collect::<Vec<_>>()
932 ));
933 None
934 }
935
936 pub fn extract_placeholders(template: &str) -> Vec<String> {
939 let mut placeholders = Vec::new();
942 let mut in_placeholder = false;
943 let mut current = String::new();
944
945 for ch in template.chars() {
946 if ch == '<' {
947 in_placeholder = true;
948 current.clear();
949 current.push(ch);
950 } else if ch == '>' && in_placeholder {
951 current.push(ch);
952 placeholders.push(current.clone());
953 in_placeholder = false;
954 current.clear();
955 } else if in_placeholder {
956 current.push(ch);
957 }
958 }
959
960 placeholders
961 }
962
963 fn find_node_by_name(&self, name: &str) -> Option<&StaticNode> {
965 self.nodes_by_id.values().find(|node| node.name == name)
966 }
967
968 fn extract_display_value_from_tiles(
974 tiles: &[&StaticTile],
975 node_id: &str,
976 datatype: &str,
977 cache: Option<&super::resources::ResourceCache>,
978 extension_registry: Option<&crate::extension_type_registry::ExtensionTypeRegistry>,
979 ) -> Option<String> {
980 if let Some(cache) = cache {
983 if datatype == "resource-instance" || datatype == "resource-instance-list" {
984 for tile in tiles {
985 if let Some(tile_id) = &tile.tileid {
986 if let Some(node_entries) = cache.get(tile_id) {
987 if let Some(entry) = node_entries.get(node_id) {
988 return Self::display_from_cache_entry(entry);
989 }
990 }
991 }
992 }
993 }
994 }
995
996 for tile in tiles {
997 if let Some(value) = tile.data.get(node_id) {
998 if let Some(registry) = extension_registry {
1000 if let Ok(Some(display)) = registry.render_display(datatype, value, "en") {
1001 return Some(display);
1002 }
1003 }
1004
1005 let result = crate::type_serialization::serialize_display(datatype, value, "en");
1007 if !result.is_error() {
1008 match &result.value {
1009 serde_json::Value::String(s) if !s.is_empty() => return Some(s.clone()),
1010 serde_json::Value::Number(n) => return Some(n.to_string()),
1011 serde_json::Value::Bool(b) => return Some(b.to_string()),
1012 _ => {}
1013 }
1014 }
1015
1016 if let Some(extracted) = Self::extract_string_from_json(value) {
1018 return Some(extracted);
1019 }
1020 }
1021 }
1022 None
1023 }
1024
1025 fn display_from_cache_entry(entry: &super::resources::CacheEntry) -> Option<String> {
1027 match entry {
1028 super::resources::CacheEntry::Single(r) => r.title.clone(),
1029 super::resources::CacheEntry::List(list) => {
1030 let titles: Vec<&str> = list
1031 .entries
1032 .iter()
1033 .filter_map(|e| e.title.as_deref())
1034 .collect();
1035 if titles.is_empty() {
1036 None
1037 } else {
1038 Some(titles.join(", "))
1039 }
1040 }
1041 }
1042 }
1043
1044 fn extract_string_from_json(value: &serde_json::Value) -> Option<String> {
1050 match value {
1051 serde_json::Value::String(s) => Some(s.clone()),
1052 serde_json::Value::Number(n) => Some(n.to_string()),
1053 serde_json::Value::Bool(b) => Some(b.to_string()),
1054 serde_json::Value::Object(map) => {
1055 Self::extract_lang_value(map, "en").or_else(|| {
1057 map.values().find_map(Self::extract_single_lang_value)
1059 })
1060 }
1061 _ => None,
1062 }
1063 }
1064
1065 fn extract_lang_value(
1067 map: &serde_json::Map<String, serde_json::Value>,
1068 lang: &str,
1069 ) -> Option<String> {
1070 map.get(lang).and_then(Self::extract_single_lang_value)
1071 }
1072
1073 fn extract_single_lang_value(value: &serde_json::Value) -> Option<String> {
1077 match value {
1078 serde_json::Value::String(s) => Some(s.clone()),
1079 serde_json::Value::Object(obj) => {
1080 obj.get("value")
1082 .and_then(|v| v.as_str())
1083 .map(|s| s.to_string())
1084 }
1085 _ => None,
1086 }
1087 }
1088}