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/// Normalizes an edge label into an uppercase backend relationship type.
679///
680/// Non-ASCII-alphanumeric characters become underscores. Empty labels fall
681/// back to `RELATED_TO`.
682pub fn relationship_type(value: &str) -> String {
683    let relationship = value
684        .chars()
685        .map(|ch| {
686            if ch.is_ascii_alphanumeric() {
687                ch.to_ascii_uppercase()
688            } else {
689                '_'
690            }
691        })
692        .collect::<String>();
693    if relationship.is_empty() {
694        "RELATED_TO".to_string()
695    } else {
696        relationship
697    }
698}
699
700/// Normalizes arbitrary schema text into a lower_snake_case backend identifier.
701///
702/// This helper is for SQL-like backends that require identifiers to start with a
703/// non-digit ASCII alphanumeric or underscore character after normalization.
704pub fn schema_identifier(value: &str) -> Result<String> {
705    let identifier = value
706        .chars()
707        .map(|ch| {
708            if ch.is_ascii_alphanumeric() {
709                ch.to_ascii_lowercase()
710            } else {
711                '_'
712            }
713        })
714        .collect::<String>();
715    if identifier.is_empty()
716        || identifier
717            .chars()
718            .next()
719            .is_some_and(|ch| ch.is_ascii_digit())
720    {
721        return Err(GrustError::Schema(format!(
722            "invalid schema identifier '{value}'"
723        )));
724    }
725    Ok(identifier)
726}
727
728/// Returns the stable key used by tabular/export backends for an edge.
729///
730/// Explicit edge IDs win. Otherwise the structural key joins `from`, `label`,
731/// and `to` with U+001F (Unit Separator). Callers that accept arbitrary IDs or
732/// labels should reject U+001F before relying on reversibility.
733pub fn edge_key(edge: &Edge) -> String {
734    edge.id
735        .as_ref()
736        .map(EdgeId::as_str)
737        .map(ToString::to_string)
738        .unwrap_or_else(|| {
739            format!(
740                "{}\u{1f}{}\u{1f}{}",
741                edge.from.as_str(),
742                edge.label.as_str(),
743                edge.to.as_str()
744            )
745        })
746}
747
748#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
749pub struct Graph {
750    pub nodes: Vec<Node>,
751    pub edges: Vec<Edge>,
752}
753
754impl Graph {
755    pub fn new(nodes: Vec<Node>, edges: Vec<Edge>) -> Self {
756        Self { nodes, edges }
757    }
758
759    pub fn from_yaml(yaml: &str) -> Result<Self> {
760        yaml::graph_from_yaml(yaml)
761    }
762
763    pub fn to_yaml(&self) -> Result<String> {
764        yaml::graph_to_yaml(self)
765    }
766
767    pub fn from_json(json: &str) -> Result<Self> {
768        json::graph_from_json(json)
769    }
770
771    pub fn to_json(&self) -> Result<String> {
772        json::graph_to_json(self)
773    }
774
775    pub fn from_xml(xml: &str) -> Result<Self> {
776        xml::graph_from_xml(xml)
777    }
778
779    pub fn to_xml(&self) -> Result<String> {
780        xml::graph_to_xml(self)
781    }
782
783    pub fn builder() -> GraphBuilder {
784        GraphBuilder::new()
785    }
786}
787
788mod graph_doc {
789    use std::collections::{BTreeMap, BTreeSet};
790
791    use serde::{Deserialize, Serialize};
792
793    use crate::{Edge, EdgeId, Graph, GrustError, Label, Node, NodeId, Props, Value};
794
795    #[derive(Debug, Serialize, Deserialize)]
796    pub(super) struct GraphDoc {
797        #[serde(default)]
798        pub(super) nodes: Vec<NodeDoc>,
799        #[serde(default)]
800        pub(super) edges: Vec<EdgeDoc>,
801    }
802
803    #[derive(Debug, Serialize, Deserialize)]
804    pub(super) struct NodeDoc {
805        pub(super) id: NodeId,
806        pub(super) label: Label,
807        #[serde(default, deserialize_with = "deserialize_props")]
808        pub(super) props: Props,
809    }
810
811    #[derive(Debug, Serialize, Deserialize)]
812    pub(super) struct NodeDocOut {
813        pub(super) id: NodeId,
814        pub(super) label: Label,
815        #[serde(default)]
816        pub(super) props: Props,
817    }
818
819    #[derive(Debug, Serialize, Deserialize)]
820    pub(super) struct EdgeDoc {
821        #[serde(default)]
822        pub(super) id: Option<EdgeId>,
823        pub(super) label: Label,
824        pub(super) from: NodeId,
825        pub(super) to: NodeId,
826        #[serde(default, deserialize_with = "deserialize_props")]
827        pub(super) props: Props,
828    }
829
830    #[derive(Debug, Serialize, Deserialize)]
831    pub(super) struct EdgeDocOut {
832        #[serde(default)]
833        pub(super) id: Option<EdgeId>,
834        pub(super) label: Label,
835        pub(super) from: NodeId,
836        pub(super) to: NodeId,
837        #[serde(default)]
838        pub(super) props: Props,
839    }
840
841    pub(super) fn graph_from_doc(doc: GraphDoc) -> super::Result<Graph> {
842        let mut ids = BTreeSet::new();
843        for node in &doc.nodes {
844            if !ids.insert(node.id.clone()) {
845                return Err(GrustError::Schema(format!(
846                    "duplicate node id '{}'",
847                    node.id
848                )));
849            }
850        }
851
852        let mut edges = Vec::with_capacity(doc.edges.len());
853        for edge in doc.edges {
854            if !ids.contains(&edge.from) {
855                return Err(GrustError::Schema(format!(
856                    "edge '{}' references unknown from node '{}'",
857                    edge.label, edge.from
858                )));
859            }
860            if !ids.contains(&edge.to) {
861                return Err(GrustError::Schema(format!(
862                    "edge '{}' references unknown to node '{}'",
863                    edge.label, edge.to
864                )));
865            }
866
867            let mut graph_edge = Edge::new(edge.label, edge.from, edge.to, edge.props);
868            graph_edge.id = edge.id;
869            edges.push(graph_edge);
870        }
871
872        let nodes = doc
873            .nodes
874            .into_iter()
875            .map(|node| Node::new(node.label, node.id, node.props))
876            .collect();
877
878        Ok(Graph::new(nodes, edges))
879    }
880
881    pub(super) fn graph_to_doc(graph: &Graph) -> GraphDocOut {
882        GraphDocOut {
883            nodes: graph
884                .nodes
885                .iter()
886                .map(|node| NodeDocOut {
887                    id: node.id.clone(),
888                    label: node.label.clone(),
889                    props: without_generated_id(&node.props, &node.id),
890                })
891                .collect(),
892            edges: graph
893                .edges
894                .iter()
895                .map(|edge| EdgeDocOut {
896                    id: edge.id.clone(),
897                    label: edge.label.clone(),
898                    from: edge.from.clone(),
899                    to: edge.to.clone(),
900                    props: edge.props.clone(),
901                })
902                .collect(),
903        }
904    }
905
906    fn without_generated_id(props: &Props, id: &NodeId) -> Props {
907        let mut props = props.clone();
908        if props.get("id") == Some(&Value::from(id.as_str())) {
909            props.remove("id");
910        }
911        props
912    }
913
914    fn deserialize_props<'de, D>(deserializer: D) -> std::result::Result<Props, D::Error>
915    where
916        D: serde::Deserializer<'de>,
917    {
918        let raw = BTreeMap::<String, serde_json::Value>::deserialize(deserializer)?;
919        raw.into_iter()
920            .map(|(key, value)| {
921                value_from_json(value)
922                    .map(|value| (key, value))
923                    .map_err(serde::de::Error::custom)
924            })
925            .collect()
926    }
927
928    fn value_from_json(value: serde_json::Value) -> std::result::Result<Value, String> {
929        if let serde_json::Value::Object(mapping) = &value
930            && mapping.contains_key("type")
931            && mapping.contains_key("value")
932        {
933            return serde_json::from_value(value)
934                .map_err(|err| format!("invalid tagged Grust value: {err}"));
935        }
936
937        Ok(Value::from_json(value))
938    }
939
940    #[derive(Debug, Serialize, Deserialize)]
941    pub(super) struct GraphDocOut {
942        pub(super) nodes: Vec<NodeDocOut>,
943        pub(super) edges: Vec<EdgeDocOut>,
944    }
945}
946
947mod yaml {
948    use crate::{Graph, GrustError};
949
950    pub(super) fn graph_from_yaml(yaml: &str) -> super::Result<Graph> {
951        let doc: super::graph_doc::GraphDoc = serde_yaml::from_str(yaml)
952            .map_err(|err| GrustError::Serialization(format!("YAML parse error: {err}")))?;
953        super::graph_doc::graph_from_doc(doc)
954    }
955
956    pub(super) fn graph_to_yaml(graph: &Graph) -> super::Result<String> {
957        serde_yaml::to_string(&super::graph_doc::graph_to_doc(graph))
958            .map_err(|err| GrustError::Serialization(format!("YAML serialization error: {err}")))
959    }
960}
961
962mod json {
963    use crate::{Graph, GrustError};
964
965    pub(super) fn graph_from_json(json: &str) -> super::Result<Graph> {
966        let doc: super::graph_doc::GraphDoc = serde_json::from_str(json)
967            .map_err(|err| GrustError::Serialization(format!("JSON parse error: {err}")))?;
968        super::graph_doc::graph_from_doc(doc)
969    }
970
971    pub(super) fn graph_to_json(graph: &Graph) -> super::Result<String> {
972        serde_json::to_string_pretty(&super::graph_doc::graph_to_doc(graph))
973            .map_err(|err| GrustError::Serialization(format!("JSON serialization error: {err}")))
974    }
975}
976
977mod xml {
978    use serde::{Deserialize, Serialize};
979
980    use crate::{Edge, EdgeId, Graph, GrustError, Label, Node, NodeId, Props, Value};
981
982    #[derive(Debug, Serialize, Deserialize)]
983    #[serde(rename = "graph")]
984    struct GraphXml {
985        #[serde(default)]
986        nodes: NodesXml,
987        #[serde(default)]
988        edges: EdgesXml,
989    }
990
991    #[derive(Debug, Default, Serialize, Deserialize)]
992    struct NodesXml {
993        #[serde(rename = "node", default)]
994        items: Vec<NodeXml>,
995    }
996
997    #[derive(Debug, Default, Serialize, Deserialize)]
998    struct EdgesXml {
999        #[serde(rename = "edge", default)]
1000        items: Vec<EdgeXml>,
1001    }
1002
1003    #[derive(Debug, Serialize, Deserialize)]
1004    struct NodeXml {
1005        id: NodeId,
1006        label: Label,
1007        #[serde(default)]
1008        props: PropsXml,
1009    }
1010
1011    #[derive(Debug, Serialize, Deserialize)]
1012    struct EdgeXml {
1013        #[serde(default, skip_serializing_if = "Option::is_none")]
1014        id: Option<EdgeId>,
1015        label: Label,
1016        from: NodeId,
1017        to: NodeId,
1018        #[serde(default)]
1019        props: PropsXml,
1020    }
1021
1022    #[derive(Debug, Default, Serialize, Deserialize)]
1023    struct PropsXml {
1024        #[serde(rename = "prop", default)]
1025        items: Vec<PropXml>,
1026    }
1027
1028    #[derive(Debug, Serialize, Deserialize)]
1029    struct PropXml {
1030        key: String,
1031        value: Value,
1032    }
1033
1034    pub(super) fn graph_from_xml(xml: &str) -> super::Result<Graph> {
1035        let doc: GraphXml = quick_xml::de::from_str(xml)
1036            .map_err(|err| GrustError::Serialization(format!("XML parse error: {err}")))?;
1037        super::graph_doc::graph_from_doc(doc.into())
1038    }
1039
1040    pub(super) fn graph_to_xml(graph: &Graph) -> super::Result<String> {
1041        quick_xml::se::to_string(&GraphXml::from(graph))
1042            .map_err(|err| GrustError::Serialization(format!("XML serialization error: {err}")))
1043    }
1044
1045    impl From<GraphXml> for super::graph_doc::GraphDoc {
1046        fn from(value: GraphXml) -> Self {
1047            Self {
1048                nodes: value.nodes.items.into_iter().map(Into::into).collect(),
1049                edges: value.edges.items.into_iter().map(Into::into).collect(),
1050            }
1051        }
1052    }
1053
1054    impl From<NodeXml> for super::graph_doc::NodeDoc {
1055        fn from(value: NodeXml) -> Self {
1056            Self {
1057                id: value.id,
1058                label: value.label,
1059                props: value.props.into(),
1060            }
1061        }
1062    }
1063
1064    impl From<EdgeXml> for super::graph_doc::EdgeDoc {
1065        fn from(value: EdgeXml) -> Self {
1066            Self {
1067                id: value.id,
1068                label: value.label,
1069                from: value.from,
1070                to: value.to,
1071                props: value.props.into(),
1072            }
1073        }
1074    }
1075
1076    impl From<PropsXml> for Props {
1077        fn from(value: PropsXml) -> Self {
1078            value
1079                .items
1080                .into_iter()
1081                .map(|prop| (prop.key, prop.value))
1082                .collect()
1083        }
1084    }
1085
1086    impl From<&Graph> for GraphXml {
1087        fn from(graph: &Graph) -> Self {
1088            Self {
1089                nodes: NodesXml {
1090                    items: graph.nodes.iter().map(NodeXml::from).collect(),
1091                },
1092                edges: EdgesXml {
1093                    items: graph.edges.iter().map(EdgeXml::from).collect(),
1094                },
1095            }
1096        }
1097    }
1098
1099    impl From<&Node> for NodeXml {
1100        fn from(node: &Node) -> Self {
1101            let props = super::graph_doc::graph_to_doc(&Graph::new(vec![node.clone()], Vec::new()))
1102                .nodes
1103                .into_iter()
1104                .next()
1105                .expect("node exists")
1106                .props;
1107            Self {
1108                id: node.id.clone(),
1109                label: node.label.clone(),
1110                props: props.into(),
1111            }
1112        }
1113    }
1114
1115    impl From<&Edge> for EdgeXml {
1116        fn from(edge: &Edge) -> Self {
1117            Self {
1118                id: edge.id.clone(),
1119                label: edge.label.clone(),
1120                from: edge.from.clone(),
1121                to: edge.to.clone(),
1122                props: edge.props.clone().into(),
1123            }
1124        }
1125    }
1126
1127    impl From<Props> for PropsXml {
1128        fn from(value: Props) -> Self {
1129            Self {
1130                items: value
1131                    .into_iter()
1132                    .map(|(key, value)| PropXml { key, value })
1133                    .collect(),
1134            }
1135        }
1136    }
1137}
1138
1139#[derive(Clone, Debug, Default, Eq, PartialEq)]
1140pub enum EdgePolicy {
1141    AllowDuplicates,
1142    #[default]
1143    DedupeByFromLabelTo,
1144}
1145
1146#[derive(Clone, Debug, Default)]
1147pub struct GraphBuilder {
1148    nodes: BTreeMap<NodeId, Node>,
1149    edges: Vec<Edge>,
1150    edge_keys: BTreeSet<(NodeId, Label, NodeId)>,
1151    edge_policy: EdgePolicy,
1152}
1153
1154impl GraphBuilder {
1155    pub fn new() -> Self {
1156        Self::default()
1157    }
1158
1159    pub fn edge_policy(mut self, edge_policy: EdgePolicy) -> Self {
1160        self.edge_policy = edge_policy;
1161        self
1162    }
1163
1164    pub fn node<'a>(
1165        &'a mut self,
1166        label: impl Into<Label>,
1167        id: impl Into<NodeId>,
1168    ) -> NodeBuilder<'a> {
1169        NodeBuilder {
1170            builder: self,
1171            label: label.into(),
1172            id: id.into(),
1173            props: Props::new(),
1174        }
1175    }
1176
1177    pub fn edge<'a>(
1178        &'a mut self,
1179        label: impl Into<Label>,
1180        from: impl Into<NodeId>,
1181        to: impl Into<NodeId>,
1182    ) -> EdgeBuilder<'a> {
1183        EdgeBuilder {
1184            builder: self,
1185            id: None,
1186            label: label.into(),
1187            from: from.into(),
1188            to: to.into(),
1189            props: Props::new(),
1190        }
1191    }
1192
1193    /// Adds a node, merging with any existing node that has the same id.
1194    ///
1195    /// If a node with the same id and the same label already exists, the new
1196    /// props are merged in (new values win). If a node with the same id but a
1197    /// different label exists, the new node replaces it entirely (last write
1198    /// wins, matching `GraphStore::put_node` overwrite semantics).
1199    pub fn add_node(&mut self, node: Node) -> NodeId {
1200        let id = node.id.clone();
1201        self.nodes
1202            .entry(id.clone())
1203            .and_modify(|existing| {
1204                if existing.label == node.label {
1205                    existing.props.extend(node.props.clone());
1206                } else {
1207                    *existing = node.clone();
1208                }
1209            })
1210            .or_insert(node);
1211        id
1212    }
1213
1214    /// Adds an edge, reporting whether it was stored or dropped by the
1215    /// builder's [`EdgePolicy`].
1216    pub fn add_edge(&mut self, edge: Edge) -> PutOutcome {
1217        match self.edge_policy {
1218            EdgePolicy::AllowDuplicates => {
1219                self.edges.push(edge);
1220                PutOutcome::Inserted
1221            }
1222            EdgePolicy::DedupeByFromLabelTo => {
1223                let key = (edge.from.clone(), edge.label.clone(), edge.to.clone());
1224                if self.edge_keys.insert(key) {
1225                    self.edges.push(edge);
1226                    PutOutcome::Inserted
1227                } else {
1228                    PutOutcome::Deduped
1229                }
1230            }
1231        }
1232    }
1233
1234    pub fn build(self) -> Graph {
1235        Graph {
1236            nodes: self.nodes.into_values().collect(),
1237            edges: self.edges,
1238        }
1239    }
1240}
1241
1242pub struct NodeBuilder<'a> {
1243    builder: &'a mut GraphBuilder,
1244    label: Label,
1245    id: NodeId,
1246    props: Props,
1247}
1248
1249impl<'a> NodeBuilder<'a> {
1250    pub fn prop(mut self, key: impl Into<String>, value: impl Into<Value>) -> Self {
1251        self.props.insert(key.into(), value.into());
1252        self
1253    }
1254
1255    pub fn props(mut self, props: Props) -> Self {
1256        self.props.extend(props);
1257        self
1258    }
1259
1260    pub fn finish(self) -> NodeId {
1261        let node = Node::new(self.label, self.id, self.props);
1262        self.builder.add_node(node)
1263    }
1264}
1265
1266pub struct EdgeBuilder<'a> {
1267    builder: &'a mut GraphBuilder,
1268    id: Option<EdgeId>,
1269    label: Label,
1270    from: NodeId,
1271    to: NodeId,
1272    props: Props,
1273}
1274
1275impl<'a> EdgeBuilder<'a> {
1276    pub fn id(mut self, id: impl Into<EdgeId>) -> Self {
1277        self.id = Some(id.into());
1278        self
1279    }
1280
1281    pub fn prop(mut self, key: impl Into<String>, value: impl Into<Value>) -> Self {
1282        self.props.insert(key.into(), value.into());
1283        self
1284    }
1285
1286    pub fn props(mut self, props: Props) -> Self {
1287        self.props.extend(props);
1288        self
1289    }
1290
1291    pub fn finish(self) -> PutOutcome {
1292        let mut edge = Edge::new(self.label, self.from, self.to, self.props);
1293        edge.id = self.id;
1294        self.builder.add_edge(edge)
1295    }
1296}
1297
1298#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1299pub struct GraphSchema {
1300    pub nodes: Vec<NodeType>,
1301    pub edges: Vec<EdgeType>,
1302}
1303
1304impl GraphSchema {
1305    pub fn builder() -> GraphSchemaBuilder {
1306        GraphSchemaBuilder::default()
1307    }
1308
1309    pub fn node_type(&self, label: &Label) -> Option<&NodeType> {
1310        self.nodes
1311            .iter()
1312            .find(|node_type| &node_type.label == label)
1313    }
1314
1315    pub fn edge_type(&self, label: &Label) -> Option<&EdgeType> {
1316        self.edges
1317            .iter()
1318            .find(|edge_type| &edge_type.label == label)
1319    }
1320
1321    pub fn validate_graph(&self, graph: &Graph) -> Result<()> {
1322        for node in &graph.nodes {
1323            self.validate_node(node)?;
1324        }
1325        let labels: BTreeMap<&NodeId, &Label> = graph
1326            .nodes
1327            .iter()
1328            .map(|node| (&node.id, &node.label))
1329            .collect();
1330        for edge in &graph.edges {
1331            self.validate_edge_with(edge, |id| labels.get(id).copied())?;
1332        }
1333        self.validate_edge_uniqueness(graph)
1334    }
1335
1336    /// Enforces each edge type's [`EdgeUniqueness`]: at most one edge of the
1337    /// type between a given endpoint pair (unordered for undirected types).
1338    fn validate_edge_uniqueness(&self, graph: &Graph) -> Result<()> {
1339        let mut seen = BTreeSet::new();
1340        for edge in &graph.edges {
1341            let Some(edge_type) = self.edge_type(&edge.label) else {
1342                continue;
1343            };
1344            if edge_type.uniqueness == EdgeUniqueness::None {
1345                continue;
1346            }
1347            let (a, b) = if edge_type.directed || edge.from <= edge.to {
1348                (&edge.from, &edge.to)
1349            } else {
1350                (&edge.to, &edge.from)
1351            };
1352            if !seen.insert((edge.label.clone(), a.clone(), b.clone())) {
1353                return Err(GrustError::Schema(format!(
1354                    "duplicate edge '{}' between '{}' and '{}' violates {:?} uniqueness",
1355                    edge.label.as_str(),
1356                    a.as_str(),
1357                    b.as_str(),
1358                    edge_type.uniqueness
1359                )));
1360            }
1361        }
1362        Ok(())
1363    }
1364
1365    pub fn validate_node(&self, node: &Node) -> Result<()> {
1366        let node_type = self.node_type(&node.label).ok_or_else(|| {
1367            GrustError::Schema(format!("schema has no node type '{}'", node.label.as_str()))
1368        })?;
1369        validate_props(
1370            &node.props,
1371            &node_type.fields,
1372            &format!("node '{}'", node.id.as_str()),
1373        )
1374    }
1375
1376    pub fn validate_edge(&self, edge: &Edge, graph: &Graph) -> Result<()> {
1377        self.validate_edge_with(edge, |id| {
1378            graph
1379                .nodes
1380                .iter()
1381                .find(|node| &node.id == id)
1382                .map(|node| &node.label)
1383        })
1384    }
1385
1386    /// Validates an edge using a label lookup instead of a full `Graph`, so
1387    /// stores can validate against their own node index without cloning.
1388    pub fn validate_edge_with<'a>(
1389        &self,
1390        edge: &Edge,
1391        lookup: impl Fn(&NodeId) -> Option<&'a Label>,
1392    ) -> Result<()> {
1393        let edge_type = self.edge_type(&edge.label).ok_or_else(|| {
1394            GrustError::Schema(format!("schema has no edge type '{}'", edge.label.as_str()))
1395        })?;
1396
1397        let from_label = lookup(&edge.from).ok_or_else(|| {
1398            GrustError::Schema(format!(
1399                "edge '{}' references unknown from node '{}'",
1400                edge.label.as_str(),
1401                edge.from.as_str()
1402            ))
1403        })?;
1404        let to_label = lookup(&edge.to).ok_or_else(|| {
1405            GrustError::Schema(format!(
1406                "edge '{}' references unknown to node '{}'",
1407                edge.label.as_str(),
1408                edge.to.as_str()
1409            ))
1410        })?;
1411
1412        let from_matches =
1413            |label: &Label| edge_type.from.is_empty() || edge_type.from.contains(label);
1414        let to_matches = |label: &Label| edge_type.to.is_empty() || edge_type.to.contains(label);
1415        // Undirected edge types accept their endpoint labels in either
1416        // orientation.
1417        let endpoints_ok = (from_matches(from_label) && to_matches(to_label))
1418            || (!edge_type.directed && from_matches(to_label) && to_matches(from_label));
1419        if !endpoints_ok {
1420            if !from_matches(from_label) {
1421                return Err(GrustError::Schema(format!(
1422                    "edge '{}' cannot start from node label '{}'",
1423                    edge.label.as_str(),
1424                    from_label.as_str()
1425                )));
1426            }
1427            return Err(GrustError::Schema(format!(
1428                "edge '{}' cannot end at node label '{}'",
1429                edge.label.as_str(),
1430                to_label.as_str()
1431            )));
1432        }
1433
1434        validate_props(
1435            &edge.props,
1436            &edge_type.fields,
1437            &format!("edge '{}'", edge.label.as_str()),
1438        )
1439    }
1440
1441    /// Validates an edge's label and props against the schema without
1442    /// checking endpoint nodes, for stores that persist a single edge and
1443    /// cannot cheaply resolve its endpoints.
1444    pub fn validate_edge_props(&self, edge: &Edge) -> Result<()> {
1445        let edge_type = self.edge_type(&edge.label).ok_or_else(|| {
1446            GrustError::Schema(format!("schema has no edge type '{}'", edge.label.as_str()))
1447        })?;
1448        validate_props(
1449            &edge.props,
1450            &edge_type.fields,
1451            &format!("edge '{}'", edge.label.as_str()),
1452        )
1453    }
1454}
1455
1456#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1457pub struct NodeType {
1458    pub label: Label,
1459    pub fields: Vec<Field>,
1460}
1461
1462#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1463pub struct EdgeType {
1464    pub label: Label,
1465    pub from: Vec<Label>,
1466    pub to: Vec<Label>,
1467    pub fields: Vec<Field>,
1468    pub directed: bool,
1469    pub uniqueness: EdgeUniqueness,
1470}
1471
1472#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1473pub struct Field {
1474    pub name: String,
1475    pub ty: FieldType,
1476    pub required: bool,
1477}
1478
1479#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1480pub enum FieldType {
1481    String,
1482    Int,
1483    Float,
1484    Bool,
1485    DateTime,
1486    StringArray,
1487    IntArray,
1488    FloatArray,
1489    Json,
1490}
1491
1492/// How many edges of one type may exist between a pair of nodes.
1493///
1494/// `validate_graph` enforces `FromTo` and `FromLabelTo` identically — at most
1495/// one edge of the type between a given endpoint pair (unordered when the
1496/// type is undirected). The distinction is a storage-key hint for backends
1497/// that keep all edge labels in one table: `FromLabelTo` keys include the
1498/// label, `FromTo` keys do not.
1499#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1500pub enum EdgeUniqueness {
1501    None,
1502    FromTo,
1503    FromLabelTo,
1504}
1505
1506#[derive(Clone, Debug, Default)]
1507pub struct GraphSchemaBuilder {
1508    nodes: Vec<NodeType>,
1509    edges: Vec<EdgeType>,
1510}
1511
1512impl GraphSchemaBuilder {
1513    pub fn node(mut self, label: impl Into<Label>, fields: impl Into<Vec<Field>>) -> Self {
1514        self.nodes.push(NodeType {
1515            label: label.into(),
1516            fields: fields.into(),
1517        });
1518        self
1519    }
1520
1521    pub fn edge(
1522        mut self,
1523        label: impl Into<Label>,
1524        from: impl Into<Vec<Label>>,
1525        to: impl Into<Vec<Label>>,
1526        fields: impl Into<Vec<Field>>,
1527    ) -> Self {
1528        self.edges.push(EdgeType {
1529            label: label.into(),
1530            from: from.into(),
1531            to: to.into(),
1532            fields: fields.into(),
1533            directed: true,
1534            uniqueness: EdgeUniqueness::FromLabelTo,
1535        });
1536        self
1537    }
1538
1539    pub fn edge_type(mut self, edge_type: EdgeType) -> Self {
1540        self.edges.push(edge_type);
1541        self
1542    }
1543
1544    pub fn build(self) -> GraphSchema {
1545        GraphSchema {
1546            nodes: self.nodes,
1547            edges: self.edges,
1548        }
1549    }
1550}
1551
1552impl Field {
1553    pub fn required(name: impl Into<String>, ty: FieldType) -> Self {
1554        Self {
1555            name: name.into(),
1556            ty,
1557            required: true,
1558        }
1559    }
1560
1561    pub fn optional(name: impl Into<String>, ty: FieldType) -> Self {
1562        Self {
1563            name: name.into(),
1564            ty,
1565            required: false,
1566        }
1567    }
1568}
1569
1570fn validate_props(props: &Props, fields: &[Field], context: &str) -> Result<()> {
1571    for field in fields {
1572        match props.get(&field.name) {
1573            Some(value) => validate_field_value(value, &field.ty, context, &field.name)?,
1574            None if field.required => {
1575                return Err(GrustError::Schema(format!(
1576                    "{context} missing required field '{}'",
1577                    field.name
1578                )));
1579            }
1580            None => {}
1581        }
1582    }
1583    Ok(())
1584}
1585
1586fn validate_field_value(
1587    value: &Value,
1588    ty: &FieldType,
1589    context: &str,
1590    field_name: &str,
1591) -> Result<()> {
1592    let matches = match (value, ty) {
1593        (Value::String(_), FieldType::String)
1594        | (Value::Int(_), FieldType::Int)
1595        | (Value::Float(_), FieldType::Float)
1596        | (Value::Bool(_), FieldType::Bool)
1597        | (Value::DateTime(_), FieldType::DateTime)
1598        | (Value::StringArray(_), FieldType::StringArray)
1599        | (Value::IntArray(_), FieldType::IntArray)
1600        | (Value::FloatArray(_), FieldType::FloatArray)
1601        | (_, FieldType::Json) => true,
1602        // Plain strings remain valid date-times for backward compatibility,
1603        // but must still parse as RFC 3339.
1604        (Value::String(value), FieldType::DateTime) => is_rfc3339_datetime(value),
1605        _ => false,
1606    };
1607    if matches {
1608        Ok(())
1609    } else {
1610        Err(GrustError::Schema(format!(
1611            "{context} field '{field_name}' expected {ty:?}, got {value:?}"
1612        )))
1613    }
1614}
1615
1616#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1617pub struct Traversal {
1618    pub start: Start,
1619    pub steps: Vec<Step>,
1620    pub limit: Option<u32>,
1621}
1622
1623impl Traversal {
1624    pub fn from_node(id: impl Into<NodeId>) -> Self {
1625        Self {
1626            start: Start::Node(id.into()),
1627            steps: Vec::new(),
1628            limit: None,
1629        }
1630    }
1631
1632    pub fn out(mut self, edge: impl Into<Label>) -> Self {
1633        self.steps.push(Step {
1634            direction: Direction::Out,
1635            edge: Some(edge.into()),
1636            node: None,
1637        });
1638        self
1639    }
1640
1641    pub fn in_(mut self, edge: impl Into<Label>) -> Self {
1642        self.steps.push(Step {
1643            direction: Direction::In,
1644            edge: Some(edge.into()),
1645            node: None,
1646        });
1647        self
1648    }
1649
1650    pub fn both(mut self, edge: impl Into<Label>) -> Self {
1651        self.steps.push(Step {
1652            direction: Direction::Both,
1653            edge: Some(edge.into()),
1654            node: None,
1655        });
1656        self
1657    }
1658
1659    /// Constrains the target node label of the most recent step.
1660    ///
1661    /// # Panics
1662    ///
1663    /// Panics if called before any step has been added with `out`, `in_`, or
1664    /// `both`, since there is no step for the label to apply to.
1665    pub fn to(mut self, node: impl Into<Label>) -> Self {
1666        let step = self
1667            .steps
1668            .last_mut()
1669            .expect("Traversal::to() must follow out(), in_(), or both()");
1670        step.node = Some(node.into());
1671        self
1672    }
1673
1674    pub fn limit(mut self, limit: u32) -> Self {
1675        self.limit = Some(limit);
1676        self
1677    }
1678}
1679
1680#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1681pub enum Start {
1682    Node(NodeId),
1683    NodesByLabel(Label),
1684    NodesByProperty {
1685        label: Label,
1686        key: String,
1687        value: Value,
1688    },
1689}
1690
1691#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1692pub struct Step {
1693    pub direction: Direction,
1694    pub edge: Option<Label>,
1695    pub node: Option<Label>,
1696}
1697
1698#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1699pub enum Direction {
1700    Out,
1701    In,
1702    Both,
1703}
1704
1705#[derive(Clone, Debug, Default, PartialEq)]
1706pub struct EdgeQuery {
1707    pub from: Option<NodeId>,
1708    pub to: Option<NodeId>,
1709    pub label: Option<Label>,
1710}
1711
1712/// Outcome of writing a single node or edge.
1713#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
1714pub enum PutOutcome {
1715    /// The element did not exist before and was created.
1716    Inserted,
1717    /// An element with the same identity existed and was overwritten or
1718    /// merged.
1719    Updated,
1720    /// The element was written by an upsert and the backend cannot tell
1721    /// whether it was an insert or an update.
1722    Upserted,
1723    /// The element was dropped by a dedupe policy and nothing was written.
1724    Deduped,
1725}
1726
1727impl PutOutcome {
1728    /// True unless the element was deduped away.
1729    pub fn written(self) -> bool {
1730        !matches!(self, Self::Deduped)
1731    }
1732}
1733
1734/// Counts of nodes and edges written by a bulk load. Each count reflects an
1735/// upsert applied to the backend, not distinct newly created elements.
1736#[derive(Clone, Debug, Default, Eq, PartialEq)]
1737pub struct LoadReport {
1738    pub nodes: usize,
1739    pub edges: usize,
1740}
1741
1742#[async_trait]
1743pub trait GraphStore: Send + Sync {
1744    async fn apply_schema(&self, _schema: &GraphSchema) -> Result<()> {
1745        Ok(())
1746    }
1747
1748    async fn put_node(&self, node: &Node) -> Result<PutOutcome>;
1749    async fn put_edge(&self, edge: &Edge) -> Result<PutOutcome>;
1750
1751    async fn put_graph(&self, graph: &Graph) -> Result<LoadReport> {
1752        let mut report = LoadReport::default();
1753        for node in &graph.nodes {
1754            if self.put_node(node).await?.written() {
1755                report.nodes += 1;
1756            }
1757        }
1758        for edge in &graph.edges {
1759            if self.put_edge(edge).await?.written() {
1760                report.edges += 1;
1761            }
1762        }
1763        Ok(report)
1764    }
1765
1766    async fn put_typed_graph(&self, schema: &GraphSchema, graph: &Graph) -> Result<LoadReport> {
1767        schema.validate_graph(graph)?;
1768        self.apply_schema(schema).await?;
1769        self.put_graph(graph).await
1770    }
1771
1772    async fn get_node(&self, id: &NodeId) -> Result<Option<Node>>;
1773
1774    /// Reads multiple nodes by ID.
1775    ///
1776    /// The default implementation preserves the input order and calls
1777    /// [`GraphStore::get_node`] once per ID. Backends with a native batch-read
1778    /// path should override this to avoid per-node round trips during traversal
1779    /// and other fan-out reads.
1780    async fn get_nodes(&self, ids: &[NodeId]) -> Result<Vec<Node>> {
1781        let mut nodes = Vec::new();
1782        for id in ids {
1783            if let Some(node) = self.get_node(id).await? {
1784                nodes.push(node);
1785            }
1786        }
1787        Ok(nodes)
1788    }
1789
1790    async fn get_edges(&self, query: EdgeQuery) -> Result<Vec<Edge>>;
1791    async fn traverse(&self, traversal: Traversal) -> Result<Vec<Node>>;
1792}
1793
1794#[async_trait]
1795pub trait GraphAdminStore: GraphStore {
1796    async fn bootstrap(&self) -> Result<()> {
1797        Ok(())
1798    }
1799
1800    async fn clear(&self) -> Result<()>;
1801}
1802
1803/// A single incremental change to a graph, for delta-oriented pipelines.
1804#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1805pub enum GraphMutation {
1806    UpsertNode(Node),
1807    DeleteNode(NodeId),
1808    UpsertEdge(Edge),
1809    DeleteEdge {
1810        id: Option<EdgeId>,
1811        from: NodeId,
1812        label: Label,
1813        to: NodeId,
1814    },
1815}
1816
1817/// Incremental mutation support for stores that can delete elements.
1818///
1819/// Deletes are idempotent: removing an element that does not exist is not an
1820/// error.
1821#[async_trait]
1822pub trait GraphMutationStore: GraphStore {
1823    /// Deletes a node and all edges incident to it.
1824    async fn delete_node(&self, id: &NodeId) -> Result<()>;
1825
1826    /// Deletes the edge(s) matching `(from, label, to)`.
1827    async fn delete_edge(&self, from: &NodeId, label: &Label, to: &NodeId) -> Result<()>;
1828
1829    /// Applies mutations in order, stopping at the first error.
1830    async fn apply_mutations(&self, mutations: &[GraphMutation]) -> Result<()> {
1831        for mutation in mutations {
1832            match mutation {
1833                GraphMutation::UpsertNode(node) => {
1834                    self.put_node(node).await?;
1835                }
1836                GraphMutation::DeleteNode(id) => self.delete_node(id).await?,
1837                GraphMutation::UpsertEdge(edge) => {
1838                    self.put_edge(edge).await?;
1839                }
1840                GraphMutation::DeleteEdge {
1841                    from, label, to, ..
1842                } => self.delete_edge(from, label, to).await?,
1843            }
1844        }
1845        Ok(())
1846    }
1847}
1848
1849pub mod prelude {
1850    pub use crate::{
1851        Direction, Edge, EdgeId, EdgePolicy, EdgeQuery, EdgeType, EdgeUniqueness, Field, FieldType,
1852        Graph, GraphAdminStore, GraphBuilder, GraphMutation, GraphMutationStore, GraphSchema,
1853        GraphSchemaBuilder, GraphStore, GrustError, Label, LoadReport, Node, NodeId, NodeType,
1854        Props, PutOutcome, Result, Start, Step, Traversal, Value, edge_key, relationship_type,
1855        schema_identifier,
1856    };
1857
1858    #[cfg(feature = "typed-garde")]
1859    pub use crate::typed::{TypedEdge, TypedGraphBuilder, TypedNode, garde, props_from_serialize};
1860
1861    #[cfg(feature = "typed-zod-rs")]
1862    pub use crate::typed::{parse_typed_json, parse_typed_json_with, zod_rs};
1863}
1864
1865#[cfg(test)]
1866mod tests;