Skip to main content

platform_core/
graph.rs

1//
2// Copyright 2018-2026 Accenture Technology
3//
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at
7//
8//     http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15//
16
17//! The minimalist in-memory property graph — Rust port of
18//! `org.platformlambda.core.graph.MiniGraph` (+ `SimpleNode`,
19//! `SimpleConnection`, `SimpleRelationship`): nodes with types and untyped
20//! properties, unidirectional connections carrying optional typed relations,
21//! alias/id/type/property lookups, neighbor traversal and BFS path
22//! discovery. Designed for a few hundred nodes held entirely in memory —
23//! deliberately never a database (the repo vision's non-goal); the Active
24//! Knowledge Graph layer (layer 3) builds on it.
25//!
26//! Ownership model (the one Rust translation): Java hands out shared mutable
27//! `SimpleNode` objects; here nodes, connections and relations are
28//! `Arc`-shared with interior mutability, so engine code can hold node
29//! handles and mutate properties exactly like the Java callers do. Property
30//! values are `rmpv::Value` — the same currency as envelope bodies and the
31//! layer-2 state machine.
32
33use std::collections::{HashMap, HashSet, VecDeque};
34use std::sync::atomic::{AtomicUsize, Ordering};
35use std::sync::{Arc, Mutex};
36
37use rmpv::Value;
38
39use crate::function::AppError;
40
41/// Node aliases that would collide with the knowledge-graph state-machine
42/// namespaces (Java `RESERVED_NAMES`).
43const RESERVED_NAMES: &[&str] = &[
44    "input",
45    "output",
46    "model",
47    "response",
48    "result",
49    "parameter",
50    "none",
51    "next",
52    "api",
53    "error",
54];
55
56fn invalid(message: impl Into<String>) -> AppError {
57    AppError::new(400, message)
58}
59
60/// Java `GraphProperties.validateName`: aliases, types and property keys use
61/// `0-9 A-Z a-z _ -` only.
62fn validate_name(name: &str) -> Result<(), AppError> {
63    let valid = !name.is_empty()
64        && name
65            .bytes()
66            .all(|b| b.is_ascii_alphanumeric() || b == b'_' || b == b'-');
67    if valid {
68        Ok(())
69    } else {
70        Err(invalid(format!(
71            "Invalid syntax ({name}). Please use 0-9, A-Z, a-z, underscore and hyphen characters."
72        )))
73    }
74}
75
76/// Shared property-bag behavior (Java `GraphProperties`).
77#[derive(Debug, Default)]
78pub struct GraphProperties {
79    properties: Mutex<HashMap<String, Value>>,
80}
81
82impl GraphProperties {
83    /// A copy of all properties.
84    pub fn get_properties(&self) -> HashMap<String, Value> {
85        self.properties.lock().expect("graph properties").clone()
86    }
87
88    pub fn get_property(&self, key: &str) -> Option<Value> {
89        self.properties
90            .lock()
91            .expect("graph properties")
92            .get(key)
93            .cloned()
94    }
95
96    /// Add a key-value (key syntax validated; null values rejected).
97    pub fn add_property(&self, key: &str, value: Value) -> Result<(), AppError> {
98        if key.is_empty() {
99            return Err(invalid("key cannot be empty"));
100        }
101        if matches!(value, Value::Nil) {
102            return Err(invalid("value cannot be null"));
103        }
104        validate_name(key)?;
105        self.properties
106            .lock()
107            .expect("graph properties")
108            .insert(key.to_string(), value);
109        Ok(())
110    }
111
112    pub fn remove_property(&self, key: &str) -> Result<(), AppError> {
113        if key.is_empty() {
114            return Err(invalid("key cannot be empty"));
115        }
116        self.properties
117            .lock()
118            .expect("graph properties")
119            .remove(key);
120        Ok(())
121    }
122}
123
124/// A graph node (Java `SimpleNode`): unique alias, one or more types, and a
125/// property bag. Shared by `Arc`; equality is by node id.
126#[derive(Debug)]
127pub struct SimpleNode {
128    id: String,
129    alias: String,
130    types: Mutex<HashSet<String>>,
131    properties: GraphProperties,
132}
133
134impl PartialEq for SimpleNode {
135    fn eq(&self, other: &Self) -> bool {
136        self.id == other.id
137    }
138}
139impl Eq for SimpleNode {}
140
141impl SimpleNode {
142    fn new(id: &str, alias: &str, node_type: &str) -> Result<Self, AppError> {
143        validate_name(alias)?;
144        validate_name(node_type)?;
145        let node = SimpleNode {
146            id: id.to_string(),
147            alias: alias.to_string(),
148            types: Mutex::new(HashSet::new()),
149            properties: GraphProperties::default(),
150        };
151        node.types
152            .lock()
153            .expect("node types")
154            .insert(node_type.to_string());
155        Ok(node)
156    }
157
158    pub fn get_id(&self) -> &str {
159        &self.id
160    }
161
162    pub fn get_alias(&self) -> &str {
163        &self.alias
164    }
165
166    pub fn get_types(&self) -> HashSet<String> {
167        self.types.lock().expect("node types").clone()
168    }
169
170    pub fn add_type(&self, node_type: &str) -> Result<(), AppError> {
171        if node_type.is_empty() {
172            return Err(invalid("type cannot be empty"));
173        }
174        validate_name(node_type)?;
175        self.types
176            .lock()
177            .expect("node types")
178            .insert(node_type.to_string());
179        Ok(())
180    }
181
182    /// Replace all types with one initial type (the Java Playground's
183    /// `update node` mutates the live type set; this is the explicit analog).
184    pub fn reset_types(&self, initial: &str) -> Result<(), AppError> {
185        validate_name(initial)?;
186        let mut types = self.types.lock().expect("node types");
187        types.clear();
188        types.insert(initial.to_string());
189        Ok(())
190    }
191
192    /// Remove all properties (the Java Playground's `update node` analog).
193    pub fn clear_properties(&self) {
194        self.properties
195            .properties
196            .lock()
197            .expect("graph properties")
198            .clear();
199    }
200
201    /// A node keeps at least one type (Java parity).
202    pub fn remove_type(&self, node_type: &str) -> Result<(), AppError> {
203        if node_type.is_empty() {
204            return Err(invalid("type cannot be empty"));
205        }
206        let mut types = self.types.lock().expect("node types");
207        if types.len() == 1 {
208            return Err(invalid(
209                "Cannot remove type because a node must have at least one type",
210            ));
211        }
212        types.remove(node_type);
213        Ok(())
214    }
215
216    pub fn get_properties(&self) -> HashMap<String, Value> {
217        self.properties.get_properties()
218    }
219
220    pub fn get_property(&self, key: &str) -> Option<Value> {
221        self.properties.get_property(key)
222    }
223
224    pub fn add_property(&self, key: &str, value: Value) -> Result<(), AppError> {
225        self.properties.add_property(key, value)
226    }
227
228    pub fn remove_property(&self, key: &str) -> Result<(), AppError> {
229        self.properties.remove_property(key)
230    }
231}
232
233/// A typed relation on a connection (Java `SimpleRelationship`).
234#[derive(Debug)]
235pub struct SimpleRelationship {
236    relation_type: String,
237    source_alias: String,
238    target_alias: String,
239    properties: GraphProperties,
240}
241
242impl PartialEq for SimpleRelationship {
243    fn eq(&self, other: &Self) -> bool {
244        self.relation_type == other.relation_type
245            && self.source_alias == other.source_alias
246            && self.target_alias == other.target_alias
247    }
248}
249
250impl SimpleRelationship {
251    pub fn get_type(&self) -> &str {
252        &self.relation_type
253    }
254
255    pub fn get_source_alias(&self) -> &str {
256        &self.source_alias
257    }
258
259    pub fn get_target_alias(&self) -> &str {
260        &self.target_alias
261    }
262
263    pub fn get_properties(&self) -> HashMap<String, Value> {
264        self.properties.get_properties()
265    }
266
267    pub fn get_property(&self, key: &str) -> Option<Value> {
268        self.properties.get_property(key)
269    }
270
271    pub fn add_property(&self, key: &str, value: Value) -> Result<(), AppError> {
272        self.properties.add_property(key, value)
273    }
274}
275
276/// A unidirectional connection between two nodes, optionally carrying typed
277/// relations (Java `SimpleConnection`). Equality is by connection id.
278#[derive(Debug)]
279pub struct SimpleConnection {
280    id: String,
281    source: Arc<SimpleNode>,
282    target: Arc<SimpleNode>,
283    relationships: Mutex<HashMap<String, Arc<SimpleRelationship>>>,
284}
285
286impl PartialEq for SimpleConnection {
287    fn eq(&self, other: &Self) -> bool {
288        self.id == other.id
289    }
290}
291
292impl SimpleConnection {
293    pub fn get_id(&self) -> &str {
294        &self.id
295    }
296
297    pub fn get_source(&self) -> &Arc<SimpleNode> {
298        &self.source
299    }
300
301    pub fn get_target(&self) -> &Arc<SimpleNode> {
302        &self.target
303    }
304
305    /// Add (or replace) a relation of the given type.
306    pub fn add_relation(&self, relation_type: &str) -> Arc<SimpleRelationship> {
307        let relation = Arc::new(SimpleRelationship {
308            relation_type: relation_type.to_string(),
309            source_alias: self.source.get_alias().to_string(),
310            target_alias: self.target.get_alias().to_string(),
311            properties: GraphProperties::default(),
312        });
313        self.relationships
314            .lock()
315            .expect("relations")
316            .insert(relation_type.to_lowercase(), relation.clone());
317        relation
318    }
319
320    /// Case-insensitive relation lookup.
321    pub fn get_relation(&self, relation_type: &str) -> Option<Arc<SimpleRelationship>> {
322        self.relationships
323            .lock()
324            .expect("relations")
325            .get(&relation_type.to_lowercase())
326            .cloned()
327    }
328
329    pub fn get_relations(&self) -> Vec<Arc<SimpleRelationship>> {
330        self.relationships
331            .lock()
332            .expect("relations")
333            .values()
334            .cloned()
335            .collect()
336    }
337}
338
339struct GraphState {
340    nodes_by_alias: HashMap<String, Arc<SimpleNode>>,
341    nodes_by_id: HashMap<String, Arc<SimpleNode>>,
342    connections: HashMap<String, Arc<SimpleConnection>>,
343    successors: HashMap<String, HashSet<String>>,
344    predecessors: HashMap<String, HashSet<String>>,
345}
346
347/// The mini-graph (Java `MiniGraph`): an in-memory property graph designed to
348/// handle a small number of nodes very efficiently (default cap 750).
349pub struct MiniGraph {
350    graph_id: String,
351    max_nodes: usize,
352    node_count: AtomicUsize,
353    state: Mutex<GraphState>,
354}
355
356impl Default for MiniGraph {
357    fn default() -> Self {
358        Self::new()
359    }
360}
361
362impl MiniGraph {
363    /// Create a mini-graph with the default maximum of 750 nodes.
364    pub fn new() -> Self {
365        Self::with_max_nodes(750)
366    }
367
368    /// Create a mini-graph with an explicit node cap — be conservative:
369    /// everything lives in memory and performance decreases as nodes grow.
370    pub fn with_max_nodes(max_nodes: usize) -> Self {
371        MiniGraph {
372            graph_id: uuid::Uuid::new_v4().simple().to_string(),
373            max_nodes,
374            node_count: AtomicUsize::new(0),
375            state: Mutex::new(GraphState {
376                nodes_by_alias: HashMap::new(),
377                nodes_by_id: HashMap::new(),
378                connections: HashMap::new(),
379                successors: HashMap::new(),
380                predecessors: HashMap::new(),
381            }),
382        }
383    }
384
385    /// The unique id of this mini-graph.
386    pub fn get_id(&self) -> &str {
387        &self.graph_id
388    }
389
390    pub fn get_node_count(&self) -> usize {
391        self.node_count.load(Ordering::SeqCst)
392    }
393
394    pub fn is_empty(&self) -> bool {
395        self.get_node_count() == 0
396    }
397
398    pub fn create_root_node(&self) -> Result<Arc<SimpleNode>, AppError> {
399        self.create_node("root", "root")
400    }
401
402    pub fn create_end_node(&self) -> Result<Arc<SimpleNode>, AppError> {
403        self.create_node("end", "end")
404    }
405
406    pub fn get_root_node(&self) -> Option<Arc<SimpleNode>> {
407        self.find_node_by_alias("root").ok().flatten()
408    }
409
410    pub fn get_end_node(&self) -> Option<Arc<SimpleNode>> {
411        self.find_node_by_alias("end").ok().flatten()
412    }
413
414    /// Create a node with a unique alias and an initial type.
415    pub fn create_node(&self, alias: &str, node_type: &str) -> Result<Arc<SimpleNode>, AppError> {
416        if alias.is_empty() {
417            return Err(invalid("alias must not be empty"));
418        }
419        if node_type.is_empty() {
420            return Err(invalid("type must not be empty"));
421        }
422        let alias_lower = alias.to_lowercase();
423        if RESERVED_NAMES.contains(&alias_lower.as_str()) {
424            return Err(invalid(format!("alias '{alias_lower}' is a reserved name")));
425        }
426        let mut state = self.state.lock().expect("graph state");
427        if state.nodes_by_alias.contains_key(&alias_lower) {
428            return Err(invalid(format!("alias '{alias_lower}' already exists")));
429        }
430        if self.node_count.load(Ordering::SeqCst) > self.max_nodes {
431            return Err(invalid(format!(
432                "max number of nodes is {}",
433                self.max_nodes
434            )));
435        }
436        let count = self.node_count.fetch_add(1, Ordering::SeqCst) + 1;
437        let id = uuid::Uuid::new_v4().simple().to_string();
438        let node = Arc::new(SimpleNode::new(&id, alias, node_type)?);
439        state.nodes_by_alias.insert(alias_lower, node.clone());
440        state.nodes_by_id.insert(id, node.clone());
441        log::debug!("Created {node_type} as {alias}, total={count}");
442        Ok(node)
443    }
444
445    /// Remove a node and every connection touching it.
446    pub fn remove_node(&self, alias: &str) -> Result<(), AppError> {
447        if alias.is_empty() {
448            return Err(invalid("alias must not be empty"));
449        }
450        let Some(node) = self.find_node_by_alias(alias)? else {
451            return Ok(());
452        };
453        for neighbor in self.get_forward_links(alias)? {
454            self.remove_connection(alias, neighbor.get_alias())?;
455        }
456        for neighbor in self.get_backward_links(alias)? {
457            self.remove_connection(neighbor.get_alias(), alias)?;
458        }
459        let mut state = self.state.lock().expect("graph state");
460        state.nodes_by_id.remove(node.get_id());
461        // divergence note: Java removes with the raw alias here (a latent
462        // case-sensitivity slip); the lowercased key matches every other
463        // lookup and is the intended behavior
464        state.nodes_by_alias.remove(&alias.to_lowercase());
465        let count = self.node_count.fetch_sub(1, Ordering::SeqCst) - 1;
466        log::debug!("Removed {alias}, total={count}");
467        Ok(())
468    }
469
470    pub fn get_nodes(&self) -> Vec<Arc<SimpleNode>> {
471        self.state
472            .lock()
473            .expect("graph state")
474            .nodes_by_id
475            .values()
476            .cloned()
477            .collect()
478    }
479
480    pub fn get_connections(&self) -> Vec<Arc<SimpleConnection>> {
481        self.state
482            .lock()
483            .expect("graph state")
484            .connections
485            .values()
486            .cloned()
487            .collect()
488    }
489
490    /// Clear the graph, de-referencing all nodes and connections.
491    pub fn reset(&self) {
492        let mut state = self.state.lock().expect("graph state");
493        state.connections.clear();
494        state.successors.clear();
495        state.predecessors.clear();
496        state.nodes_by_alias.clear();
497        state.nodes_by_id.clear();
498        self.node_count.store(0, Ordering::SeqCst);
499    }
500
501    fn resolve_pair(
502        &self,
503        source_alias: &str,
504        target_alias: &str,
505    ) -> Result<(Arc<SimpleNode>, Arc<SimpleNode>), AppError> {
506        if source_alias.is_empty() {
507            return Err(invalid("source alias cannot be null"));
508        }
509        if target_alias.is_empty() {
510            return Err(invalid("target alias cannot be null"));
511        }
512        if source_alias.eq_ignore_ascii_case(target_alias) {
513            return Err(invalid("source and target aliases cannot be the same"));
514        }
515        let source = self
516            .find_node_by_alias(source_alias)?
517            .ok_or_else(|| invalid("source node does not exist"))?;
518        let target = self
519            .find_node_by_alias(target_alias)?
520            .ok_or_else(|| invalid("target node does not exist"))?;
521        Ok((source, target))
522    }
523
524    /// Connect two nodes (idempotent: an existing connection is returned).
525    pub fn connect(
526        &self,
527        source_alias: &str,
528        target_alias: &str,
529    ) -> Result<Arc<SimpleConnection>, AppError> {
530        let (source, target) = self.resolve_pair(source_alias, target_alias)?;
531        let key = pair_key(source.get_id(), target.get_id());
532        let mut state = self.state.lock().expect("graph state");
533        if let Some(existing) = state.connections.get(&key) {
534            return Ok(existing.clone());
535        }
536        let connection = Arc::new(SimpleConnection {
537            id: uuid::Uuid::new_v4().simple().to_string(),
538            source: source.clone(),
539            target: target.clone(),
540            relationships: Mutex::new(HashMap::new()),
541        });
542        state.connections.insert(key, connection.clone());
543        state
544            .successors
545            .entry(source.get_id().to_string())
546            .or_default()
547            .insert(target.get_id().to_string());
548        state
549            .predecessors
550            .entry(target.get_id().to_string())
551            .or_default()
552            .insert(source.get_id().to_string());
553        log::debug!("Created connection {source_alias} to {target_alias}");
554        Ok(connection)
555    }
556
557    /// Remove the connection between two nodes (with its relations).
558    pub fn remove_connection(
559        &self,
560        source_alias: &str,
561        target_alias: &str,
562    ) -> Result<(), AppError> {
563        let (source, target) = self.resolve_pair(source_alias, target_alias)?;
564        let key = pair_key(source.get_id(), target.get_id());
565        let mut state = self.state.lock().expect("graph state");
566        if state.connections.remove(&key).is_some() {
567            if let Some(targets) = state.successors.get_mut(source.get_id()) {
568                targets.remove(target.get_id());
569                if targets.is_empty() {
570                    state.successors.remove(source.get_id());
571                }
572            }
573            if let Some(sources) = state.predecessors.get_mut(target.get_id()) {
574                sources.remove(source.get_id());
575                if sources.is_empty() {
576                    state.predecessors.remove(target.get_id());
577                }
578            }
579            log::debug!(
580                "Removed connection {} to {}",
581                source.get_alias(),
582                target.get_alias()
583            );
584        }
585        Ok(())
586    }
587
588    /// Case-insensitive alias lookup. Errors on an empty alias (Java: null).
589    pub fn find_node_by_alias(&self, alias: &str) -> Result<Option<Arc<SimpleNode>>, AppError> {
590        if alias.is_empty() {
591            return Err(invalid("alias cannot be null"));
592        }
593        Ok(self
594            .state
595            .lock()
596            .expect("graph state")
597            .nodes_by_alias
598            .get(&alias.to_lowercase())
599            .cloned())
600    }
601
602    pub fn find_node_by_id(&self, id: &str) -> Result<Option<Arc<SimpleNode>>, AppError> {
603        if id.is_empty() {
604            return Err(invalid("id cannot be null"));
605        }
606        Ok(self
607            .state
608            .lock()
609            .expect("graph state")
610            .nodes_by_id
611            .get(id)
612            .cloned())
613    }
614
615    /// All nodes carrying the given type (case-insensitive).
616    pub fn find_nodes_by_type(&self, node_type: &str) -> Result<Vec<Arc<SimpleNode>>, AppError> {
617        if node_type.is_empty() {
618            return Err(invalid("type cannot be empty"));
619        }
620        Ok(self
621            .get_nodes()
622            .into_iter()
623            .filter(|node| {
624                node.get_types()
625                    .iter()
626                    .any(|t| t.eq_ignore_ascii_case(node_type))
627            })
628            .collect())
629    }
630
631    /// All relations of the given type across every connection.
632    pub fn find_relation_by_type(
633        &self,
634        relation_type: &str,
635    ) -> Result<Vec<Arc<SimpleRelationship>>, AppError> {
636        if relation_type.is_empty() {
637            return Err(invalid("type cannot be empty"));
638        }
639        Ok(self
640            .get_connections()
641            .into_iter()
642            .filter_map(|conn| conn.get_relation(relation_type))
643            .collect())
644    }
645
646    /// All nodes with a property whose key matches case-insensitively and
647    /// whose value equals the given one.
648    pub fn find_nodes_by_property(
649        &self,
650        key: &str,
651        value: &Value,
652    ) -> Result<Vec<Arc<SimpleNode>>, AppError> {
653        if key.is_empty() {
654            return Err(invalid("key cannot be empty"));
655        }
656        Ok(self
657            .get_nodes()
658            .into_iter()
659            .filter(|node| {
660                node.get_properties()
661                    .iter()
662                    .any(|(k, v)| k.eq_ignore_ascii_case(key) && v == value)
663            })
664            .collect())
665    }
666
667    pub fn find_connection(
668        &self,
669        source_alias: &str,
670        target_alias: &str,
671    ) -> Result<Option<Arc<SimpleConnection>>, AppError> {
672        let (source, target) = self.resolve_pair(source_alias, target_alias)?;
673        Ok(self
674            .state
675            .lock()
676            .expect("graph state")
677            .connections
678            .get(&pair_key(source.get_id(), target.get_id()))
679            .cloned())
680    }
681
682    /// Both directions between two nodes: 0 to 2 connections, forward first.
683    pub fn find_bi_directional_connection(
684        &self,
685        source_alias: &str,
686        target_alias: &str,
687    ) -> Result<Vec<Arc<SimpleConnection>>, AppError> {
688        let mut both = Vec::new();
689        if let Some(forward) = self.find_connection(source_alias, target_alias)? {
690            both.push(forward);
691        }
692        if let Some(backward) = self.find_connection(target_alias, source_alias)? {
693            both.push(backward);
694        }
695        Ok(both)
696    }
697
698    fn require_node(&self, alias: &str) -> Result<Arc<SimpleNode>, AppError> {
699        self.find_node_by_alias(alias)?
700            .ok_or_else(|| invalid("node does not exist"))
701    }
702
703    fn linked_nodes(&self, node_id: &str, forward: bool) -> Vec<Arc<SimpleNode>> {
704        let state = self.state.lock().expect("graph state");
705        let map = if forward {
706            &state.successors
707        } else {
708            &state.predecessors
709        };
710        map.get(node_id)
711            .map(|ids| {
712                ids.iter()
713                    .filter_map(|id| state.nodes_by_id.get(id).cloned())
714                    .collect()
715            })
716            .unwrap_or_default()
717    }
718
719    /// Nodes connected in either direction.
720    pub fn get_neighbors(&self, alias: &str) -> Result<Vec<Arc<SimpleNode>>, AppError> {
721        let node = self.require_node(alias)?;
722        let mut seen = HashSet::new();
723        let mut result = Vec::new();
724        for neighbor in self
725            .linked_nodes(node.get_id(), true)
726            .into_iter()
727            .chain(self.linked_nodes(node.get_id(), false))
728        {
729            if seen.insert(neighbor.get_id().to_string()) {
730                result.push(neighbor);
731            }
732        }
733        Ok(result)
734    }
735
736    pub fn get_forward_links(&self, alias: &str) -> Result<Vec<Arc<SimpleNode>>, AppError> {
737        let node = self.require_node(alias)?;
738        Ok(self.linked_nodes(node.get_id(), true))
739    }
740
741    pub fn get_backward_links(&self, alias: &str) -> Result<Vec<Arc<SimpleNode>>, AppError> {
742        let node = self.require_node(alias)?;
743        Ok(self.linked_nodes(node.get_id(), false))
744    }
745
746    /// BFS level discovery from a node (direction-agnostic): one list of
747    /// aliases per distance level; unreachable nodes are skipped.
748    pub fn find_paths(&self, alias: &str) -> Result<Vec<Vec<String>>, AppError> {
749        let start = self.require_node(alias)?;
750        let mut distances: HashMap<String, i64> = HashMap::new();
751        for node in self.get_nodes() {
752            distances.insert(node.get_id().to_string(), -1);
753        }
754        distances.insert(start.get_id().to_string(), 0);
755        let mut queue: VecDeque<String> = VecDeque::new();
756        queue.push_back(start.get_id().to_string());
757        while let Some(current) = queue.pop_front() {
758            let current_distance = distances[&current];
759            let mut adjacent: HashSet<String> = HashSet::new();
760            for n in self
761                .linked_nodes(&current, true)
762                .into_iter()
763                .chain(self.linked_nodes(&current, false))
764            {
765                adjacent.insert(n.get_id().to_string());
766            }
767            for next in adjacent {
768                if distances.get(&next) == Some(&-1) {
769                    distances.insert(next.clone(), current_distance + 1);
770                    queue.push_back(next);
771                }
772            }
773        }
774        let mut levels: HashMap<i64, Vec<String>> = HashMap::new();
775        for (id, level) in &distances {
776            if *level != -1 {
777                if let Some(node) = self.find_node_by_id(id)? {
778                    levels
779                        .entry(*level)
780                        .or_default()
781                        .push(node.get_alias().to_string());
782                }
783            }
784        }
785        let mut sorted_levels: Vec<i64> = levels.keys().copied().collect();
786        sorted_levels.sort_unstable();
787        Ok(sorted_levels
788            .into_iter()
789            .map(|level| levels.remove(&level).unwrap_or_default())
790            .collect())
791    }
792
793    /// Export the graph as a map value: nodes sorted by alias, connections by
794    /// source:target, relations by type — deterministic for round-tripping.
795    pub fn export_graph(&self) -> Value {
796        let mut node_entries: Vec<(String, Value)> = self
797            .get_nodes()
798            .into_iter()
799            .map(|node| {
800                let mut types: Vec<String> = node.get_types().into_iter().collect();
801                types.sort_unstable();
802                let mut properties: Vec<(String, Value)> =
803                    node.get_properties().into_iter().collect();
804                properties.sort_by(|a, b| a.0.cmp(&b.0));
805                let entry = Value::Map(vec![
806                    (Value::from("alias"), Value::from(node.get_alias())),
807                    (
808                        Value::from("types"),
809                        Value::Array(types.into_iter().map(Value::from).collect()),
810                    ),
811                    (
812                        Value::from("properties"),
813                        Value::Map(
814                            properties
815                                .into_iter()
816                                .map(|(k, v)| (Value::from(k.as_str()), v))
817                                .collect(),
818                        ),
819                    ),
820                ]);
821                (node.get_alias().to_string(), entry)
822            })
823            .collect();
824        node_entries.sort_by(|a, b| a.0.cmp(&b.0));
825        let mut connection_entries: Vec<(String, Value)> = self
826            .get_connections()
827            .into_iter()
828            .map(|conn| {
829                let mut relations: Vec<(String, Value)> = conn
830                    .get_relations()
831                    .into_iter()
832                    .map(|relation| {
833                        let mut properties: Vec<(String, Value)> =
834                            relation.get_properties().into_iter().collect();
835                        properties.sort_by(|a, b| a.0.cmp(&b.0));
836                        let entry = Value::Map(vec![
837                            (Value::from("type"), Value::from(relation.get_type())),
838                            (
839                                Value::from("properties"),
840                                Value::Map(
841                                    properties
842                                        .into_iter()
843                                        .map(|(k, v)| (Value::from(k.as_str()), v))
844                                        .collect(),
845                                ),
846                            ),
847                        ]);
848                        (relation.get_type().to_string(), entry)
849                    })
850                    .collect();
851                relations.sort_by(|a, b| a.0.cmp(&b.0));
852                let key = format!(
853                    "{}:{}",
854                    conn.get_source().get_alias(),
855                    conn.get_target().get_alias()
856                );
857                let entry = Value::Map(vec![
858                    (
859                        Value::from("source"),
860                        Value::from(conn.get_source().get_alias()),
861                    ),
862                    (
863                        Value::from("target"),
864                        Value::from(conn.get_target().get_alias()),
865                    ),
866                    (
867                        Value::from("relations"),
868                        Value::Array(relations.into_iter().map(|(_, v)| v).collect()),
869                    ),
870                ]);
871                (key, entry)
872            })
873            .collect();
874        connection_entries.sort_by(|a, b| a.0.cmp(&b.0));
875        Value::Map(vec![
876            (
877                Value::from("nodes"),
878                Value::Array(node_entries.into_iter().map(|(_, v)| v).collect()),
879            ),
880            (
881                Value::from("connections"),
882                Value::Array(connection_entries.into_iter().map(|(_, v)| v).collect()),
883            ),
884        ])
885    }
886
887    /// Import a graph map (the export shape). The graph resets first; an
888    /// invalid payload leaves it empty (Java parity).
889    pub fn import_graph(&self, map: &Value) -> Result<(), AppError> {
890        self.reset();
891        let outcome = self.import_nodes_and_connections(map);
892        if outcome.is_err() {
893            self.reset();
894        }
895        outcome
896    }
897
898    fn import_nodes_and_connections(&self, map: &Value) -> Result<(), AppError> {
899        let Value::Map(entries) = map else {
900            return Ok(());
901        };
902        let get = |key: &str| -> Option<&Value> {
903            entries
904                .iter()
905                .find(|(k, _)| k.as_str() == Some(key))
906                .map(|(_, v)| v)
907        };
908        let Some(Value::Array(nodes)) = get("nodes") else {
909            return Ok(());
910        };
911        for (i, entry) in nodes.iter().enumerate() {
912            self.import_node(entry, i)?;
913        }
914        if let Some(Value::Array(connections)) = get("connections") {
915            for (i, entry) in connections.iter().enumerate() {
916                self.import_connection(entry, i)?;
917            }
918        }
919        Ok(())
920    }
921
922    fn import_node(&self, entry: &Value, index: usize) -> Result<(), AppError> {
923        let get = |key: &str| -> Option<&Value> {
924            match entry {
925                Value::Map(map) => map
926                    .iter()
927                    .find(|(k, _)| k.as_str() == Some(key))
928                    .map(|(_, v)| v),
929                _ => None,
930            }
931        };
932        let alias = get("alias")
933            .and_then(|v| v.as_str())
934            .ok_or_else(|| invalid(format!("missing alias in node entry-{}", index + 1)))?;
935        let Some(Value::Array(types)) = get("types") else {
936            return Err(invalid(format!(
937                "invalid types in node entry-{}",
938                index + 1
939            )));
940        };
941        if types.is_empty() {
942            return Err(invalid(format!(
943                "invalid types in node entry-{}",
944                index + 1
945            )));
946        }
947        let first = types[0].as_str().unwrap_or_default().to_string();
948        let node = self.create_node(alias, &first)?;
949        for t in types.iter().skip(1) {
950            node.add_type(t.as_str().unwrap_or_default())?;
951        }
952        if let Some(Value::Map(properties)) = get("properties") {
953            for (k, v) in properties {
954                node.add_property(k.as_str().unwrap_or_default(), v.clone())?;
955            }
956        }
957        Ok(())
958    }
959
960    fn import_connection(&self, entry: &Value, index: usize) -> Result<(), AppError> {
961        let get = |key: &str| -> Option<&Value> {
962            match entry {
963                Value::Map(map) => map
964                    .iter()
965                    .find(|(k, _)| k.as_str() == Some(key))
966                    .map(|(_, v)| v),
967                _ => None,
968            }
969        };
970        let (Some(source), Some(target)) = (
971            get("source").and_then(|v| v.as_str()),
972            get("target").and_then(|v| v.as_str()),
973        ) else {
974            return Err(invalid(format!(
975                "invalid source/target alias in connection entry-{}",
976                index + 1
977            )));
978        };
979        let connection = self.connect(source, target)?;
980        if let Some(Value::Array(relations)) = get("relations") {
981            for relation_entry in relations {
982                if let Value::Map(relation_map) = relation_entry {
983                    let rget = |key: &str| -> Option<&Value> {
984                        relation_map
985                            .iter()
986                            .find(|(k, _)| k.as_str() == Some(key))
987                            .map(|(_, v)| v)
988                    };
989                    if let Some(relation_type) = rget("type").and_then(|v| v.as_str()) {
990                        let relation = connection.add_relation(relation_type);
991                        if let Some(Value::Map(properties)) = rget("properties") {
992                            for (k, v) in properties {
993                                relation.add_property(k.as_str().unwrap_or_default(), v.clone())?;
994                            }
995                        }
996                    }
997                }
998            }
999        }
1000        Ok(())
1001    }
1002
1003    /// Structural equality with another graph: same nodes (aliases, types,
1004    /// deep-equal properties) and same connections/relations.
1005    pub fn same_as(&self, that: &MiniGraph) -> bool {
1006        self.same_nodes(that) && self.same_connections(that)
1007    }
1008
1009    fn same_nodes(&self, that: &MiniGraph) -> bool {
1010        let nodes1 = self.get_nodes();
1011        if nodes1.len() != that.get_node_count() {
1012            return false;
1013        }
1014        for n1 in nodes1 {
1015            let Ok(Some(n2)) = that.find_node_by_alias(n1.get_alias()) else {
1016                return false;
1017            };
1018            if n1.get_types() != n2.get_types() {
1019                return false;
1020            }
1021            if !same_properties(&n1.get_properties(), &n2.get_properties()) {
1022                return false;
1023            }
1024        }
1025        true
1026    }
1027
1028    fn same_connections(&self, that: &MiniGraph) -> bool {
1029        let conn1 = self.get_connections();
1030        if conn1.len() != that.get_connections().len() {
1031            return false;
1032        }
1033        for c1 in conn1 {
1034            let Ok(Some(c2)) =
1035                that.find_connection(c1.get_source().get_alias(), c1.get_target().get_alias())
1036            else {
1037                return false;
1038            };
1039            let relations1 = c1.get_relations();
1040            if relations1.len() != c2.get_relations().len() {
1041                return false;
1042            }
1043            for r1 in relations1 {
1044                let Some(r2) = c2.get_relation(r1.get_type()) else {
1045                    return false;
1046                };
1047                if !same_properties(&r1.get_properties(), &r2.get_properties()) {
1048                    return false;
1049                }
1050            }
1051        }
1052        true
1053    }
1054
1055    /// Export as a JSON string (deterministic ordering).
1056    pub fn to_json(&self) -> String {
1057        serde_json::to_value(self.export_graph())
1058            .map(|v| v.to_string())
1059            .unwrap_or_default()
1060    }
1061}
1062
1063fn pair_key(source_id: &str, target_id: &str) -> String {
1064    format!("{source_id}:{target_id}")
1065}
1066
1067/// Deep property comparison independent of map ordering (Java flattens both
1068/// sides with `getFlatMap` and compares keys and values).
1069fn same_properties(a: &HashMap<String, Value>, b: &HashMap<String, Value>) -> bool {
1070    let mut flat_a = Vec::new();
1071    let mut flat_b = Vec::new();
1072    for (k, v) in a {
1073        flatten(k, v, &mut flat_a);
1074    }
1075    for (k, v) in b {
1076        flatten(k, v, &mut flat_b);
1077    }
1078    flat_a.sort_by(|x, y| x.0.cmp(&y.0));
1079    flat_b.sort_by(|x, y| x.0.cmp(&y.0));
1080    flat_a == flat_b
1081}
1082
1083fn flatten(prefix: &str, value: &Value, target: &mut Vec<(String, Value)>) {
1084    match value {
1085        Value::Map(entries) => {
1086            for (k, v) in entries {
1087                flatten(
1088                    &format!("{prefix}.{}", k.as_str().unwrap_or_default()),
1089                    v,
1090                    target,
1091                );
1092            }
1093        }
1094        Value::Array(items) => {
1095            for (i, v) in items.iter().enumerate() {
1096                flatten(&format!("{prefix}[{i}]"), v, target);
1097            }
1098        }
1099        leaf => target.push((prefix.to_string(), leaf.clone())),
1100    }
1101}