use super::compat::{EntityRef, Link};
use serde::{Deserialize, Serialize};
pub const MAX_TRAVERSAL_DEPTH: usize = 20;
pub const MAX_TRAVERSAL_RESULTS: usize = 10_000;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Direction {
#[default]
#[serde(alias = "Out")]
Out,
#[serde(alias = "In")]
In,
#[serde(alias = "Both")]
Both,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PathNode {
pub entity_id: EntityRef,
pub depth: usize,
pub via_link: Option<Link>,
pub path_weight: f64,
}
impl PathNode {
pub fn start(entity_id: EntityRef) -> Self {
Self {
entity_id,
depth: 0,
via_link: None,
path_weight: 0.0,
}
}
pub fn from_outgoing_link(link: Link, depth: usize, path_weight: f64) -> Self {
Self {
entity_id: link.target.clone(),
depth,
via_link: Some(link),
path_weight,
}
}
pub fn from_incoming_link(link: Link, depth: usize, path_weight: f64) -> Self {
Self {
entity_id: link.source.clone(),
depth,
via_link: Some(link),
path_weight,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TraversalOptions {
pub max_depth: usize,
pub limit: Option<usize>,
pub direction: Direction,
pub link_types: Option<Vec<String>>,
pub min_weight: Option<f64>,
}
impl Default for TraversalOptions {
fn default() -> Self {
Self {
max_depth: 3,
limit: Some(MAX_TRAVERSAL_RESULTS),
direction: Direction::Out,
link_types: None,
min_weight: None,
}
}
}
impl TraversalOptions {
pub fn new(max_depth: usize) -> Self {
Self {
max_depth: max_depth.min(MAX_TRAVERSAL_DEPTH),
limit: Some(MAX_TRAVERSAL_RESULTS),
..Default::default()
}
}
#[must_use]
pub fn with_max_depth(mut self, depth: usize) -> Self {
self.max_depth = depth.min(MAX_TRAVERSAL_DEPTH);
self
}
#[must_use]
pub fn with_direction(mut self, direction: Direction) -> Self {
self.direction = direction;
self
}
#[must_use]
pub fn with_link_types(mut self, types: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.link_types = Some(types.into_iter().map(Into::into).collect());
self
}
#[must_use]
pub fn with_limit(mut self, limit: usize) -> Self {
self.limit = Some(limit.min(MAX_TRAVERSAL_RESULTS));
self
}
#[must_use]
pub fn with_min_weight(mut self, weight: f64) -> Self {
self.min_weight = Some(weight);
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_traversal_options_default() {
let opts = TraversalOptions::default();
assert_eq!(opts.max_depth, 3);
assert_eq!(opts.direction, Direction::Out);
assert!(opts.link_types.is_none());
assert_eq!(opts.limit, Some(MAX_TRAVERSAL_RESULTS));
}
#[test]
fn test_traversal_options_builder() {
let opts = TraversalOptions::new(5)
.with_direction(Direction::Both)
.with_link_types(["contains", "references"])
.with_limit(100)
.with_min_weight(0.5);
assert_eq!(opts.max_depth, 5);
assert_eq!(opts.direction, Direction::Both);
assert_eq!(
opts.link_types,
Some(vec!["contains".to_string(), "references".to_string()])
);
assert_eq!(opts.limit, Some(100));
assert_eq!(opts.min_weight, Some(0.5));
}
#[test]
fn test_traversal_options_clamping() {
let opts = TraversalOptions::new(100);
assert_eq!(opts.max_depth, MAX_TRAVERSAL_DEPTH);
let opts = TraversalOptions::new(3).with_limit(100_000);
assert_eq!(opts.limit, Some(MAX_TRAVERSAL_RESULTS));
}
#[test]
fn test_path_node_start() {
let entity = EntityRef::External("test".to_string());
let node = PathNode::start(entity.clone());
assert_eq!(node.entity_id, entity);
assert_eq!(node.depth, 0);
assert!(node.via_link.is_none());
assert_eq!(node.path_weight, 0.0);
}
#[test]
fn test_direction_default() {
let dir = Direction::default();
assert_eq!(dir, Direction::Out);
}
#[test]
fn test_safety_constants() {
assert_eq!(MAX_TRAVERSAL_DEPTH, 20);
assert_eq!(MAX_TRAVERSAL_RESULTS, 10_000);
}
}