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
24#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)]
25pub struct NodeId(String);
26
27impl NodeId {
28    pub fn new(value: impl Into<String>) -> Self {
29        Self(value.into())
30    }
31
32    pub fn as_str(&self) -> &str {
33        &self.0
34    }
35
36    pub fn into_string(self) -> String {
37        self.0
38    }
39}
40
41impl fmt::Display for NodeId {
42    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43        f.write_str(&self.0)
44    }
45}
46
47impl From<String> for NodeId {
48    fn from(value: String) -> Self {
49        Self::new(value)
50    }
51}
52
53impl From<&str> for NodeId {
54    fn from(value: &str) -> Self {
55        Self::new(value)
56    }
57}
58
59impl From<&String> for NodeId {
60    fn from(value: &String) -> Self {
61        Self::new(value.clone())
62    }
63}
64
65impl From<&NodeId> for NodeId {
66    fn from(value: &NodeId) -> Self {
67        value.clone()
68    }
69}
70
71#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)]
72pub struct EdgeId(String);
73
74impl EdgeId {
75    pub fn new(value: impl Into<String>) -> Self {
76        Self(value.into())
77    }
78
79    pub fn as_str(&self) -> &str {
80        &self.0
81    }
82
83    pub fn into_string(self) -> String {
84        self.0
85    }
86}
87
88impl fmt::Display for EdgeId {
89    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
90        f.write_str(&self.0)
91    }
92}
93
94impl From<String> for EdgeId {
95    fn from(value: String) -> Self {
96        Self::new(value)
97    }
98}
99
100impl From<&str> for EdgeId {
101    fn from(value: &str) -> Self {
102        Self::new(value)
103    }
104}
105
106impl From<&String> for EdgeId {
107    fn from(value: &String) -> Self {
108        Self::new(value.clone())
109    }
110}
111
112impl From<&EdgeId> for EdgeId {
113    fn from(value: &EdgeId) -> Self {
114        value.clone()
115    }
116}
117
118#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)]
119pub struct Label(String);
120
121impl Label {
122    pub fn new(value: impl Into<String>) -> Self {
123        Self(value.into())
124    }
125
126    pub fn as_str(&self) -> &str {
127        &self.0
128    }
129
130    pub fn into_string(self) -> String {
131        self.0
132    }
133}
134
135impl fmt::Display for Label {
136    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
137        f.write_str(&self.0)
138    }
139}
140
141impl From<String> for Label {
142    fn from(value: String) -> Self {
143        Self::new(value)
144    }
145}
146
147impl From<&str> for Label {
148    fn from(value: &str) -> Self {
149        Self::new(value)
150    }
151}
152
153impl From<&String> for Label {
154    fn from(value: &String) -> Self {
155        Self::new(value.clone())
156    }
157}
158
159impl From<&Label> for Label {
160    fn from(value: &Label) -> Self {
161        value.clone()
162    }
163}
164
165#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
166#[serde(tag = "type", content = "value", rename_all = "snake_case")]
167pub enum Value {
168    Null,
169    Bool(bool),
170    Int(i64),
171    Float(f64),
172    String(String),
173    StringArray(Vec<String>),
174    Json(serde_json::Value),
175}
176
177impl Value {
178    pub fn as_str(&self) -> Option<&str> {
179        match self {
180            Self::String(value) => Some(value),
181            _ => None,
182        }
183    }
184
185    pub fn as_string_array(&self) -> Option<&[String]> {
186        match self {
187            Self::StringArray(values) => Some(values),
188            _ => None,
189        }
190    }
191}
192
193impl From<String> for Value {
194    fn from(value: String) -> Self {
195        Self::String(value)
196    }
197}
198
199impl From<&str> for Value {
200    fn from(value: &str) -> Self {
201        Self::String(value.to_string())
202    }
203}
204
205impl From<&String> for Value {
206    fn from(value: &String) -> Self {
207        Self::String(value.clone())
208    }
209}
210
211impl From<Vec<String>> for Value {
212    fn from(value: Vec<String>) -> Self {
213        Self::StringArray(value)
214    }
215}
216
217impl From<bool> for Value {
218    fn from(value: bool) -> Self {
219        Self::Bool(value)
220    }
221}
222
223impl From<i64> for Value {
224    fn from(value: i64) -> Self {
225        Self::Int(value)
226    }
227}
228
229impl From<i32> for Value {
230    fn from(value: i32) -> Self {
231        Self::Int(i64::from(value))
232    }
233}
234
235impl From<usize> for Value {
236    fn from(value: usize) -> Self {
237        Self::Int(value as i64)
238    }
239}
240
241impl From<f64> for Value {
242    fn from(value: f64) -> Self {
243        Self::Float(value)
244    }
245}
246
247impl From<serde_json::Value> for Value {
248    fn from(value: serde_json::Value) -> Self {
249        match value {
250            serde_json::Value::Null => Self::Null,
251            serde_json::Value::Bool(value) => Self::Bool(value),
252            serde_json::Value::Number(value) => {
253                if let Some(value) = value.as_i64() {
254                    Self::Int(value)
255                } else if let Some(value) = value.as_f64() {
256                    Self::Float(value)
257                } else {
258                    Self::Json(serde_json::Value::Number(value))
259                }
260            }
261            serde_json::Value::String(value) => Self::String(value),
262            serde_json::Value::Array(values) => {
263                let strings = values
264                    .iter()
265                    .filter_map(|value| value.as_str().map(ToString::to_string))
266                    .collect::<Vec<_>>();
267                if strings.len() == values.len() {
268                    Self::StringArray(strings)
269                } else {
270                    Self::Json(serde_json::Value::Array(values))
271                }
272            }
273            serde_json::Value::Object(value) => Self::Json(serde_json::Value::Object(value)),
274        }
275    }
276}
277
278#[cfg(feature = "typed-garde")]
279pub mod typed {
280    use serde::Serialize;
281    #[cfg(feature = "typed-zod-rs")]
282    use serde::de::DeserializeOwned;
283
284    use crate::{
285        Edge, EdgeId, Graph, GraphBuilder, GrustError, Node, NodeId, Props, Result, Value,
286    };
287
288    pub use garde;
289    #[cfg(feature = "typed-zod-rs")]
290    pub use zod_rs;
291
292    pub trait TypedNode: garde::Validate + Serialize {
293        const LABEL: &'static str;
294
295        fn node_id(&self) -> NodeId;
296
297        fn node_props(&self) -> Result<Props> {
298            props_from_serialize(self)
299        }
300    }
301
302    pub trait TypedEdge: garde::Validate + Serialize {
303        const LABEL: &'static str;
304
305        fn from_node_id(&self) -> NodeId;
306
307        fn to_node_id(&self) -> NodeId;
308
309        fn edge_id(&self) -> Option<EdgeId> {
310            None
311        }
312
313        fn edge_props(&self) -> Result<Props> {
314            props_from_serialize(self)
315        }
316    }
317
318    #[derive(Clone, Debug, Default)]
319    pub struct TypedGraphBuilder {
320        builder: GraphBuilder,
321    }
322
323    impl TypedGraphBuilder {
324        pub fn new() -> Self {
325            Self::default()
326        }
327
328        pub fn from_builder(builder: GraphBuilder) -> Self {
329            Self { builder }
330        }
331
332        pub fn from_graph(graph: Graph) -> Self {
333            let mut builder = GraphBuilder::new();
334            for node in graph.nodes {
335                builder.add_node(node);
336            }
337            for edge in graph.edges {
338                builder.add_edge(edge);
339            }
340            Self { builder }
341        }
342
343        pub fn add_raw_node(&mut self, node: Node) -> NodeId {
344            self.builder.add_node(node)
345        }
346
347        pub fn add_raw_edge(&mut self, edge: Edge) -> Option<EdgeId> {
348            self.builder.add_edge(edge)
349        }
350
351        pub fn add_node<T>(&mut self, node: &T) -> Result<NodeId>
352        where
353            T: TypedNode,
354            T::Context: Default,
355        {
356            node.validate()
357                .map_err(|err| validation_error(T::LABEL, err))?;
358            self.add_validated_node(node)
359        }
360
361        pub fn add_node_with<T>(&mut self, node: &T, ctx: &T::Context) -> Result<NodeId>
362        where
363            T: TypedNode,
364        {
365            node.validate_with(ctx)
366                .map_err(|err| validation_error(T::LABEL, err))?;
367            self.add_validated_node(node)
368        }
369
370        pub fn add_edge<T>(&mut self, edge: &T) -> Result<Option<EdgeId>>
371        where
372            T: TypedEdge,
373            T::Context: Default,
374        {
375            edge.validate()
376                .map_err(|err| validation_error(T::LABEL, err))?;
377            self.add_validated_edge(edge)
378        }
379
380        pub fn add_edge_with<T>(&mut self, edge: &T, ctx: &T::Context) -> Result<Option<EdgeId>>
381        where
382            T: TypedEdge,
383        {
384            edge.validate_with(ctx)
385                .map_err(|err| validation_error(T::LABEL, err))?;
386            self.add_validated_edge(edge)
387        }
388
389        #[cfg(feature = "typed-zod-rs")]
390        pub fn add_node_from_json<T, S>(
391            &mut self,
392            schema: &S,
393            value: &serde_json::Value,
394        ) -> Result<NodeId>
395        where
396            T: TypedNode + DeserializeOwned,
397            T::Context: Default,
398            S: zod_rs::Schema<serde_json::Value>,
399        {
400            let node = parse_typed_json::<T, S>(schema, value)?;
401            self.add_validated_node(&node)
402        }
403
404        #[cfg(feature = "typed-zod-rs")]
405        pub fn add_node_from_json_with<T, S>(
406            &mut self,
407            schema: &S,
408            value: &serde_json::Value,
409            ctx: &T::Context,
410        ) -> Result<NodeId>
411        where
412            T: TypedNode + DeserializeOwned,
413            S: zod_rs::Schema<serde_json::Value>,
414        {
415            let node = parse_typed_json_with::<T, S>(schema, value, ctx)?;
416            self.add_validated_node(&node)
417        }
418
419        #[cfg(feature = "typed-zod-rs")]
420        pub fn add_edge_from_json<T, S>(
421            &mut self,
422            schema: &S,
423            value: &serde_json::Value,
424        ) -> Result<Option<EdgeId>>
425        where
426            T: TypedEdge + DeserializeOwned,
427            T::Context: Default,
428            S: zod_rs::Schema<serde_json::Value>,
429        {
430            let edge = parse_typed_json::<T, S>(schema, value)?;
431            self.add_validated_edge(&edge)
432        }
433
434        #[cfg(feature = "typed-zod-rs")]
435        pub fn add_edge_from_json_with<T, S>(
436            &mut self,
437            schema: &S,
438            value: &serde_json::Value,
439            ctx: &T::Context,
440        ) -> Result<Option<EdgeId>>
441        where
442            T: TypedEdge + DeserializeOwned,
443            S: zod_rs::Schema<serde_json::Value>,
444        {
445            let edge = parse_typed_json_with::<T, S>(schema, value, ctx)?;
446            self.add_validated_edge(&edge)
447        }
448
449        pub fn build(self) -> Graph {
450            self.builder.build()
451        }
452
453        pub fn into_builder(self) -> GraphBuilder {
454            self.builder
455        }
456
457        fn add_validated_node<T>(&mut self, node: &T) -> Result<NodeId>
458        where
459            T: TypedNode,
460        {
461            let node_id = node.node_id();
462            let mut props = node.node_props()?;
463            props.insert("id".to_string(), Value::from(node_id.as_str()));
464            let graph_node = Node::new(T::LABEL, node_id, props);
465            Ok(self.builder.add_node(graph_node))
466        }
467
468        fn add_validated_edge<T>(&mut self, edge: &T) -> Result<Option<EdgeId>>
469        where
470            T: TypedEdge,
471        {
472            let mut graph_edge = Edge::new(
473                T::LABEL,
474                edge.from_node_id(),
475                edge.to_node_id(),
476                edge.edge_props()?,
477            );
478            graph_edge.id = edge.edge_id();
479            Ok(self.builder.add_edge(graph_edge))
480        }
481    }
482
483    pub fn props_from_serialize<T>(value: &T) -> Result<Props>
484    where
485        T: Serialize + ?Sized,
486    {
487        let serialized = serde_json::to_value(value)
488            .map_err(|err| GrustError::Serialization(format!("typed props error: {err}")))?;
489        let serde_json::Value::Object(fields) = serialized else {
490            return Err(GrustError::Schema(
491                "typed graph values must serialize as JSON objects".to_string(),
492            ));
493        };
494
495        Ok(fields
496            .into_iter()
497            .map(|(key, value)| (key, Value::from(value)))
498            .collect())
499    }
500
501    #[cfg(feature = "typed-zod-rs")]
502    pub fn parse_typed_json<T, S>(schema: &S, value: &serde_json::Value) -> Result<T>
503    where
504        T: DeserializeOwned + garde::Validate,
505        T::Context: Default,
506        S: zod_rs::Schema<serde_json::Value>,
507    {
508        let ctx = T::Context::default();
509        parse_typed_json_with(schema, value, &ctx)
510    }
511
512    #[cfg(feature = "typed-zod-rs")]
513    pub fn parse_typed_json_with<T, S>(
514        schema: &S,
515        value: &serde_json::Value,
516        ctx: &T::Context,
517    ) -> Result<T>
518    where
519        T: DeserializeOwned + garde::Validate,
520        S: zod_rs::Schema<serde_json::Value>,
521    {
522        schema
523            .safe_parse(value)
524            .map_err(|err| GrustError::Schema(format!("zod-rs validation failed: {err}")))?;
525        let typed: T = serde_json::from_value(value.clone())
526            .map_err(|err| GrustError::Serialization(format!("typed JSON decode error: {err}")))?;
527        typed
528            .validate_with(ctx)
529            .map_err(|err| GrustError::Schema(format!("typed validation failed: {err}")))?;
530        Ok(typed)
531    }
532
533    fn validation_error(label: &str, err: garde::Report) -> GrustError {
534        GrustError::Schema(format!("{label} validation failed: {err}"))
535    }
536}
537
538#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
539pub struct Node {
540    pub id: NodeId,
541    pub label: Label,
542    pub props: Props,
543}
544
545impl Node {
546    pub fn new(label: impl Into<Label>, id: impl Into<NodeId>, props: impl Into<Props>) -> Self {
547        let id = id.into();
548        let mut props = props.into();
549        props
550            .entry("id".to_string())
551            .or_insert_with(|| Value::from(id.as_str()));
552        Self {
553            id,
554            label: label.into(),
555            props,
556        }
557    }
558}
559
560#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
561pub struct Edge {
562    pub id: Option<EdgeId>,
563    pub from: NodeId,
564    pub to: NodeId,
565    pub label: Label,
566    pub props: Props,
567}
568
569impl Edge {
570    pub fn new(
571        label: impl Into<Label>,
572        from: impl Into<NodeId>,
573        to: impl Into<NodeId>,
574        props: impl Into<Props>,
575    ) -> Self {
576        Self {
577            id: None,
578            from: from.into(),
579            to: to.into(),
580            label: label.into(),
581            props: props.into(),
582        }
583    }
584
585    pub fn with_id(mut self, id: impl Into<EdgeId>) -> Self {
586        self.id = Some(id.into());
587        self
588    }
589}
590
591#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
592pub struct Graph {
593    pub nodes: Vec<Node>,
594    pub edges: Vec<Edge>,
595}
596
597impl Graph {
598    pub fn new(nodes: Vec<Node>, edges: Vec<Edge>) -> Self {
599        Self { nodes, edges }
600    }
601
602    pub fn from_yaml(yaml: &str) -> Result<Self> {
603        yaml::graph_from_yaml(yaml)
604    }
605
606    pub fn to_yaml(&self) -> Result<String> {
607        yaml::graph_to_yaml(self)
608    }
609
610    pub fn from_json(json: &str) -> Result<Self> {
611        json::graph_from_json(json)
612    }
613
614    pub fn to_json(&self) -> Result<String> {
615        json::graph_to_json(self)
616    }
617
618    pub fn from_xml(xml: &str) -> Result<Self> {
619        xml::graph_from_xml(xml)
620    }
621
622    pub fn to_xml(&self) -> Result<String> {
623        xml::graph_to_xml(self)
624    }
625
626    pub fn builder() -> GraphBuilder {
627        GraphBuilder::new()
628    }
629}
630
631mod graph_doc {
632    use std::collections::{BTreeMap, BTreeSet};
633
634    use serde::{Deserialize, Serialize};
635
636    use crate::{Edge, EdgeId, Graph, GrustError, Label, Node, NodeId, Props, Value};
637
638    #[derive(Debug, Serialize, Deserialize)]
639    pub(super) struct GraphDoc {
640        #[serde(default)]
641        pub(super) nodes: Vec<NodeDoc>,
642        #[serde(default)]
643        pub(super) edges: Vec<EdgeDoc>,
644    }
645
646    #[derive(Debug, Serialize, Deserialize)]
647    pub(super) struct NodeDoc {
648        pub(super) id: NodeId,
649        pub(super) label: Label,
650        #[serde(default, deserialize_with = "deserialize_props")]
651        pub(super) props: Props,
652    }
653
654    #[derive(Debug, Serialize, Deserialize)]
655    pub(super) struct NodeDocOut {
656        pub(super) id: NodeId,
657        pub(super) label: Label,
658        #[serde(default)]
659        pub(super) props: Props,
660    }
661
662    #[derive(Debug, Serialize, Deserialize)]
663    pub(super) struct EdgeDoc {
664        #[serde(default)]
665        pub(super) id: Option<EdgeId>,
666        pub(super) label: Label,
667        pub(super) from: NodeId,
668        pub(super) to: NodeId,
669        #[serde(default, deserialize_with = "deserialize_props")]
670        pub(super) props: Props,
671    }
672
673    #[derive(Debug, Serialize, Deserialize)]
674    pub(super) struct EdgeDocOut {
675        #[serde(default)]
676        pub(super) id: Option<EdgeId>,
677        pub(super) label: Label,
678        pub(super) from: NodeId,
679        pub(super) to: NodeId,
680        #[serde(default)]
681        pub(super) props: Props,
682    }
683
684    pub(super) fn graph_from_doc(doc: GraphDoc) -> super::Result<Graph> {
685        let mut ids = BTreeSet::new();
686        for node in &doc.nodes {
687            if !ids.insert(node.id.clone()) {
688                return Err(GrustError::Schema(format!(
689                    "duplicate node id '{}'",
690                    node.id
691                )));
692            }
693        }
694
695        let mut edges = Vec::with_capacity(doc.edges.len());
696        for edge in doc.edges {
697            if !ids.contains(&edge.from) {
698                return Err(GrustError::Schema(format!(
699                    "edge '{}' references unknown from node '{}'",
700                    edge.label, edge.from
701                )));
702            }
703            if !ids.contains(&edge.to) {
704                return Err(GrustError::Schema(format!(
705                    "edge '{}' references unknown to node '{}'",
706                    edge.label, edge.to
707                )));
708            }
709
710            let mut graph_edge = Edge::new(edge.label, edge.from, edge.to, edge.props);
711            graph_edge.id = edge.id;
712            edges.push(graph_edge);
713        }
714
715        let nodes = doc
716            .nodes
717            .into_iter()
718            .map(|node| Node::new(node.label, node.id, node.props))
719            .collect();
720
721        Ok(Graph::new(nodes, edges))
722    }
723
724    pub(super) fn graph_to_doc(graph: &Graph) -> GraphDocOut {
725        GraphDocOut {
726            nodes: graph
727                .nodes
728                .iter()
729                .map(|node| NodeDocOut {
730                    id: node.id.clone(),
731                    label: node.label.clone(),
732                    props: without_generated_id(&node.props, &node.id),
733                })
734                .collect(),
735            edges: graph
736                .edges
737                .iter()
738                .map(|edge| EdgeDocOut {
739                    id: edge.id.clone(),
740                    label: edge.label.clone(),
741                    from: edge.from.clone(),
742                    to: edge.to.clone(),
743                    props: edge.props.clone(),
744                })
745                .collect(),
746        }
747    }
748
749    fn without_generated_id(props: &Props, id: &NodeId) -> Props {
750        let mut props = props.clone();
751        if props.get("id") == Some(&Value::from(id.as_str())) {
752            props.remove("id");
753        }
754        props
755    }
756
757    fn deserialize_props<'de, D>(deserializer: D) -> std::result::Result<Props, D::Error>
758    where
759        D: serde::Deserializer<'de>,
760    {
761        let raw = BTreeMap::<String, serde_json::Value>::deserialize(deserializer)?;
762        raw.into_iter()
763            .map(|(key, value)| {
764                value_from_json(value)
765                    .map(|value| (key, value))
766                    .map_err(serde::de::Error::custom)
767            })
768            .collect()
769    }
770
771    fn value_from_json(value: serde_json::Value) -> std::result::Result<Value, String> {
772        if let serde_json::Value::Object(mapping) = &value
773            && mapping.contains_key("type")
774            && mapping.contains_key("value")
775        {
776            return serde_json::from_value(value)
777                .map_err(|err| format!("invalid tagged Grust value: {err}"));
778        }
779
780        Ok(Value::from(value))
781    }
782
783    #[derive(Debug, Serialize, Deserialize)]
784    pub(super) struct GraphDocOut {
785        pub(super) nodes: Vec<NodeDocOut>,
786        pub(super) edges: Vec<EdgeDocOut>,
787    }
788}
789
790mod yaml {
791    use crate::{Graph, GrustError};
792
793    pub(super) fn graph_from_yaml(yaml: &str) -> super::Result<Graph> {
794        let doc: super::graph_doc::GraphDoc = serde_yaml::from_str(yaml)
795            .map_err(|err| GrustError::Serialization(format!("YAML parse error: {err}")))?;
796        super::graph_doc::graph_from_doc(doc)
797    }
798
799    pub(super) fn graph_to_yaml(graph: &Graph) -> super::Result<String> {
800        serde_yaml::to_string(&super::graph_doc::graph_to_doc(graph))
801            .map_err(|err| GrustError::Serialization(format!("YAML serialization error: {err}")))
802    }
803}
804
805mod json {
806    use crate::{Graph, GrustError};
807
808    pub(super) fn graph_from_json(json: &str) -> super::Result<Graph> {
809        let doc: super::graph_doc::GraphDoc = serde_json::from_str(json)
810            .map_err(|err| GrustError::Serialization(format!("JSON parse error: {err}")))?;
811        super::graph_doc::graph_from_doc(doc)
812    }
813
814    pub(super) fn graph_to_json(graph: &Graph) -> super::Result<String> {
815        serde_json::to_string_pretty(&super::graph_doc::graph_to_doc(graph))
816            .map_err(|err| GrustError::Serialization(format!("JSON serialization error: {err}")))
817    }
818}
819
820mod xml {
821    use serde::{Deserialize, Serialize};
822
823    use crate::{Edge, EdgeId, Graph, GrustError, Label, Node, NodeId, Props, Value};
824
825    #[derive(Debug, Serialize, Deserialize)]
826    #[serde(rename = "graph")]
827    struct GraphXml {
828        #[serde(default)]
829        nodes: NodesXml,
830        #[serde(default)]
831        edges: EdgesXml,
832    }
833
834    #[derive(Debug, Default, Serialize, Deserialize)]
835    struct NodesXml {
836        #[serde(rename = "node", default)]
837        items: Vec<NodeXml>,
838    }
839
840    #[derive(Debug, Default, Serialize, Deserialize)]
841    struct EdgesXml {
842        #[serde(rename = "edge", default)]
843        items: Vec<EdgeXml>,
844    }
845
846    #[derive(Debug, Serialize, Deserialize)]
847    struct NodeXml {
848        id: NodeId,
849        label: Label,
850        #[serde(default)]
851        props: PropsXml,
852    }
853
854    #[derive(Debug, Serialize, Deserialize)]
855    struct EdgeXml {
856        #[serde(default, skip_serializing_if = "Option::is_none")]
857        id: Option<EdgeId>,
858        label: Label,
859        from: NodeId,
860        to: NodeId,
861        #[serde(default)]
862        props: PropsXml,
863    }
864
865    #[derive(Debug, Default, Serialize, Deserialize)]
866    struct PropsXml {
867        #[serde(rename = "prop", default)]
868        items: Vec<PropXml>,
869    }
870
871    #[derive(Debug, Serialize, Deserialize)]
872    struct PropXml {
873        key: String,
874        value: Value,
875    }
876
877    pub(super) fn graph_from_xml(xml: &str) -> super::Result<Graph> {
878        let doc: GraphXml = quick_xml::de::from_str(xml)
879            .map_err(|err| GrustError::Serialization(format!("XML parse error: {err}")))?;
880        super::graph_doc::graph_from_doc(doc.into())
881    }
882
883    pub(super) fn graph_to_xml(graph: &Graph) -> super::Result<String> {
884        quick_xml::se::to_string(&GraphXml::from(graph))
885            .map_err(|err| GrustError::Serialization(format!("XML serialization error: {err}")))
886    }
887
888    impl From<GraphXml> for super::graph_doc::GraphDoc {
889        fn from(value: GraphXml) -> Self {
890            Self {
891                nodes: value.nodes.items.into_iter().map(Into::into).collect(),
892                edges: value.edges.items.into_iter().map(Into::into).collect(),
893            }
894        }
895    }
896
897    impl From<NodeXml> for super::graph_doc::NodeDoc {
898        fn from(value: NodeXml) -> Self {
899            Self {
900                id: value.id,
901                label: value.label,
902                props: value.props.into(),
903            }
904        }
905    }
906
907    impl From<EdgeXml> for super::graph_doc::EdgeDoc {
908        fn from(value: EdgeXml) -> Self {
909            Self {
910                id: value.id,
911                label: value.label,
912                from: value.from,
913                to: value.to,
914                props: value.props.into(),
915            }
916        }
917    }
918
919    impl From<PropsXml> for Props {
920        fn from(value: PropsXml) -> Self {
921            value
922                .items
923                .into_iter()
924                .map(|prop| (prop.key, prop.value))
925                .collect()
926        }
927    }
928
929    impl From<&Graph> for GraphXml {
930        fn from(graph: &Graph) -> Self {
931            Self {
932                nodes: NodesXml {
933                    items: graph.nodes.iter().map(NodeXml::from).collect(),
934                },
935                edges: EdgesXml {
936                    items: graph.edges.iter().map(EdgeXml::from).collect(),
937                },
938            }
939        }
940    }
941
942    impl From<&Node> for NodeXml {
943        fn from(node: &Node) -> Self {
944            let props = super::graph_doc::graph_to_doc(&Graph::new(vec![node.clone()], Vec::new()))
945                .nodes
946                .into_iter()
947                .next()
948                .expect("node exists")
949                .props;
950            Self {
951                id: node.id.clone(),
952                label: node.label.clone(),
953                props: props.into(),
954            }
955        }
956    }
957
958    impl From<&Edge> for EdgeXml {
959        fn from(edge: &Edge) -> Self {
960            Self {
961                id: edge.id.clone(),
962                label: edge.label.clone(),
963                from: edge.from.clone(),
964                to: edge.to.clone(),
965                props: edge.props.clone().into(),
966            }
967        }
968    }
969
970    impl From<Props> for PropsXml {
971        fn from(value: Props) -> Self {
972            Self {
973                items: value
974                    .into_iter()
975                    .map(|(key, value)| PropXml { key, value })
976                    .collect(),
977            }
978        }
979    }
980}
981
982#[derive(Clone, Debug, Eq, PartialEq)]
983pub enum EdgePolicy {
984    AllowDuplicates,
985    DedupeByFromLabelTo,
986}
987
988impl Default for EdgePolicy {
989    fn default() -> Self {
990        Self::DedupeByFromLabelTo
991    }
992}
993
994#[derive(Clone, Debug, Default)]
995pub struct GraphBuilder {
996    nodes: BTreeMap<NodeId, Node>,
997    edges: Vec<Edge>,
998    edge_keys: BTreeSet<(NodeId, Label, NodeId)>,
999    edge_policy: EdgePolicy,
1000}
1001
1002impl GraphBuilder {
1003    pub fn new() -> Self {
1004        Self::default()
1005    }
1006
1007    pub fn edge_policy(mut self, edge_policy: EdgePolicy) -> Self {
1008        self.edge_policy = edge_policy;
1009        self
1010    }
1011
1012    pub fn node<'a>(
1013        &'a mut self,
1014        label: impl Into<Label>,
1015        id: impl Into<NodeId>,
1016    ) -> NodeBuilder<'a> {
1017        NodeBuilder {
1018            builder: self,
1019            label: label.into(),
1020            id: id.into(),
1021            props: Props::new(),
1022        }
1023    }
1024
1025    pub fn edge<'a>(
1026        &'a mut self,
1027        label: impl Into<Label>,
1028        from: impl Into<NodeId>,
1029        to: impl Into<NodeId>,
1030    ) -> EdgeBuilder<'a> {
1031        EdgeBuilder {
1032            builder: self,
1033            id: None,
1034            label: label.into(),
1035            from: from.into(),
1036            to: to.into(),
1037            props: Props::new(),
1038        }
1039    }
1040
1041    pub fn add_node(&mut self, node: Node) -> NodeId {
1042        let id = node.id.clone();
1043        self.nodes
1044            .entry(id.clone())
1045            .and_modify(|existing| {
1046                if existing.label == node.label {
1047                    existing.props.extend(node.props.clone());
1048                }
1049            })
1050            .or_insert(node);
1051        id
1052    }
1053
1054    pub fn add_edge(&mut self, edge: Edge) -> Option<EdgeId> {
1055        let id = edge.id.clone();
1056        match self.edge_policy {
1057            EdgePolicy::AllowDuplicates => self.edges.push(edge),
1058            EdgePolicy::DedupeByFromLabelTo => {
1059                let key = (edge.from.clone(), edge.label.clone(), edge.to.clone());
1060                if self.edge_keys.insert(key) {
1061                    self.edges.push(edge);
1062                }
1063            }
1064        }
1065        id
1066    }
1067
1068    pub fn build(self) -> Graph {
1069        Graph {
1070            nodes: self.nodes.into_values().collect(),
1071            edges: self.edges,
1072        }
1073    }
1074}
1075
1076pub struct NodeBuilder<'a> {
1077    builder: &'a mut GraphBuilder,
1078    label: Label,
1079    id: NodeId,
1080    props: Props,
1081}
1082
1083impl<'a> NodeBuilder<'a> {
1084    pub fn prop(mut self, key: impl Into<String>, value: impl Into<Value>) -> Self {
1085        self.props.insert(key.into(), value.into());
1086        self
1087    }
1088
1089    pub fn props(mut self, props: Props) -> Self {
1090        self.props.extend(props);
1091        self
1092    }
1093
1094    pub fn finish(self) -> NodeId {
1095        let node = Node::new(self.label, self.id, self.props);
1096        self.builder.add_node(node)
1097    }
1098}
1099
1100pub struct EdgeBuilder<'a> {
1101    builder: &'a mut GraphBuilder,
1102    id: Option<EdgeId>,
1103    label: Label,
1104    from: NodeId,
1105    to: NodeId,
1106    props: Props,
1107}
1108
1109impl<'a> EdgeBuilder<'a> {
1110    pub fn id(mut self, id: impl Into<EdgeId>) -> Self {
1111        self.id = Some(id.into());
1112        self
1113    }
1114
1115    pub fn prop(mut self, key: impl Into<String>, value: impl Into<Value>) -> Self {
1116        self.props.insert(key.into(), value.into());
1117        self
1118    }
1119
1120    pub fn props(mut self, props: Props) -> Self {
1121        self.props.extend(props);
1122        self
1123    }
1124
1125    pub fn finish(self) -> Option<EdgeId> {
1126        let mut edge = Edge::new(self.label, self.from, self.to, self.props);
1127        edge.id = self.id;
1128        self.builder.add_edge(edge)
1129    }
1130}
1131
1132#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1133pub struct GraphSchema {
1134    pub nodes: Vec<NodeType>,
1135    pub edges: Vec<EdgeType>,
1136}
1137
1138impl GraphSchema {
1139    pub fn builder() -> GraphSchemaBuilder {
1140        GraphSchemaBuilder::default()
1141    }
1142
1143    pub fn node_type(&self, label: &Label) -> Option<&NodeType> {
1144        self.nodes
1145            .iter()
1146            .find(|node_type| &node_type.label == label)
1147    }
1148
1149    pub fn edge_type(&self, label: &Label) -> Option<&EdgeType> {
1150        self.edges
1151            .iter()
1152            .find(|edge_type| &edge_type.label == label)
1153    }
1154
1155    pub fn validate_graph(&self, graph: &Graph) -> Result<()> {
1156        for node in &graph.nodes {
1157            self.validate_node(node)?;
1158        }
1159        for edge in &graph.edges {
1160            self.validate_edge(edge, graph)?;
1161        }
1162        Ok(())
1163    }
1164
1165    pub fn validate_node(&self, node: &Node) -> Result<()> {
1166        let node_type = self.node_type(&node.label).ok_or_else(|| {
1167            GrustError::Schema(format!("schema has no node type '{}'", node.label.as_str()))
1168        })?;
1169        validate_props(
1170            &node.props,
1171            &node_type.fields,
1172            &format!("node '{}'", node.id.as_str()),
1173        )
1174    }
1175
1176    pub fn validate_edge(&self, edge: &Edge, graph: &Graph) -> Result<()> {
1177        let edge_type = self.edge_type(&edge.label).ok_or_else(|| {
1178            GrustError::Schema(format!("schema has no edge type '{}'", edge.label.as_str()))
1179        })?;
1180
1181        let from = graph.nodes.iter().find(|node| node.id == edge.from);
1182        let to = graph.nodes.iter().find(|node| node.id == edge.to);
1183        let from = from.ok_or_else(|| {
1184            GrustError::Schema(format!(
1185                "edge '{}' references unknown from node '{}'",
1186                edge.label.as_str(),
1187                edge.from.as_str()
1188            ))
1189        })?;
1190        let to = to.ok_or_else(|| {
1191            GrustError::Schema(format!(
1192                "edge '{}' references unknown to node '{}'",
1193                edge.label.as_str(),
1194                edge.to.as_str()
1195            ))
1196        })?;
1197
1198        if !edge_type.from.is_empty() && !edge_type.from.iter().any(|label| label == &from.label) {
1199            return Err(GrustError::Schema(format!(
1200                "edge '{}' cannot start from node label '{}'",
1201                edge.label.as_str(),
1202                from.label.as_str()
1203            )));
1204        }
1205        if !edge_type.to.is_empty() && !edge_type.to.iter().any(|label| label == &to.label) {
1206            return Err(GrustError::Schema(format!(
1207                "edge '{}' cannot end at node label '{}'",
1208                edge.label.as_str(),
1209                to.label.as_str()
1210            )));
1211        }
1212
1213        validate_props(
1214            &edge.props,
1215            &edge_type.fields,
1216            &format!("edge '{}'", edge.label.as_str()),
1217        )
1218    }
1219}
1220
1221#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1222pub struct NodeType {
1223    pub label: Label,
1224    pub fields: Vec<Field>,
1225}
1226
1227#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1228pub struct EdgeType {
1229    pub label: Label,
1230    pub from: Vec<Label>,
1231    pub to: Vec<Label>,
1232    pub fields: Vec<Field>,
1233    pub directed: bool,
1234    pub uniqueness: EdgeUniqueness,
1235}
1236
1237#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1238pub struct Field {
1239    pub name: String,
1240    pub ty: FieldType,
1241    pub required: bool,
1242}
1243
1244#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1245pub enum FieldType {
1246    String,
1247    Int,
1248    Float,
1249    Bool,
1250    DateTime,
1251    StringArray,
1252    Json,
1253}
1254
1255#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1256pub enum EdgeUniqueness {
1257    None,
1258    FromTo,
1259    FromLabelTo,
1260}
1261
1262#[derive(Clone, Debug, Default)]
1263pub struct GraphSchemaBuilder {
1264    nodes: Vec<NodeType>,
1265    edges: Vec<EdgeType>,
1266}
1267
1268impl GraphSchemaBuilder {
1269    pub fn node(mut self, label: impl Into<Label>, fields: impl Into<Vec<Field>>) -> Self {
1270        self.nodes.push(NodeType {
1271            label: label.into(),
1272            fields: fields.into(),
1273        });
1274        self
1275    }
1276
1277    pub fn edge(
1278        mut self,
1279        label: impl Into<Label>,
1280        from: impl Into<Vec<Label>>,
1281        to: impl Into<Vec<Label>>,
1282        fields: impl Into<Vec<Field>>,
1283    ) -> Self {
1284        self.edges.push(EdgeType {
1285            label: label.into(),
1286            from: from.into(),
1287            to: to.into(),
1288            fields: fields.into(),
1289            directed: true,
1290            uniqueness: EdgeUniqueness::FromLabelTo,
1291        });
1292        self
1293    }
1294
1295    pub fn edge_type(mut self, edge_type: EdgeType) -> Self {
1296        self.edges.push(edge_type);
1297        self
1298    }
1299
1300    pub fn build(self) -> GraphSchema {
1301        GraphSchema {
1302            nodes: self.nodes,
1303            edges: self.edges,
1304        }
1305    }
1306}
1307
1308impl Field {
1309    pub fn required(name: impl Into<String>, ty: FieldType) -> Self {
1310        Self {
1311            name: name.into(),
1312            ty,
1313            required: true,
1314        }
1315    }
1316
1317    pub fn optional(name: impl Into<String>, ty: FieldType) -> Self {
1318        Self {
1319            name: name.into(),
1320            ty,
1321            required: false,
1322        }
1323    }
1324}
1325
1326fn validate_props(props: &Props, fields: &[Field], context: &str) -> Result<()> {
1327    for field in fields {
1328        match props.get(&field.name) {
1329            Some(value) => validate_field_value(value, &field.ty, context, &field.name)?,
1330            None if field.required => {
1331                return Err(GrustError::Schema(format!(
1332                    "{context} missing required field '{}'",
1333                    field.name
1334                )));
1335            }
1336            None => {}
1337        }
1338    }
1339    Ok(())
1340}
1341
1342fn validate_field_value(
1343    value: &Value,
1344    ty: &FieldType,
1345    context: &str,
1346    field_name: &str,
1347) -> Result<()> {
1348    let matches = matches!(
1349        (value, ty),
1350        (Value::String(_), FieldType::String)
1351            | (Value::Int(_), FieldType::Int)
1352            | (Value::Float(_), FieldType::Float)
1353            | (Value::Bool(_), FieldType::Bool)
1354            | (Value::String(_), FieldType::DateTime)
1355            | (Value::StringArray(_), FieldType::StringArray)
1356            | (_, FieldType::Json)
1357    );
1358    if matches {
1359        Ok(())
1360    } else {
1361        Err(GrustError::Schema(format!(
1362            "{context} field '{field_name}' expected {ty:?}, got {value:?}"
1363        )))
1364    }
1365}
1366
1367#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1368pub struct Traversal {
1369    pub start: Start,
1370    pub steps: Vec<Step>,
1371    pub limit: Option<u32>,
1372}
1373
1374impl Traversal {
1375    pub fn from_node(id: impl Into<NodeId>) -> Self {
1376        Self {
1377            start: Start::Node(id.into()),
1378            steps: Vec::new(),
1379            limit: None,
1380        }
1381    }
1382
1383    pub fn out(mut self, edge: impl Into<Label>) -> Self {
1384        self.steps.push(Step {
1385            direction: Direction::Out,
1386            edge: Some(edge.into()),
1387            node: None,
1388        });
1389        self
1390    }
1391
1392    pub fn in_(mut self, edge: impl Into<Label>) -> Self {
1393        self.steps.push(Step {
1394            direction: Direction::In,
1395            edge: Some(edge.into()),
1396            node: None,
1397        });
1398        self
1399    }
1400
1401    pub fn both(mut self, edge: impl Into<Label>) -> Self {
1402        self.steps.push(Step {
1403            direction: Direction::Both,
1404            edge: Some(edge.into()),
1405            node: None,
1406        });
1407        self
1408    }
1409
1410    pub fn to(mut self, node: impl Into<Label>) -> Self {
1411        if let Some(step) = self.steps.last_mut() {
1412            step.node = Some(node.into());
1413        }
1414        self
1415    }
1416
1417    pub fn limit(mut self, limit: u32) -> Self {
1418        self.limit = Some(limit);
1419        self
1420    }
1421}
1422
1423#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1424pub enum Start {
1425    Node(NodeId),
1426    NodesByLabel(Label),
1427    NodesByProperty {
1428        label: Label,
1429        key: String,
1430        value: Value,
1431    },
1432}
1433
1434#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1435pub struct Step {
1436    pub direction: Direction,
1437    pub edge: Option<Label>,
1438    pub node: Option<Label>,
1439}
1440
1441#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1442pub enum Direction {
1443    Out,
1444    In,
1445    Both,
1446}
1447
1448#[derive(Clone, Debug, Default, PartialEq)]
1449pub struct EdgeQuery {
1450    pub from: Option<NodeId>,
1451    pub to: Option<NodeId>,
1452    pub label: Option<Label>,
1453}
1454
1455#[derive(Clone, Debug, Default, Eq, PartialEq)]
1456pub struct LoadReport {
1457    pub nodes: usize,
1458    pub edges: usize,
1459}
1460
1461#[async_trait]
1462pub trait GraphStore: Send + Sync {
1463    async fn apply_schema(&self, _schema: &GraphSchema) -> Result<()> {
1464        Ok(())
1465    }
1466
1467    async fn put_node(&self, node: &Node) -> Result<NodeId>;
1468    async fn put_edge(&self, edge: &Edge) -> Result<Option<EdgeId>>;
1469
1470    async fn put_graph(&self, graph: &Graph) -> Result<LoadReport> {
1471        let mut report = LoadReport::default();
1472        for node in &graph.nodes {
1473            self.put_node(node).await?;
1474            report.nodes += 1;
1475        }
1476        for edge in &graph.edges {
1477            self.put_edge(edge).await?;
1478            report.edges += 1;
1479        }
1480        Ok(report)
1481    }
1482
1483    async fn put_typed_graph(&self, schema: &GraphSchema, graph: &Graph) -> Result<LoadReport> {
1484        schema.validate_graph(graph)?;
1485        self.apply_schema(schema).await?;
1486        self.put_graph(graph).await
1487    }
1488
1489    async fn get_node(&self, id: &NodeId) -> Result<Option<Node>>;
1490    async fn get_edges(&self, query: EdgeQuery) -> Result<Vec<Edge>>;
1491    async fn traverse(&self, traversal: Traversal) -> Result<Vec<Node>>;
1492}
1493
1494#[async_trait]
1495pub trait GraphAdminStore: GraphStore {
1496    async fn bootstrap(&self) -> Result<()> {
1497        Ok(())
1498    }
1499
1500    async fn clear(&self) -> Result<()>;
1501}
1502
1503pub mod prelude {
1504    pub use crate::{
1505        Direction, Edge, EdgeId, EdgePolicy, EdgeQuery, EdgeType, Field, FieldType, Graph,
1506        GraphAdminStore, GraphBuilder, GraphSchema, GraphSchemaBuilder, GraphStore, GrustError,
1507        Label, LoadReport, Node, NodeId, NodeType, Props, Result, Start, Step, Traversal, Value,
1508    };
1509
1510    #[cfg(feature = "typed-garde")]
1511    pub use crate::typed::{TypedEdge, TypedGraphBuilder, TypedNode, garde, props_from_serialize};
1512
1513    #[cfg(feature = "typed-zod-rs")]
1514    pub use crate::typed::{parse_typed_json, parse_typed_json_with, zod_rs};
1515}
1516
1517#[cfg(test)]
1518mod tests;