Skip to main content

grust_core/
lib.rs

1use std::{
2    collections::{BTreeMap, BTreeSet},
3    fmt,
4};
5
6use async_trait::async_trait;
7use serde::{Deserialize, Serialize};
8
9pub type Result<T> = std::result::Result<T, GrustError>;
10pub type Props = BTreeMap<String, Value>;
11
12#[derive(Debug, thiserror::Error)]
13pub enum GrustError {
14    #[error("backend error: {0}")]
15    Backend(String),
16    #[error("schema error: {0}")]
17    Schema(String),
18    #[error("unsupported graph feature: {0}")]
19    Unsupported(String),
20    #[error("serialization error: {0}")]
21    Serialization(String),
22}
23
24macro_rules! string_newtype {
25    ($(#[$meta:meta])* $name:ident) => {
26        $(#[$meta])*
27        #[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)]
28        pub struct $name(String);
29
30        impl $name {
31            pub fn new(value: impl Into<String>) -> Self {
32                Self(value.into())
33            }
34
35            pub fn as_str(&self) -> &str {
36                &self.0
37            }
38
39            pub fn into_string(self) -> String {
40                self.0
41            }
42        }
43
44        impl fmt::Display for $name {
45            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46                f.write_str(&self.0)
47            }
48        }
49
50        impl From<String> for $name {
51            fn from(value: String) -> Self {
52                Self::new(value)
53            }
54        }
55
56        impl From<&str> for $name {
57            fn from(value: &str) -> Self {
58                Self::new(value)
59            }
60        }
61
62        impl From<&String> for $name {
63            fn from(value: &String) -> Self {
64                Self::new(value.clone())
65            }
66        }
67
68        impl From<&$name> for $name {
69            fn from(value: &$name) -> Self {
70                value.clone()
71            }
72        }
73    };
74}
75
76string_newtype!(
77    /// Stable application-level node identifier.
78    NodeId
79);
80string_newtype!(
81    /// Optional application-level edge identifier.
82    EdgeId
83);
84string_newtype!(
85    /// Node or edge label.
86    Label
87);
88
89#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
90#[serde(tag = "type", content = "value", rename_all = "snake_case")]
91pub enum Value {
92    Null,
93    Bool(bool),
94    Int(i64),
95    Float(f64),
96    String(String),
97    /// An RFC 3339 date-time, e.g. `2026-06-12T09:30:00Z`. Construct with
98    /// [`Value::datetime`] to get format validation.
99    DateTime(String),
100    StringArray(Vec<String>),
101    IntArray(Vec<i64>),
102    FloatArray(Vec<f64>),
103    Json(serde_json::Value),
104}
105
106impl Value {
107    /// Creates a `Value::DateTime`, validating that the input is an RFC 3339
108    /// date-time such as `2026-06-12T09:30:00Z` or
109    /// `2026-06-12T09:30:00.123+02:00`.
110    pub fn datetime(value: impl Into<String>) -> Result<Self> {
111        let value = value.into();
112        if is_rfc3339_datetime(&value) {
113            Ok(Self::DateTime(value))
114        } else {
115            Err(GrustError::Schema(format!(
116                "'{value}' is not an RFC 3339 date-time"
117            )))
118        }
119    }
120
121    pub fn as_str(&self) -> Option<&str> {
122        match self {
123            Self::String(value) => Some(value),
124            _ => None,
125        }
126    }
127
128    pub fn as_datetime(&self) -> Option<&str> {
129        match self {
130            Self::DateTime(value) => Some(value),
131            _ => None,
132        }
133    }
134
135    pub fn as_string_array(&self) -> Option<&[String]> {
136        match self {
137            Self::StringArray(values) => Some(values),
138            _ => None,
139        }
140    }
141
142    pub fn as_int_array(&self) -> Option<&[i64]> {
143        match self {
144            Self::IntArray(values) => Some(values),
145            _ => None,
146        }
147    }
148
149    pub fn as_float_array(&self) -> Option<&[f64]> {
150        match self {
151            Self::FloatArray(values) => Some(values),
152            _ => None,
153        }
154    }
155
156    /// Converts to a plain (untagged) JSON value. `DateTime` becomes a JSON
157    /// string, so a `to_json`/`from_json` round trip yields `Value::String`.
158    pub fn to_json(&self) -> serde_json::Value {
159        match self {
160            Self::Null => serde_json::Value::Null,
161            Self::Bool(value) => serde_json::Value::Bool(*value),
162            Self::Int(value) => serde_json::Value::from(*value),
163            Self::Float(value) => serde_json::Value::from(*value),
164            Self::String(value) | Self::DateTime(value) => serde_json::Value::String(value.clone()),
165            Self::StringArray(values) => serde_json::Value::from(values.clone()),
166            Self::IntArray(values) => serde_json::Value::from(values.clone()),
167            Self::FloatArray(values) => serde_json::Value::from(values.clone()),
168            Self::Json(value) => value.clone(),
169        }
170    }
171
172    /// Converts from JSON, accepting both plain values and the tagged
173    /// `{"type": ..., "value": ...}` form that `Value`'s serde representation
174    /// produces.
175    pub fn from_json(value: serde_json::Value) -> Self {
176        if let serde_json::Value::Object(mapping) = &value
177            && mapping.contains_key("type")
178            && mapping.contains_key("value")
179            && let Ok(tagged) = serde_json::from_value(value.clone())
180        {
181            return tagged;
182        }
183        Self::from(value)
184    }
185}
186
187/// Validates the RFC 3339 date-time shape `YYYY-MM-DDTHH:MM:SS[.frac](Z|±HH:MM)`.
188fn is_rfc3339_datetime(value: &str) -> bool {
189    let bytes = value.as_bytes();
190    if bytes.len() < 20 {
191        return false;
192    }
193    let digit = |i: usize| bytes[i].is_ascii_digit();
194    let all_digits = |range: std::ops::Range<usize>| range.clone().all(digit);
195    let pair = |i: usize| (bytes[i] - b'0') * 10 + (bytes[i + 1] - b'0');
196
197    if !(all_digits(0..4)
198        && bytes[4] == b'-'
199        && all_digits(5..7)
200        && bytes[7] == b'-'
201        && all_digits(8..10)
202        && bytes[10] == b'T'
203        && all_digits(11..13)
204        && bytes[13] == b':'
205        && all_digits(14..16)
206        && bytes[16] == b':'
207        && all_digits(17..19))
208    {
209        return false;
210    }
211    let year = bytes[0..4]
212        .iter()
213        .fold(0u16, |acc, digit| acc * 10 + u16::from(digit - b'0'));
214    let month = pair(5);
215    let day = pair(8);
216    let leap_year = (year % 4 == 0 && year % 100 != 0) || year % 400 == 0;
217    let max_day = match month {
218        1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
219        4 | 6 | 9 | 11 => 30,
220        2 if leap_year => 29,
221        2 => 28,
222        _ => return false,
223    };
224    if !(day >= 1 && day <= max_day && pair(11) < 24 && pair(14) < 60 && pair(17) <= 60) {
225        return false;
226    }
227
228    let mut i = 19;
229    if bytes[i] == b'.' {
230        let start = i + 1;
231        i = start;
232        while i < bytes.len() && bytes[i].is_ascii_digit() {
233            i += 1;
234        }
235        if i == start {
236            return false;
237        }
238    }
239    match bytes.get(i) {
240        Some(b'Z') => i + 1 == bytes.len(),
241        Some(b'+' | b'-') => {
242            i + 6 == bytes.len()
243                && all_digits(i + 1..i + 3)
244                && bytes[i + 3] == b':'
245                && all_digits(i + 4..i + 6)
246                && pair(i + 1) < 24
247                && pair(i + 4) < 60
248        }
249        _ => false,
250    }
251}
252
253impl From<String> for Value {
254    fn from(value: String) -> Self {
255        Self::String(value)
256    }
257}
258
259impl From<&str> for Value {
260    fn from(value: &str) -> Self {
261        Self::String(value.to_string())
262    }
263}
264
265impl From<&String> for Value {
266    fn from(value: &String) -> Self {
267        Self::String(value.clone())
268    }
269}
270
271impl From<Vec<String>> for Value {
272    fn from(value: Vec<String>) -> Self {
273        Self::StringArray(value)
274    }
275}
276
277impl From<Vec<i64>> for Value {
278    fn from(value: Vec<i64>) -> Self {
279        Self::IntArray(value)
280    }
281}
282
283impl From<Vec<f64>> for Value {
284    fn from(value: Vec<f64>) -> Self {
285        Self::FloatArray(value)
286    }
287}
288
289impl From<bool> for Value {
290    fn from(value: bool) -> Self {
291        Self::Bool(value)
292    }
293}
294
295impl From<i64> for Value {
296    fn from(value: i64) -> Self {
297        Self::Int(value)
298    }
299}
300
301impl From<i32> for Value {
302    fn from(value: i32) -> Self {
303        Self::Int(i64::from(value))
304    }
305}
306
307impl From<usize> for Value {
308    fn from(value: usize) -> Self {
309        Self::Int(value as i64)
310    }
311}
312
313impl From<f64> for Value {
314    fn from(value: f64) -> Self {
315        Self::Float(value)
316    }
317}
318
319impl From<serde_json::Value> for Value {
320    fn from(value: serde_json::Value) -> Self {
321        match value {
322            serde_json::Value::Null => Self::Null,
323            serde_json::Value::Bool(value) => Self::Bool(value),
324            serde_json::Value::Number(value) => {
325                if let Some(value) = value.as_i64() {
326                    Self::Int(value)
327                } else if let Some(value) = value.as_f64() {
328                    Self::Float(value)
329                } else {
330                    Self::Json(serde_json::Value::Number(value))
331                }
332            }
333            serde_json::Value::String(value) => Self::String(value),
334            serde_json::Value::Array(values) => {
335                let ints = values
336                    .iter()
337                    .filter_map(serde_json::Value::as_i64)
338                    .collect::<Vec<_>>();
339                if !values.is_empty() && ints.len() == values.len() {
340                    return Self::IntArray(ints);
341                }
342                let floats = values
343                    .iter()
344                    .filter_map(serde_json::Value::as_f64)
345                    .collect::<Vec<_>>();
346                if !values.is_empty() && floats.len() == values.len() {
347                    return Self::FloatArray(floats);
348                }
349                let strings = values
350                    .iter()
351                    .filter_map(|value| value.as_str().map(ToString::to_string))
352                    .collect::<Vec<_>>();
353                if strings.len() == values.len() {
354                    Self::StringArray(strings)
355                } else {
356                    Self::Json(serde_json::Value::Array(values))
357                }
358            }
359            serde_json::Value::Object(value) => Self::Json(serde_json::Value::Object(value)),
360        }
361    }
362}
363
364#[cfg(feature = "typed-garde")]
365pub mod typed {
366    use serde::Serialize;
367    #[cfg(feature = "typed-zod-rs")]
368    use serde::de::DeserializeOwned;
369
370    use crate::{
371        Edge, EdgeId, Graph, GraphBuilder, GrustError, Node, NodeId, Props, PutOutcome, Result,
372        Value,
373    };
374
375    pub use garde;
376    #[cfg(feature = "typed-zod-rs")]
377    pub use zod_rs;
378
379    pub trait TypedNode: garde::Validate + Serialize {
380        const LABEL: &'static str;
381
382        fn node_id(&self) -> NodeId;
383
384        fn node_props(&self) -> Result<Props> {
385            props_from_serialize(self)
386        }
387    }
388
389    pub trait TypedEdge: garde::Validate + Serialize {
390        const LABEL: &'static str;
391
392        fn source_node_id(&self) -> NodeId;
393
394        fn target_node_id(&self) -> NodeId;
395
396        fn edge_id(&self) -> Option<EdgeId> {
397            None
398        }
399
400        fn edge_props(&self) -> Result<Props> {
401            props_from_serialize(self)
402        }
403    }
404
405    #[derive(Clone, Debug, Default)]
406    pub struct TypedGraphBuilder {
407        builder: GraphBuilder,
408    }
409
410    impl TypedGraphBuilder {
411        pub fn new() -> Self {
412            Self::default()
413        }
414
415        pub fn from_builder(builder: GraphBuilder) -> Self {
416            Self { builder }
417        }
418
419        pub fn from_graph(graph: Graph) -> Self {
420            let mut builder = GraphBuilder::new();
421            for node in graph.nodes {
422                builder.add_node(node);
423            }
424            for edge in graph.edges {
425                builder.add_edge(edge);
426            }
427            Self { builder }
428        }
429
430        pub fn add_raw_node(&mut self, node: Node) -> NodeId {
431            self.builder.add_node(node)
432        }
433
434        pub fn add_raw_edge(&mut self, edge: Edge) -> PutOutcome {
435            self.builder.add_edge(edge)
436        }
437
438        pub fn add_node<T>(&mut self, node: &T) -> Result<NodeId>
439        where
440            T: TypedNode,
441            T::Context: Default,
442        {
443            node.validate()
444                .map_err(|err| validation_error(T::LABEL, err))?;
445            self.add_validated_node(node)
446        }
447
448        pub fn add_node_with<T>(&mut self, node: &T, ctx: &T::Context) -> Result<NodeId>
449        where
450            T: TypedNode,
451        {
452            node.validate_with(ctx)
453                .map_err(|err| validation_error(T::LABEL, err))?;
454            self.add_validated_node(node)
455        }
456
457        pub fn add_edge<T>(&mut self, edge: &T) -> Result<PutOutcome>
458        where
459            T: TypedEdge,
460            T::Context: Default,
461        {
462            edge.validate()
463                .map_err(|err| validation_error(T::LABEL, err))?;
464            self.add_validated_edge(edge)
465        }
466
467        pub fn add_edge_with<T>(&mut self, edge: &T, ctx: &T::Context) -> Result<PutOutcome>
468        where
469            T: TypedEdge,
470        {
471            edge.validate_with(ctx)
472                .map_err(|err| validation_error(T::LABEL, err))?;
473            self.add_validated_edge(edge)
474        }
475
476        #[cfg(feature = "typed-zod-rs")]
477        pub fn add_node_from_json<T, S>(
478            &mut self,
479            schema: &S,
480            value: &serde_json::Value,
481        ) -> Result<NodeId>
482        where
483            T: TypedNode + DeserializeOwned,
484            T::Context: Default,
485            S: zod_rs::Schema<serde_json::Value>,
486        {
487            let node = parse_typed_json::<T, S>(schema, value)?;
488            self.add_validated_node(&node)
489        }
490
491        #[cfg(feature = "typed-zod-rs")]
492        pub fn add_node_from_json_with<T, S>(
493            &mut self,
494            schema: &S,
495            value: &serde_json::Value,
496            ctx: &T::Context,
497        ) -> Result<NodeId>
498        where
499            T: TypedNode + DeserializeOwned,
500            S: zod_rs::Schema<serde_json::Value>,
501        {
502            let node = parse_typed_json_with::<T, S>(schema, value, ctx)?;
503            self.add_validated_node(&node)
504        }
505
506        #[cfg(feature = "typed-zod-rs")]
507        pub fn add_edge_from_json<T, S>(
508            &mut self,
509            schema: &S,
510            value: &serde_json::Value,
511        ) -> Result<PutOutcome>
512        where
513            T: TypedEdge + DeserializeOwned,
514            T::Context: Default,
515            S: zod_rs::Schema<serde_json::Value>,
516        {
517            let edge = parse_typed_json::<T, S>(schema, value)?;
518            self.add_validated_edge(&edge)
519        }
520
521        #[cfg(feature = "typed-zod-rs")]
522        pub fn add_edge_from_json_with<T, S>(
523            &mut self,
524            schema: &S,
525            value: &serde_json::Value,
526            ctx: &T::Context,
527        ) -> Result<PutOutcome>
528        where
529            T: TypedEdge + DeserializeOwned,
530            S: zod_rs::Schema<serde_json::Value>,
531        {
532            let edge = parse_typed_json_with::<T, S>(schema, value, ctx)?;
533            self.add_validated_edge(&edge)
534        }
535
536        pub fn build(self) -> Graph {
537            self.builder.build()
538        }
539
540        pub fn into_builder(self) -> GraphBuilder {
541            self.builder
542        }
543
544        fn add_validated_node<T>(&mut self, node: &T) -> Result<NodeId>
545        where
546            T: TypedNode,
547        {
548            let node_id = node.node_id();
549            let mut props = node.node_props()?;
550            props.insert("id".to_string(), Value::from(node_id.as_str()));
551            let graph_node = Node::new(T::LABEL, node_id, props);
552            Ok(self.builder.add_node(graph_node))
553        }
554
555        fn add_validated_edge<T>(&mut self, edge: &T) -> Result<PutOutcome>
556        where
557            T: TypedEdge,
558        {
559            let mut graph_edge = Edge::new(
560                T::LABEL,
561                edge.source_node_id(),
562                edge.target_node_id(),
563                edge.edge_props()?,
564            );
565            graph_edge.id = edge.edge_id();
566            Ok(self.builder.add_edge(graph_edge))
567        }
568    }
569
570    pub fn props_from_serialize<T>(value: &T) -> Result<Props>
571    where
572        T: Serialize + ?Sized,
573    {
574        let serialized = serde_json::to_value(value)
575            .map_err(|err| GrustError::Serialization(format!("typed props error: {err}")))?;
576        let serde_json::Value::Object(fields) = serialized else {
577            return Err(GrustError::Schema(
578                "typed graph values must serialize as JSON objects".to_string(),
579            ));
580        };
581
582        Ok(fields
583            .into_iter()
584            .map(|(key, value)| (key, Value::from(value)))
585            .collect())
586    }
587
588    #[cfg(feature = "typed-zod-rs")]
589    pub fn parse_typed_json<T, S>(schema: &S, value: &serde_json::Value) -> Result<T>
590    where
591        T: DeserializeOwned + garde::Validate,
592        T::Context: Default,
593        S: zod_rs::Schema<serde_json::Value>,
594    {
595        let ctx = T::Context::default();
596        parse_typed_json_with(schema, value, &ctx)
597    }
598
599    #[cfg(feature = "typed-zod-rs")]
600    pub fn parse_typed_json_with<T, S>(
601        schema: &S,
602        value: &serde_json::Value,
603        ctx: &T::Context,
604    ) -> Result<T>
605    where
606        T: DeserializeOwned + garde::Validate,
607        S: zod_rs::Schema<serde_json::Value>,
608    {
609        schema
610            .safe_parse(value)
611            .map_err(|err| GrustError::Schema(format!("zod-rs validation failed: {err}")))?;
612        let typed: T = serde_json::from_value(value.clone())
613            .map_err(|err| GrustError::Serialization(format!("typed JSON decode error: {err}")))?;
614        typed
615            .validate_with(ctx)
616            .map_err(|err| GrustError::Schema(format!("typed validation failed: {err}")))?;
617        Ok(typed)
618    }
619
620    fn validation_error(label: &str, err: garde::Report) -> GrustError {
621        GrustError::Schema(format!("{label} validation failed: {err}"))
622    }
623}
624
625#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
626pub struct Node {
627    pub id: NodeId,
628    pub label: Label,
629    pub props: Props,
630}
631
632impl Node {
633    pub fn new(label: impl Into<Label>, id: impl Into<NodeId>, props: impl Into<Props>) -> Self {
634        let id = id.into();
635        let mut props = props.into();
636        props
637            .entry("id".to_string())
638            .or_insert_with(|| Value::from(id.as_str()));
639        Self {
640            id,
641            label: label.into(),
642            props,
643        }
644    }
645}
646
647#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
648pub struct Edge {
649    pub id: Option<EdgeId>,
650    pub from: NodeId,
651    pub to: NodeId,
652    pub label: Label,
653    pub props: Props,
654}
655
656impl Edge {
657    pub fn new(
658        label: impl Into<Label>,
659        from: impl Into<NodeId>,
660        to: impl Into<NodeId>,
661        props: impl Into<Props>,
662    ) -> Self {
663        Self {
664            id: None,
665            from: from.into(),
666            to: to.into(),
667            label: label.into(),
668            props: props.into(),
669        }
670    }
671
672    pub fn with_id(mut self, id: impl Into<EdgeId>) -> Self {
673        self.id = Some(id.into());
674        self
675    }
676}
677
678#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
679pub struct Graph {
680    pub nodes: Vec<Node>,
681    pub edges: Vec<Edge>,
682}
683
684impl Graph {
685    pub fn new(nodes: Vec<Node>, edges: Vec<Edge>) -> Self {
686        Self { nodes, edges }
687    }
688
689    pub fn from_yaml(yaml: &str) -> Result<Self> {
690        yaml::graph_from_yaml(yaml)
691    }
692
693    pub fn to_yaml(&self) -> Result<String> {
694        yaml::graph_to_yaml(self)
695    }
696
697    pub fn from_json(json: &str) -> Result<Self> {
698        json::graph_from_json(json)
699    }
700
701    pub fn to_json(&self) -> Result<String> {
702        json::graph_to_json(self)
703    }
704
705    pub fn from_xml(xml: &str) -> Result<Self> {
706        xml::graph_from_xml(xml)
707    }
708
709    pub fn to_xml(&self) -> Result<String> {
710        xml::graph_to_xml(self)
711    }
712
713    pub fn builder() -> GraphBuilder {
714        GraphBuilder::new()
715    }
716}
717
718mod graph_doc {
719    use std::collections::{BTreeMap, BTreeSet};
720
721    use serde::{Deserialize, Serialize};
722
723    use crate::{Edge, EdgeId, Graph, GrustError, Label, Node, NodeId, Props, Value};
724
725    #[derive(Debug, Serialize, Deserialize)]
726    pub(super) struct GraphDoc {
727        #[serde(default)]
728        pub(super) nodes: Vec<NodeDoc>,
729        #[serde(default)]
730        pub(super) edges: Vec<EdgeDoc>,
731    }
732
733    #[derive(Debug, Serialize, Deserialize)]
734    pub(super) struct NodeDoc {
735        pub(super) id: NodeId,
736        pub(super) label: Label,
737        #[serde(default, deserialize_with = "deserialize_props")]
738        pub(super) props: Props,
739    }
740
741    #[derive(Debug, Serialize, Deserialize)]
742    pub(super) struct NodeDocOut {
743        pub(super) id: NodeId,
744        pub(super) label: Label,
745        #[serde(default)]
746        pub(super) props: Props,
747    }
748
749    #[derive(Debug, Serialize, Deserialize)]
750    pub(super) struct EdgeDoc {
751        #[serde(default)]
752        pub(super) id: Option<EdgeId>,
753        pub(super) label: Label,
754        pub(super) from: NodeId,
755        pub(super) to: NodeId,
756        #[serde(default, deserialize_with = "deserialize_props")]
757        pub(super) props: Props,
758    }
759
760    #[derive(Debug, Serialize, Deserialize)]
761    pub(super) struct EdgeDocOut {
762        #[serde(default)]
763        pub(super) id: Option<EdgeId>,
764        pub(super) label: Label,
765        pub(super) from: NodeId,
766        pub(super) to: NodeId,
767        #[serde(default)]
768        pub(super) props: Props,
769    }
770
771    pub(super) fn graph_from_doc(doc: GraphDoc) -> super::Result<Graph> {
772        let mut ids = BTreeSet::new();
773        for node in &doc.nodes {
774            if !ids.insert(node.id.clone()) {
775                return Err(GrustError::Schema(format!(
776                    "duplicate node id '{}'",
777                    node.id
778                )));
779            }
780        }
781
782        let mut edges = Vec::with_capacity(doc.edges.len());
783        for edge in doc.edges {
784            if !ids.contains(&edge.from) {
785                return Err(GrustError::Schema(format!(
786                    "edge '{}' references unknown from node '{}'",
787                    edge.label, edge.from
788                )));
789            }
790            if !ids.contains(&edge.to) {
791                return Err(GrustError::Schema(format!(
792                    "edge '{}' references unknown to node '{}'",
793                    edge.label, edge.to
794                )));
795            }
796
797            let mut graph_edge = Edge::new(edge.label, edge.from, edge.to, edge.props);
798            graph_edge.id = edge.id;
799            edges.push(graph_edge);
800        }
801
802        let nodes = doc
803            .nodes
804            .into_iter()
805            .map(|node| Node::new(node.label, node.id, node.props))
806            .collect();
807
808        Ok(Graph::new(nodes, edges))
809    }
810
811    pub(super) fn graph_to_doc(graph: &Graph) -> GraphDocOut {
812        GraphDocOut {
813            nodes: graph
814                .nodes
815                .iter()
816                .map(|node| NodeDocOut {
817                    id: node.id.clone(),
818                    label: node.label.clone(),
819                    props: without_generated_id(&node.props, &node.id),
820                })
821                .collect(),
822            edges: graph
823                .edges
824                .iter()
825                .map(|edge| EdgeDocOut {
826                    id: edge.id.clone(),
827                    label: edge.label.clone(),
828                    from: edge.from.clone(),
829                    to: edge.to.clone(),
830                    props: edge.props.clone(),
831                })
832                .collect(),
833        }
834    }
835
836    fn without_generated_id(props: &Props, id: &NodeId) -> Props {
837        let mut props = props.clone();
838        if props.get("id") == Some(&Value::from(id.as_str())) {
839            props.remove("id");
840        }
841        props
842    }
843
844    fn deserialize_props<'de, D>(deserializer: D) -> std::result::Result<Props, D::Error>
845    where
846        D: serde::Deserializer<'de>,
847    {
848        let raw = BTreeMap::<String, serde_json::Value>::deserialize(deserializer)?;
849        raw.into_iter()
850            .map(|(key, value)| {
851                value_from_json(value)
852                    .map(|value| (key, value))
853                    .map_err(serde::de::Error::custom)
854            })
855            .collect()
856    }
857
858    fn value_from_json(value: serde_json::Value) -> std::result::Result<Value, String> {
859        if let serde_json::Value::Object(mapping) = &value
860            && mapping.contains_key("type")
861            && mapping.contains_key("value")
862        {
863            return serde_json::from_value(value)
864                .map_err(|err| format!("invalid tagged Grust value: {err}"));
865        }
866
867        Ok(Value::from_json(value))
868    }
869
870    #[derive(Debug, Serialize, Deserialize)]
871    pub(super) struct GraphDocOut {
872        pub(super) nodes: Vec<NodeDocOut>,
873        pub(super) edges: Vec<EdgeDocOut>,
874    }
875}
876
877mod yaml {
878    use crate::{Graph, GrustError};
879
880    pub(super) fn graph_from_yaml(yaml: &str) -> super::Result<Graph> {
881        let doc: super::graph_doc::GraphDoc = serde_yaml::from_str(yaml)
882            .map_err(|err| GrustError::Serialization(format!("YAML parse error: {err}")))?;
883        super::graph_doc::graph_from_doc(doc)
884    }
885
886    pub(super) fn graph_to_yaml(graph: &Graph) -> super::Result<String> {
887        serde_yaml::to_string(&super::graph_doc::graph_to_doc(graph))
888            .map_err(|err| GrustError::Serialization(format!("YAML serialization error: {err}")))
889    }
890}
891
892mod json {
893    use crate::{Graph, GrustError};
894
895    pub(super) fn graph_from_json(json: &str) -> super::Result<Graph> {
896        let doc: super::graph_doc::GraphDoc = serde_json::from_str(json)
897            .map_err(|err| GrustError::Serialization(format!("JSON parse error: {err}")))?;
898        super::graph_doc::graph_from_doc(doc)
899    }
900
901    pub(super) fn graph_to_json(graph: &Graph) -> super::Result<String> {
902        serde_json::to_string_pretty(&super::graph_doc::graph_to_doc(graph))
903            .map_err(|err| GrustError::Serialization(format!("JSON serialization error: {err}")))
904    }
905}
906
907mod xml {
908    use serde::{Deserialize, Serialize};
909
910    use crate::{Edge, EdgeId, Graph, GrustError, Label, Node, NodeId, Props, Value};
911
912    #[derive(Debug, Serialize, Deserialize)]
913    #[serde(rename = "graph")]
914    struct GraphXml {
915        #[serde(default)]
916        nodes: NodesXml,
917        #[serde(default)]
918        edges: EdgesXml,
919    }
920
921    #[derive(Debug, Default, Serialize, Deserialize)]
922    struct NodesXml {
923        #[serde(rename = "node", default)]
924        items: Vec<NodeXml>,
925    }
926
927    #[derive(Debug, Default, Serialize, Deserialize)]
928    struct EdgesXml {
929        #[serde(rename = "edge", default)]
930        items: Vec<EdgeXml>,
931    }
932
933    #[derive(Debug, Serialize, Deserialize)]
934    struct NodeXml {
935        id: NodeId,
936        label: Label,
937        #[serde(default)]
938        props: PropsXml,
939    }
940
941    #[derive(Debug, Serialize, Deserialize)]
942    struct EdgeXml {
943        #[serde(default, skip_serializing_if = "Option::is_none")]
944        id: Option<EdgeId>,
945        label: Label,
946        from: NodeId,
947        to: NodeId,
948        #[serde(default)]
949        props: PropsXml,
950    }
951
952    #[derive(Debug, Default, Serialize, Deserialize)]
953    struct PropsXml {
954        #[serde(rename = "prop", default)]
955        items: Vec<PropXml>,
956    }
957
958    #[derive(Debug, Serialize, Deserialize)]
959    struct PropXml {
960        key: String,
961        value: Value,
962    }
963
964    pub(super) fn graph_from_xml(xml: &str) -> super::Result<Graph> {
965        let doc: GraphXml = quick_xml::de::from_str(xml)
966            .map_err(|err| GrustError::Serialization(format!("XML parse error: {err}")))?;
967        super::graph_doc::graph_from_doc(doc.into())
968    }
969
970    pub(super) fn graph_to_xml(graph: &Graph) -> super::Result<String> {
971        quick_xml::se::to_string(&GraphXml::from(graph))
972            .map_err(|err| GrustError::Serialization(format!("XML serialization error: {err}")))
973    }
974
975    impl From<GraphXml> for super::graph_doc::GraphDoc {
976        fn from(value: GraphXml) -> Self {
977            Self {
978                nodes: value.nodes.items.into_iter().map(Into::into).collect(),
979                edges: value.edges.items.into_iter().map(Into::into).collect(),
980            }
981        }
982    }
983
984    impl From<NodeXml> for super::graph_doc::NodeDoc {
985        fn from(value: NodeXml) -> Self {
986            Self {
987                id: value.id,
988                label: value.label,
989                props: value.props.into(),
990            }
991        }
992    }
993
994    impl From<EdgeXml> for super::graph_doc::EdgeDoc {
995        fn from(value: EdgeXml) -> Self {
996            Self {
997                id: value.id,
998                label: value.label,
999                from: value.from,
1000                to: value.to,
1001                props: value.props.into(),
1002            }
1003        }
1004    }
1005
1006    impl From<PropsXml> for Props {
1007        fn from(value: PropsXml) -> Self {
1008            value
1009                .items
1010                .into_iter()
1011                .map(|prop| (prop.key, prop.value))
1012                .collect()
1013        }
1014    }
1015
1016    impl From<&Graph> for GraphXml {
1017        fn from(graph: &Graph) -> Self {
1018            Self {
1019                nodes: NodesXml {
1020                    items: graph.nodes.iter().map(NodeXml::from).collect(),
1021                },
1022                edges: EdgesXml {
1023                    items: graph.edges.iter().map(EdgeXml::from).collect(),
1024                },
1025            }
1026        }
1027    }
1028
1029    impl From<&Node> for NodeXml {
1030        fn from(node: &Node) -> Self {
1031            let props = super::graph_doc::graph_to_doc(&Graph::new(vec![node.clone()], Vec::new()))
1032                .nodes
1033                .into_iter()
1034                .next()
1035                .expect("node exists")
1036                .props;
1037            Self {
1038                id: node.id.clone(),
1039                label: node.label.clone(),
1040                props: props.into(),
1041            }
1042        }
1043    }
1044
1045    impl From<&Edge> for EdgeXml {
1046        fn from(edge: &Edge) -> Self {
1047            Self {
1048                id: edge.id.clone(),
1049                label: edge.label.clone(),
1050                from: edge.from.clone(),
1051                to: edge.to.clone(),
1052                props: edge.props.clone().into(),
1053            }
1054        }
1055    }
1056
1057    impl From<Props> for PropsXml {
1058        fn from(value: Props) -> Self {
1059            Self {
1060                items: value
1061                    .into_iter()
1062                    .map(|(key, value)| PropXml { key, value })
1063                    .collect(),
1064            }
1065        }
1066    }
1067}
1068
1069#[derive(Clone, Debug, Default, Eq, PartialEq)]
1070pub enum EdgePolicy {
1071    AllowDuplicates,
1072    #[default]
1073    DedupeByFromLabelTo,
1074}
1075
1076#[derive(Clone, Debug, Default)]
1077pub struct GraphBuilder {
1078    nodes: BTreeMap<NodeId, Node>,
1079    edges: Vec<Edge>,
1080    edge_keys: BTreeSet<(NodeId, Label, NodeId)>,
1081    edge_policy: EdgePolicy,
1082}
1083
1084impl GraphBuilder {
1085    pub fn new() -> Self {
1086        Self::default()
1087    }
1088
1089    pub fn edge_policy(mut self, edge_policy: EdgePolicy) -> Self {
1090        self.edge_policy = edge_policy;
1091        self
1092    }
1093
1094    pub fn node<'a>(
1095        &'a mut self,
1096        label: impl Into<Label>,
1097        id: impl Into<NodeId>,
1098    ) -> NodeBuilder<'a> {
1099        NodeBuilder {
1100            builder: self,
1101            label: label.into(),
1102            id: id.into(),
1103            props: Props::new(),
1104        }
1105    }
1106
1107    pub fn edge<'a>(
1108        &'a mut self,
1109        label: impl Into<Label>,
1110        from: impl Into<NodeId>,
1111        to: impl Into<NodeId>,
1112    ) -> EdgeBuilder<'a> {
1113        EdgeBuilder {
1114            builder: self,
1115            id: None,
1116            label: label.into(),
1117            from: from.into(),
1118            to: to.into(),
1119            props: Props::new(),
1120        }
1121    }
1122
1123    /// Adds a node, merging with any existing node that has the same id.
1124    ///
1125    /// If a node with the same id and the same label already exists, the new
1126    /// props are merged in (new values win). If a node with the same id but a
1127    /// different label exists, the new node replaces it entirely (last write
1128    /// wins, matching `GraphStore::put_node` overwrite semantics).
1129    pub fn add_node(&mut self, node: Node) -> NodeId {
1130        let id = node.id.clone();
1131        self.nodes
1132            .entry(id.clone())
1133            .and_modify(|existing| {
1134                if existing.label == node.label {
1135                    existing.props.extend(node.props.clone());
1136                } else {
1137                    *existing = node.clone();
1138                }
1139            })
1140            .or_insert(node);
1141        id
1142    }
1143
1144    /// Adds an edge, reporting whether it was stored or dropped by the
1145    /// builder's [`EdgePolicy`].
1146    pub fn add_edge(&mut self, edge: Edge) -> PutOutcome {
1147        match self.edge_policy {
1148            EdgePolicy::AllowDuplicates => {
1149                self.edges.push(edge);
1150                PutOutcome::Inserted
1151            }
1152            EdgePolicy::DedupeByFromLabelTo => {
1153                let key = (edge.from.clone(), edge.label.clone(), edge.to.clone());
1154                if self.edge_keys.insert(key) {
1155                    self.edges.push(edge);
1156                    PutOutcome::Inserted
1157                } else {
1158                    PutOutcome::Deduped
1159                }
1160            }
1161        }
1162    }
1163
1164    pub fn build(self) -> Graph {
1165        Graph {
1166            nodes: self.nodes.into_values().collect(),
1167            edges: self.edges,
1168        }
1169    }
1170}
1171
1172pub struct NodeBuilder<'a> {
1173    builder: &'a mut GraphBuilder,
1174    label: Label,
1175    id: NodeId,
1176    props: Props,
1177}
1178
1179impl<'a> NodeBuilder<'a> {
1180    pub fn prop(mut self, key: impl Into<String>, value: impl Into<Value>) -> Self {
1181        self.props.insert(key.into(), value.into());
1182        self
1183    }
1184
1185    pub fn props(mut self, props: Props) -> Self {
1186        self.props.extend(props);
1187        self
1188    }
1189
1190    pub fn finish(self) -> NodeId {
1191        let node = Node::new(self.label, self.id, self.props);
1192        self.builder.add_node(node)
1193    }
1194}
1195
1196pub struct EdgeBuilder<'a> {
1197    builder: &'a mut GraphBuilder,
1198    id: Option<EdgeId>,
1199    label: Label,
1200    from: NodeId,
1201    to: NodeId,
1202    props: Props,
1203}
1204
1205impl<'a> EdgeBuilder<'a> {
1206    pub fn id(mut self, id: impl Into<EdgeId>) -> Self {
1207        self.id = Some(id.into());
1208        self
1209    }
1210
1211    pub fn prop(mut self, key: impl Into<String>, value: impl Into<Value>) -> Self {
1212        self.props.insert(key.into(), value.into());
1213        self
1214    }
1215
1216    pub fn props(mut self, props: Props) -> Self {
1217        self.props.extend(props);
1218        self
1219    }
1220
1221    pub fn finish(self) -> PutOutcome {
1222        let mut edge = Edge::new(self.label, self.from, self.to, self.props);
1223        edge.id = self.id;
1224        self.builder.add_edge(edge)
1225    }
1226}
1227
1228#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1229pub struct GraphSchema {
1230    pub nodes: Vec<NodeType>,
1231    pub edges: Vec<EdgeType>,
1232}
1233
1234impl GraphSchema {
1235    pub fn builder() -> GraphSchemaBuilder {
1236        GraphSchemaBuilder::default()
1237    }
1238
1239    pub fn node_type(&self, label: &Label) -> Option<&NodeType> {
1240        self.nodes
1241            .iter()
1242            .find(|node_type| &node_type.label == label)
1243    }
1244
1245    pub fn edge_type(&self, label: &Label) -> Option<&EdgeType> {
1246        self.edges
1247            .iter()
1248            .find(|edge_type| &edge_type.label == label)
1249    }
1250
1251    pub fn validate_graph(&self, graph: &Graph) -> Result<()> {
1252        for node in &graph.nodes {
1253            self.validate_node(node)?;
1254        }
1255        let labels: BTreeMap<&NodeId, &Label> = graph
1256            .nodes
1257            .iter()
1258            .map(|node| (&node.id, &node.label))
1259            .collect();
1260        for edge in &graph.edges {
1261            self.validate_edge_with(edge, |id| labels.get(id).copied())?;
1262        }
1263        self.validate_edge_uniqueness(graph)
1264    }
1265
1266    /// Enforces each edge type's [`EdgeUniqueness`]: at most one edge of the
1267    /// type between a given endpoint pair (unordered for undirected types).
1268    fn validate_edge_uniqueness(&self, graph: &Graph) -> Result<()> {
1269        let mut seen = BTreeSet::new();
1270        for edge in &graph.edges {
1271            let Some(edge_type) = self.edge_type(&edge.label) else {
1272                continue;
1273            };
1274            if edge_type.uniqueness == EdgeUniqueness::None {
1275                continue;
1276            }
1277            let (a, b) = if edge_type.directed || edge.from <= edge.to {
1278                (&edge.from, &edge.to)
1279            } else {
1280                (&edge.to, &edge.from)
1281            };
1282            if !seen.insert((edge.label.clone(), a.clone(), b.clone())) {
1283                return Err(GrustError::Schema(format!(
1284                    "duplicate edge '{}' between '{}' and '{}' violates {:?} uniqueness",
1285                    edge.label.as_str(),
1286                    a.as_str(),
1287                    b.as_str(),
1288                    edge_type.uniqueness
1289                )));
1290            }
1291        }
1292        Ok(())
1293    }
1294
1295    pub fn validate_node(&self, node: &Node) -> Result<()> {
1296        let node_type = self.node_type(&node.label).ok_or_else(|| {
1297            GrustError::Schema(format!("schema has no node type '{}'", node.label.as_str()))
1298        })?;
1299        validate_props(
1300            &node.props,
1301            &node_type.fields,
1302            &format!("node '{}'", node.id.as_str()),
1303        )
1304    }
1305
1306    pub fn validate_edge(&self, edge: &Edge, graph: &Graph) -> Result<()> {
1307        self.validate_edge_with(edge, |id| {
1308            graph
1309                .nodes
1310                .iter()
1311                .find(|node| &node.id == id)
1312                .map(|node| &node.label)
1313        })
1314    }
1315
1316    /// Validates an edge using a label lookup instead of a full `Graph`, so
1317    /// stores can validate against their own node index without cloning.
1318    pub fn validate_edge_with<'a>(
1319        &self,
1320        edge: &Edge,
1321        lookup: impl Fn(&NodeId) -> Option<&'a Label>,
1322    ) -> Result<()> {
1323        let edge_type = self.edge_type(&edge.label).ok_or_else(|| {
1324            GrustError::Schema(format!("schema has no edge type '{}'", edge.label.as_str()))
1325        })?;
1326
1327        let from_label = lookup(&edge.from).ok_or_else(|| {
1328            GrustError::Schema(format!(
1329                "edge '{}' references unknown from node '{}'",
1330                edge.label.as_str(),
1331                edge.from.as_str()
1332            ))
1333        })?;
1334        let to_label = lookup(&edge.to).ok_or_else(|| {
1335            GrustError::Schema(format!(
1336                "edge '{}' references unknown to node '{}'",
1337                edge.label.as_str(),
1338                edge.to.as_str()
1339            ))
1340        })?;
1341
1342        let from_matches =
1343            |label: &Label| edge_type.from.is_empty() || edge_type.from.contains(label);
1344        let to_matches = |label: &Label| edge_type.to.is_empty() || edge_type.to.contains(label);
1345        // Undirected edge types accept their endpoint labels in either
1346        // orientation.
1347        let endpoints_ok = (from_matches(from_label) && to_matches(to_label))
1348            || (!edge_type.directed && from_matches(to_label) && to_matches(from_label));
1349        if !endpoints_ok {
1350            if !from_matches(from_label) {
1351                return Err(GrustError::Schema(format!(
1352                    "edge '{}' cannot start from node label '{}'",
1353                    edge.label.as_str(),
1354                    from_label.as_str()
1355                )));
1356            }
1357            return Err(GrustError::Schema(format!(
1358                "edge '{}' cannot end at node label '{}'",
1359                edge.label.as_str(),
1360                to_label.as_str()
1361            )));
1362        }
1363
1364        validate_props(
1365            &edge.props,
1366            &edge_type.fields,
1367            &format!("edge '{}'", edge.label.as_str()),
1368        )
1369    }
1370
1371    /// Validates an edge's label and props against the schema without
1372    /// checking endpoint nodes, for stores that persist a single edge and
1373    /// cannot cheaply resolve its endpoints.
1374    pub fn validate_edge_props(&self, edge: &Edge) -> Result<()> {
1375        let edge_type = self.edge_type(&edge.label).ok_or_else(|| {
1376            GrustError::Schema(format!("schema has no edge type '{}'", edge.label.as_str()))
1377        })?;
1378        validate_props(
1379            &edge.props,
1380            &edge_type.fields,
1381            &format!("edge '{}'", edge.label.as_str()),
1382        )
1383    }
1384}
1385
1386#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1387pub struct NodeType {
1388    pub label: Label,
1389    pub fields: Vec<Field>,
1390}
1391
1392#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1393pub struct EdgeType {
1394    pub label: Label,
1395    pub from: Vec<Label>,
1396    pub to: Vec<Label>,
1397    pub fields: Vec<Field>,
1398    pub directed: bool,
1399    pub uniqueness: EdgeUniqueness,
1400}
1401
1402#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1403pub struct Field {
1404    pub name: String,
1405    pub ty: FieldType,
1406    pub required: bool,
1407}
1408
1409#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1410pub enum FieldType {
1411    String,
1412    Int,
1413    Float,
1414    Bool,
1415    DateTime,
1416    StringArray,
1417    IntArray,
1418    FloatArray,
1419    Json,
1420}
1421
1422/// How many edges of one type may exist between a pair of nodes.
1423///
1424/// `validate_graph` enforces `FromTo` and `FromLabelTo` identically — at most
1425/// one edge of the type between a given endpoint pair (unordered when the
1426/// type is undirected). The distinction is a storage-key hint for backends
1427/// that keep all edge labels in one table: `FromLabelTo` keys include the
1428/// label, `FromTo` keys do not.
1429#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1430pub enum EdgeUniqueness {
1431    None,
1432    FromTo,
1433    FromLabelTo,
1434}
1435
1436#[derive(Clone, Debug, Default)]
1437pub struct GraphSchemaBuilder {
1438    nodes: Vec<NodeType>,
1439    edges: Vec<EdgeType>,
1440}
1441
1442impl GraphSchemaBuilder {
1443    pub fn node(mut self, label: impl Into<Label>, fields: impl Into<Vec<Field>>) -> Self {
1444        self.nodes.push(NodeType {
1445            label: label.into(),
1446            fields: fields.into(),
1447        });
1448        self
1449    }
1450
1451    pub fn edge(
1452        mut self,
1453        label: impl Into<Label>,
1454        from: impl Into<Vec<Label>>,
1455        to: impl Into<Vec<Label>>,
1456        fields: impl Into<Vec<Field>>,
1457    ) -> Self {
1458        self.edges.push(EdgeType {
1459            label: label.into(),
1460            from: from.into(),
1461            to: to.into(),
1462            fields: fields.into(),
1463            directed: true,
1464            uniqueness: EdgeUniqueness::FromLabelTo,
1465        });
1466        self
1467    }
1468
1469    pub fn edge_type(mut self, edge_type: EdgeType) -> Self {
1470        self.edges.push(edge_type);
1471        self
1472    }
1473
1474    pub fn build(self) -> GraphSchema {
1475        GraphSchema {
1476            nodes: self.nodes,
1477            edges: self.edges,
1478        }
1479    }
1480}
1481
1482impl Field {
1483    pub fn required(name: impl Into<String>, ty: FieldType) -> Self {
1484        Self {
1485            name: name.into(),
1486            ty,
1487            required: true,
1488        }
1489    }
1490
1491    pub fn optional(name: impl Into<String>, ty: FieldType) -> Self {
1492        Self {
1493            name: name.into(),
1494            ty,
1495            required: false,
1496        }
1497    }
1498}
1499
1500fn validate_props(props: &Props, fields: &[Field], context: &str) -> Result<()> {
1501    for field in fields {
1502        match props.get(&field.name) {
1503            Some(value) => validate_field_value(value, &field.ty, context, &field.name)?,
1504            None if field.required => {
1505                return Err(GrustError::Schema(format!(
1506                    "{context} missing required field '{}'",
1507                    field.name
1508                )));
1509            }
1510            None => {}
1511        }
1512    }
1513    Ok(())
1514}
1515
1516fn validate_field_value(
1517    value: &Value,
1518    ty: &FieldType,
1519    context: &str,
1520    field_name: &str,
1521) -> Result<()> {
1522    let matches = match (value, ty) {
1523        (Value::String(_), FieldType::String)
1524        | (Value::Int(_), FieldType::Int)
1525        | (Value::Float(_), FieldType::Float)
1526        | (Value::Bool(_), FieldType::Bool)
1527        | (Value::DateTime(_), FieldType::DateTime)
1528        | (Value::StringArray(_), FieldType::StringArray)
1529        | (Value::IntArray(_), FieldType::IntArray)
1530        | (Value::FloatArray(_), FieldType::FloatArray)
1531        | (_, FieldType::Json) => true,
1532        // Plain strings remain valid date-times for backward compatibility,
1533        // but must still parse as RFC 3339.
1534        (Value::String(value), FieldType::DateTime) => is_rfc3339_datetime(value),
1535        _ => false,
1536    };
1537    if matches {
1538        Ok(())
1539    } else {
1540        Err(GrustError::Schema(format!(
1541            "{context} field '{field_name}' expected {ty:?}, got {value:?}"
1542        )))
1543    }
1544}
1545
1546#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1547pub struct Traversal {
1548    pub start: Start,
1549    pub steps: Vec<Step>,
1550    pub limit: Option<u32>,
1551}
1552
1553impl Traversal {
1554    pub fn from_node(id: impl Into<NodeId>) -> Self {
1555        Self {
1556            start: Start::Node(id.into()),
1557            steps: Vec::new(),
1558            limit: None,
1559        }
1560    }
1561
1562    pub fn out(mut self, edge: impl Into<Label>) -> Self {
1563        self.steps.push(Step {
1564            direction: Direction::Out,
1565            edge: Some(edge.into()),
1566            node: None,
1567        });
1568        self
1569    }
1570
1571    pub fn in_(mut self, edge: impl Into<Label>) -> Self {
1572        self.steps.push(Step {
1573            direction: Direction::In,
1574            edge: Some(edge.into()),
1575            node: None,
1576        });
1577        self
1578    }
1579
1580    pub fn both(mut self, edge: impl Into<Label>) -> Self {
1581        self.steps.push(Step {
1582            direction: Direction::Both,
1583            edge: Some(edge.into()),
1584            node: None,
1585        });
1586        self
1587    }
1588
1589    /// Constrains the target node label of the most recent step.
1590    ///
1591    /// # Panics
1592    ///
1593    /// Panics if called before any step has been added with `out`, `in_`, or
1594    /// `both`, since there is no step for the label to apply to.
1595    pub fn to(mut self, node: impl Into<Label>) -> Self {
1596        let step = self
1597            .steps
1598            .last_mut()
1599            .expect("Traversal::to() must follow out(), in_(), or both()");
1600        step.node = Some(node.into());
1601        self
1602    }
1603
1604    pub fn limit(mut self, limit: u32) -> Self {
1605        self.limit = Some(limit);
1606        self
1607    }
1608}
1609
1610#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1611pub enum Start {
1612    Node(NodeId),
1613    NodesByLabel(Label),
1614    NodesByProperty {
1615        label: Label,
1616        key: String,
1617        value: Value,
1618    },
1619}
1620
1621#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1622pub struct Step {
1623    pub direction: Direction,
1624    pub edge: Option<Label>,
1625    pub node: Option<Label>,
1626}
1627
1628#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1629pub enum Direction {
1630    Out,
1631    In,
1632    Both,
1633}
1634
1635#[derive(Clone, Debug, Default, PartialEq)]
1636pub struct EdgeQuery {
1637    pub from: Option<NodeId>,
1638    pub to: Option<NodeId>,
1639    pub label: Option<Label>,
1640}
1641
1642/// Outcome of writing a single node or edge.
1643#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
1644pub enum PutOutcome {
1645    /// The element did not exist before and was created.
1646    Inserted,
1647    /// An element with the same identity existed and was overwritten or
1648    /// merged.
1649    Updated,
1650    /// The element was written by an upsert and the backend cannot tell
1651    /// whether it was an insert or an update.
1652    Upserted,
1653    /// The element was dropped by a dedupe policy and nothing was written.
1654    Deduped,
1655}
1656
1657impl PutOutcome {
1658    /// True unless the element was deduped away.
1659    pub fn written(self) -> bool {
1660        !matches!(self, Self::Deduped)
1661    }
1662}
1663
1664/// Counts of nodes and edges written by a bulk load. Each count reflects an
1665/// upsert applied to the backend, not distinct newly created elements.
1666#[derive(Clone, Debug, Default, Eq, PartialEq)]
1667pub struct LoadReport {
1668    pub nodes: usize,
1669    pub edges: usize,
1670}
1671
1672#[async_trait]
1673pub trait GraphStore: Send + Sync {
1674    async fn apply_schema(&self, _schema: &GraphSchema) -> Result<()> {
1675        Ok(())
1676    }
1677
1678    async fn put_node(&self, node: &Node) -> Result<PutOutcome>;
1679    async fn put_edge(&self, edge: &Edge) -> Result<PutOutcome>;
1680
1681    async fn put_graph(&self, graph: &Graph) -> Result<LoadReport> {
1682        let mut report = LoadReport::default();
1683        for node in &graph.nodes {
1684            if self.put_node(node).await?.written() {
1685                report.nodes += 1;
1686            }
1687        }
1688        for edge in &graph.edges {
1689            if self.put_edge(edge).await?.written() {
1690                report.edges += 1;
1691            }
1692        }
1693        Ok(report)
1694    }
1695
1696    async fn put_typed_graph(&self, schema: &GraphSchema, graph: &Graph) -> Result<LoadReport> {
1697        schema.validate_graph(graph)?;
1698        self.apply_schema(schema).await?;
1699        self.put_graph(graph).await
1700    }
1701
1702    async fn get_node(&self, id: &NodeId) -> Result<Option<Node>>;
1703    async fn get_edges(&self, query: EdgeQuery) -> Result<Vec<Edge>>;
1704    async fn traverse(&self, traversal: Traversal) -> Result<Vec<Node>>;
1705}
1706
1707#[async_trait]
1708pub trait GraphAdminStore: GraphStore {
1709    async fn bootstrap(&self) -> Result<()> {
1710        Ok(())
1711    }
1712
1713    async fn clear(&self) -> Result<()>;
1714}
1715
1716/// A single incremental change to a graph, for delta-oriented pipelines.
1717#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1718pub enum GraphMutation {
1719    UpsertNode(Node),
1720    DeleteNode(NodeId),
1721    UpsertEdge(Edge),
1722    DeleteEdge {
1723        id: Option<EdgeId>,
1724        from: NodeId,
1725        label: Label,
1726        to: NodeId,
1727    },
1728}
1729
1730/// Incremental mutation support for stores that can delete elements.
1731///
1732/// Deletes are idempotent: removing an element that does not exist is not an
1733/// error.
1734#[async_trait]
1735pub trait GraphMutationStore: GraphStore {
1736    /// Deletes a node and all edges incident to it.
1737    async fn delete_node(&self, id: &NodeId) -> Result<()>;
1738
1739    /// Deletes the edge(s) matching `(from, label, to)`.
1740    async fn delete_edge(&self, from: &NodeId, label: &Label, to: &NodeId) -> Result<()>;
1741
1742    /// Applies mutations in order, stopping at the first error.
1743    async fn apply_mutations(&self, mutations: &[GraphMutation]) -> Result<()> {
1744        for mutation in mutations {
1745            match mutation {
1746                GraphMutation::UpsertNode(node) => {
1747                    self.put_node(node).await?;
1748                }
1749                GraphMutation::DeleteNode(id) => self.delete_node(id).await?,
1750                GraphMutation::UpsertEdge(edge) => {
1751                    self.put_edge(edge).await?;
1752                }
1753                GraphMutation::DeleteEdge {
1754                    from, label, to, ..
1755                } => self.delete_edge(from, label, to).await?,
1756            }
1757        }
1758        Ok(())
1759    }
1760}
1761
1762pub mod prelude {
1763    pub use crate::{
1764        Direction, Edge, EdgeId, EdgePolicy, EdgeQuery, EdgeType, EdgeUniqueness, Field, FieldType,
1765        Graph, GraphAdminStore, GraphBuilder, GraphMutation, GraphMutationStore, GraphSchema,
1766        GraphSchemaBuilder, GraphStore, GrustError, Label, LoadReport, Node, NodeId, NodeType,
1767        Props, PutOutcome, Result, Start, Step, Traversal, Value,
1768    };
1769
1770    #[cfg(feature = "typed-garde")]
1771    pub use crate::typed::{TypedEdge, TypedGraphBuilder, TypedNode, garde, props_from_serialize};
1772
1773    #[cfg(feature = "typed-zod-rs")]
1774    pub use crate::typed::{parse_typed_json, parse_typed_json_with, zod_rs};
1775}
1776
1777#[cfg(test)]
1778mod tests;