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 NodeId
79);
80string_newtype!(
81 EdgeId
83);
84string_newtype!(
85 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 DateTime(String),
100 StringArray(Vec<String>),
101 IntArray(Vec<i64>),
102 FloatArray(Vec<f64>),
103 Json(serde_json::Value),
104}
105
106impl Value {
107 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 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 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
187fn 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, de::DeserializeOwned};
367
368 use crate::{
369 Edge, EdgeId, Graph, GraphBuilder, GrustError, Node, NodeId, Props, PutOutcome, Result,
370 Value,
371 };
372
373 pub use garde;
374 #[cfg(feature = "typed-zod-rs")]
375 pub use zod_rs;
376
377 pub trait TypedNode: garde::Validate + Serialize {
378 const LABEL: &'static str;
379
380 fn node_id(&self) -> NodeId;
381
382 fn node_props(&self) -> Result<Props> {
383 props_from_serialize(self)
384 }
385
386 fn from_node(node: &Node) -> Result<Self>
387 where
388 Self: Sized + DeserializeOwned,
389 Self::Context: Default,
390 {
391 let ctx = Self::Context::default();
392 Self::from_node_with(node, &ctx)
393 }
394
395 fn from_node_with(node: &Node, ctx: &Self::Context) -> Result<Self>
396 where
397 Self: Sized + DeserializeOwned,
398 {
399 if node.label.as_str() != Self::LABEL {
400 return Err(GrustError::Schema(format!(
401 "node '{}' has label '{}', expected '{}'",
402 node.id.as_str(),
403 node.label.as_str(),
404 Self::LABEL
405 )));
406 }
407 let typed: Self =
408 serde_json::from_value(props_to_plain_json(&node.props)).map_err(|err| {
409 GrustError::Serialization(format!("typed node decode error: {err}"))
410 })?;
411 typed
412 .validate_with(ctx)
413 .map_err(|err| validation_error(Self::LABEL, err))?;
414 let typed_id = typed.node_id();
415 if typed_id != node.id {
416 return Err(GrustError::Schema(format!(
417 "typed node '{}' decoded id '{}', expected '{}'",
418 Self::LABEL,
419 typed_id.as_str(),
420 node.id.as_str()
421 )));
422 }
423 Ok(typed)
424 }
425 }
426
427 pub trait TypedEdge: garde::Validate + Serialize {
428 const LABEL: &'static str;
429
430 fn source_node_id(&self) -> NodeId;
431
432 fn target_node_id(&self) -> NodeId;
433
434 fn edge_id(&self) -> Option<EdgeId> {
435 None
436 }
437
438 fn edge_props(&self) -> Result<Props> {
439 props_from_serialize(self)
440 }
441
442 fn from_edge(edge: &Edge) -> Result<Self>
443 where
444 Self: Sized + DeserializeOwned,
445 Self::Context: Default,
446 {
447 let ctx = Self::Context::default();
448 Self::from_edge_with(edge, &ctx)
449 }
450
451 fn from_edge_with(edge: &Edge, ctx: &Self::Context) -> Result<Self>
452 where
453 Self: Sized + DeserializeOwned,
454 {
455 if edge.label.as_str() != Self::LABEL {
456 return Err(GrustError::Schema(format!(
457 "edge from '{}' to '{}' has label '{}', expected '{}'",
458 edge.from.as_str(),
459 edge.to.as_str(),
460 edge.label.as_str(),
461 Self::LABEL
462 )));
463 }
464 let typed: Self =
465 serde_json::from_value(props_to_plain_json(&edge.props)).map_err(|err| {
466 GrustError::Serialization(format!("typed edge decode error: {err}"))
467 })?;
468 typed
469 .validate_with(ctx)
470 .map_err(|err| validation_error(Self::LABEL, err))?;
471 if typed.source_node_id() != edge.from || typed.target_node_id() != edge.to {
472 return Err(GrustError::Schema(format!(
473 "typed edge '{}' decoded endpoints '{}' -> '{}', expected '{}' -> '{}'",
474 Self::LABEL,
475 typed.source_node_id().as_str(),
476 typed.target_node_id().as_str(),
477 edge.from.as_str(),
478 edge.to.as_str()
479 )));
480 }
481 if let Some(decoded_id) = typed.edge_id()
482 && edge
483 .id
484 .as_ref()
485 .is_some_and(|edge_id| edge_id != &decoded_id)
486 {
487 return Err(GrustError::Schema(format!(
488 "typed edge '{}' decoded id '{}', expected '{}'",
489 Self::LABEL,
490 decoded_id.as_str(),
491 edge.id.as_ref().expect("edge id checked").as_str()
492 )));
493 }
494 Ok(typed)
495 }
496 }
497
498 #[derive(Clone, Debug, Default)]
499 pub struct TypedGraphBuilder {
500 builder: GraphBuilder,
501 }
502
503 impl TypedGraphBuilder {
504 pub fn new() -> Self {
505 Self::default()
506 }
507
508 pub fn from_builder(builder: GraphBuilder) -> Self {
509 Self { builder }
510 }
511
512 pub fn from_graph(graph: Graph) -> Self {
513 let mut builder = GraphBuilder::new();
514 for node in graph.nodes {
515 builder.add_node(node);
516 }
517 for edge in graph.edges {
518 builder.add_edge(edge);
519 }
520 Self { builder }
521 }
522
523 pub fn add_raw_node(&mut self, node: Node) -> NodeId {
524 self.builder.add_node(node)
525 }
526
527 pub fn add_raw_edge(&mut self, edge: Edge) -> PutOutcome {
528 self.builder.add_edge(edge)
529 }
530
531 pub fn add_node<T>(&mut self, node: &T) -> Result<NodeId>
532 where
533 T: TypedNode,
534 T::Context: Default,
535 {
536 node.validate()
537 .map_err(|err| validation_error(T::LABEL, err))?;
538 self.add_validated_node(node)
539 }
540
541 pub fn add_node_with<T>(&mut self, node: &T, ctx: &T::Context) -> Result<NodeId>
542 where
543 T: TypedNode,
544 {
545 node.validate_with(ctx)
546 .map_err(|err| validation_error(T::LABEL, err))?;
547 self.add_validated_node(node)
548 }
549
550 pub fn add_edge<T>(&mut self, edge: &T) -> Result<PutOutcome>
551 where
552 T: TypedEdge,
553 T::Context: Default,
554 {
555 edge.validate()
556 .map_err(|err| validation_error(T::LABEL, err))?;
557 self.add_validated_edge(edge)
558 }
559
560 pub fn add_edge_with<T>(&mut self, edge: &T, ctx: &T::Context) -> Result<PutOutcome>
561 where
562 T: TypedEdge,
563 {
564 edge.validate_with(ctx)
565 .map_err(|err| validation_error(T::LABEL, err))?;
566 self.add_validated_edge(edge)
567 }
568
569 #[cfg(feature = "typed-zod-rs")]
570 pub fn add_node_from_json<T, S>(
571 &mut self,
572 schema: &S,
573 value: &serde_json::Value,
574 ) -> Result<NodeId>
575 where
576 T: TypedNode + DeserializeOwned,
577 T::Context: Default,
578 S: zod_rs::Schema<serde_json::Value>,
579 {
580 let node = parse_typed_json::<T, S>(schema, value)?;
581 self.add_validated_node(&node)
582 }
583
584 #[cfg(feature = "typed-zod-rs")]
585 pub fn add_node_from_json_with<T, S>(
586 &mut self,
587 schema: &S,
588 value: &serde_json::Value,
589 ctx: &T::Context,
590 ) -> Result<NodeId>
591 where
592 T: TypedNode + DeserializeOwned,
593 S: zod_rs::Schema<serde_json::Value>,
594 {
595 let node = parse_typed_json_with::<T, S>(schema, value, ctx)?;
596 self.add_validated_node(&node)
597 }
598
599 #[cfg(feature = "typed-zod-rs")]
600 pub fn add_edge_from_json<T, S>(
601 &mut self,
602 schema: &S,
603 value: &serde_json::Value,
604 ) -> Result<PutOutcome>
605 where
606 T: TypedEdge + DeserializeOwned,
607 T::Context: Default,
608 S: zod_rs::Schema<serde_json::Value>,
609 {
610 let edge = parse_typed_json::<T, S>(schema, value)?;
611 self.add_validated_edge(&edge)
612 }
613
614 #[cfg(feature = "typed-zod-rs")]
615 pub fn add_edge_from_json_with<T, S>(
616 &mut self,
617 schema: &S,
618 value: &serde_json::Value,
619 ctx: &T::Context,
620 ) -> Result<PutOutcome>
621 where
622 T: TypedEdge + DeserializeOwned,
623 S: zod_rs::Schema<serde_json::Value>,
624 {
625 let edge = parse_typed_json_with::<T, S>(schema, value, ctx)?;
626 self.add_validated_edge(&edge)
627 }
628
629 pub fn build(self) -> Graph {
630 self.builder.build()
631 }
632
633 pub fn into_builder(self) -> GraphBuilder {
634 self.builder
635 }
636
637 fn add_validated_node<T>(&mut self, node: &T) -> Result<NodeId>
638 where
639 T: TypedNode,
640 {
641 let node_id = node.node_id();
642 let mut props = node.node_props()?;
643 props
644 .entry("id".to_string())
645 .or_insert_with(|| Value::from(node_id.as_str()));
646 let graph_node = Node::new(T::LABEL, node_id, props);
647 Ok(self.builder.add_node(graph_node))
648 }
649
650 fn add_validated_edge<T>(&mut self, edge: &T) -> Result<PutOutcome>
651 where
652 T: TypedEdge,
653 {
654 let mut graph_edge = Edge::new(
655 T::LABEL,
656 edge.source_node_id(),
657 edge.target_node_id(),
658 edge.edge_props()?,
659 );
660 graph_edge.id = edge.edge_id();
661 Ok(self.builder.add_edge(graph_edge))
662 }
663 }
664
665 pub fn props_from_serialize<T>(value: &T) -> Result<Props>
666 where
667 T: Serialize + ?Sized,
668 {
669 let serialized = serde_json::to_value(value)
670 .map_err(|err| GrustError::Serialization(format!("typed props error: {err}")))?;
671 let serde_json::Value::Object(fields) = serialized else {
672 return Err(GrustError::Schema(
673 "typed graph values must serialize as JSON objects".to_string(),
674 ));
675 };
676
677 Ok(fields
678 .into_iter()
679 .map(|(key, value)| (key, Value::from(value)))
680 .collect())
681 }
682
683 fn props_to_plain_json(props: &Props) -> serde_json::Value {
684 serde_json::Value::Object(
685 props
686 .iter()
687 .map(|(key, value)| (key.clone(), value.to_json()))
688 .collect(),
689 )
690 }
691
692 #[cfg(feature = "typed-zod-rs")]
693 pub fn parse_typed_json<T, S>(schema: &S, value: &serde_json::Value) -> Result<T>
694 where
695 T: DeserializeOwned + garde::Validate,
696 T::Context: Default,
697 S: zod_rs::Schema<serde_json::Value>,
698 {
699 let ctx = T::Context::default();
700 parse_typed_json_with(schema, value, &ctx)
701 }
702
703 #[cfg(feature = "typed-zod-rs")]
704 pub fn parse_typed_json_with<T, S>(
705 schema: &S,
706 value: &serde_json::Value,
707 ctx: &T::Context,
708 ) -> Result<T>
709 where
710 T: DeserializeOwned + garde::Validate,
711 S: zod_rs::Schema<serde_json::Value>,
712 {
713 schema
714 .safe_parse(value)
715 .map_err(|err| GrustError::Schema(format!("zod-rs validation failed: {err}")))?;
716 let typed: T = serde_json::from_value(value.clone())
717 .map_err(|err| GrustError::Serialization(format!("typed JSON decode error: {err}")))?;
718 typed
719 .validate_with(ctx)
720 .map_err(|err| GrustError::Schema(format!("typed validation failed: {err}")))?;
721 Ok(typed)
722 }
723
724 fn validation_error(label: &str, err: garde::Report) -> GrustError {
725 GrustError::Schema(format!("{label} validation failed: {err}"))
726 }
727}
728
729#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
730pub struct Node {
731 pub id: NodeId,
732 pub label: Label,
733 pub props: Props,
734}
735
736impl Node {
737 pub fn new(label: impl Into<Label>, id: impl Into<NodeId>, props: impl Into<Props>) -> Self {
738 let id = id.into();
739 let mut props = props.into();
740 props
741 .entry("id".to_string())
742 .or_insert_with(|| Value::from(id.as_str()));
743 Self {
744 id,
745 label: label.into(),
746 props,
747 }
748 }
749}
750
751#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
752pub struct Edge {
753 pub id: Option<EdgeId>,
754 pub from: NodeId,
755 pub to: NodeId,
756 pub label: Label,
757 pub props: Props,
758}
759
760impl Edge {
761 pub fn new(
762 label: impl Into<Label>,
763 from: impl Into<NodeId>,
764 to: impl Into<NodeId>,
765 props: impl Into<Props>,
766 ) -> Self {
767 Self {
768 id: None,
769 from: from.into(),
770 to: to.into(),
771 label: label.into(),
772 props: props.into(),
773 }
774 }
775
776 pub fn with_id(mut self, id: impl Into<EdgeId>) -> Self {
777 self.id = Some(id.into());
778 self
779 }
780}
781
782pub fn relationship_type(value: &str) -> String {
787 let relationship = value
788 .chars()
789 .map(|ch| {
790 if ch.is_ascii_alphanumeric() {
791 ch.to_ascii_uppercase()
792 } else {
793 '_'
794 }
795 })
796 .collect::<String>();
797 if relationship.is_empty() {
798 "RELATED_TO".to_string()
799 } else {
800 relationship
801 }
802}
803
804pub fn schema_identifier(value: &str) -> Result<String> {
809 let identifier = value
810 .chars()
811 .map(|ch| {
812 if ch.is_ascii_alphanumeric() {
813 ch.to_ascii_lowercase()
814 } else {
815 '_'
816 }
817 })
818 .collect::<String>();
819 if identifier.is_empty()
820 || identifier
821 .chars()
822 .next()
823 .is_some_and(|ch| ch.is_ascii_digit())
824 {
825 return Err(GrustError::Schema(format!(
826 "invalid schema identifier '{value}'"
827 )));
828 }
829 Ok(identifier)
830}
831
832pub fn edge_key(edge: &Edge) -> String {
838 edge.id
839 .as_ref()
840 .map(EdgeId::as_str)
841 .map(ToString::to_string)
842 .unwrap_or_else(|| {
843 format!(
844 "{}\u{1f}{}\u{1f}{}",
845 edge.from.as_str(),
846 edge.label.as_str(),
847 edge.to.as_str()
848 )
849 })
850}
851
852#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
853pub struct Graph {
854 pub nodes: Vec<Node>,
855 pub edges: Vec<Edge>,
856}
857
858impl Graph {
859 pub fn new(nodes: Vec<Node>, edges: Vec<Edge>) -> Self {
860 Self { nodes, edges }
861 }
862
863 pub fn from_yaml(yaml: &str) -> Result<Self> {
864 yaml::graph_from_yaml(yaml)
865 }
866
867 pub fn to_yaml(&self) -> Result<String> {
868 yaml::graph_to_yaml(self)
869 }
870
871 pub fn from_json(json: &str) -> Result<Self> {
872 json::graph_from_json(json)
873 }
874
875 pub fn to_json(&self) -> Result<String> {
876 json::graph_to_json(self)
877 }
878
879 pub fn from_xml(xml: &str) -> Result<Self> {
880 xml::graph_from_xml(xml)
881 }
882
883 pub fn to_xml(&self) -> Result<String> {
884 xml::graph_to_xml(self)
885 }
886
887 pub fn builder() -> GraphBuilder {
888 GraphBuilder::new()
889 }
890}
891
892mod graph_doc {
893 use std::collections::{BTreeMap, BTreeSet};
894
895 use serde::{Deserialize, Serialize};
896
897 use crate::{Edge, EdgeId, Graph, GrustError, Label, Node, NodeId, Props, Value};
898
899 #[derive(Debug, Serialize, Deserialize)]
900 pub(super) struct GraphDoc {
901 #[serde(default)]
902 pub(super) nodes: Vec<NodeDoc>,
903 #[serde(default)]
904 pub(super) edges: Vec<EdgeDoc>,
905 }
906
907 #[derive(Debug, Serialize, Deserialize)]
908 pub(super) struct NodeDoc {
909 pub(super) id: NodeId,
910 pub(super) label: Label,
911 #[serde(default, deserialize_with = "deserialize_props")]
912 pub(super) props: Props,
913 }
914
915 #[derive(Debug, Serialize, Deserialize)]
916 pub(super) struct NodeDocOut {
917 pub(super) id: NodeId,
918 pub(super) label: Label,
919 #[serde(default)]
920 pub(super) props: Props,
921 }
922
923 #[derive(Debug, Serialize, Deserialize)]
924 pub(super) struct EdgeDoc {
925 #[serde(default)]
926 pub(super) id: Option<EdgeId>,
927 pub(super) label: Label,
928 pub(super) from: NodeId,
929 pub(super) to: NodeId,
930 #[serde(default, deserialize_with = "deserialize_props")]
931 pub(super) props: Props,
932 }
933
934 #[derive(Debug, Serialize, Deserialize)]
935 pub(super) struct EdgeDocOut {
936 #[serde(default)]
937 pub(super) id: Option<EdgeId>,
938 pub(super) label: Label,
939 pub(super) from: NodeId,
940 pub(super) to: NodeId,
941 #[serde(default)]
942 pub(super) props: Props,
943 }
944
945 pub(super) fn graph_from_doc(doc: GraphDoc) -> super::Result<Graph> {
946 let mut ids = BTreeSet::new();
947 for node in &doc.nodes {
948 if !ids.insert(node.id.clone()) {
949 return Err(GrustError::Schema(format!(
950 "duplicate node id '{}'",
951 node.id
952 )));
953 }
954 }
955
956 let mut edges = Vec::with_capacity(doc.edges.len());
957 for edge in doc.edges {
958 if !ids.contains(&edge.from) {
959 return Err(GrustError::Schema(format!(
960 "edge '{}' references unknown from node '{}'",
961 edge.label, edge.from
962 )));
963 }
964 if !ids.contains(&edge.to) {
965 return Err(GrustError::Schema(format!(
966 "edge '{}' references unknown to node '{}'",
967 edge.label, edge.to
968 )));
969 }
970
971 let mut graph_edge = Edge::new(edge.label, edge.from, edge.to, edge.props);
972 graph_edge.id = edge.id;
973 edges.push(graph_edge);
974 }
975
976 let nodes = doc
977 .nodes
978 .into_iter()
979 .map(|node| Node::new(node.label, node.id, node.props))
980 .collect();
981
982 Ok(Graph::new(nodes, edges))
983 }
984
985 pub(super) fn graph_to_doc(graph: &Graph) -> GraphDocOut {
986 GraphDocOut {
987 nodes: graph
988 .nodes
989 .iter()
990 .map(|node| NodeDocOut {
991 id: node.id.clone(),
992 label: node.label.clone(),
993 props: without_generated_id(&node.props, &node.id),
994 })
995 .collect(),
996 edges: graph
997 .edges
998 .iter()
999 .map(|edge| EdgeDocOut {
1000 id: edge.id.clone(),
1001 label: edge.label.clone(),
1002 from: edge.from.clone(),
1003 to: edge.to.clone(),
1004 props: edge.props.clone(),
1005 })
1006 .collect(),
1007 }
1008 }
1009
1010 fn without_generated_id(props: &Props, id: &NodeId) -> Props {
1011 let mut props = props.clone();
1012 if props.get("id") == Some(&Value::from(id.as_str())) {
1013 props.remove("id");
1014 }
1015 props
1016 }
1017
1018 fn deserialize_props<'de, D>(deserializer: D) -> std::result::Result<Props, D::Error>
1019 where
1020 D: serde::Deserializer<'de>,
1021 {
1022 let raw = BTreeMap::<String, serde_json::Value>::deserialize(deserializer)?;
1023 raw.into_iter()
1024 .map(|(key, value)| {
1025 value_from_json(value)
1026 .map(|value| (key, value))
1027 .map_err(serde::de::Error::custom)
1028 })
1029 .collect()
1030 }
1031
1032 fn value_from_json(value: serde_json::Value) -> std::result::Result<Value, String> {
1033 if let serde_json::Value::Object(mapping) = &value
1034 && mapping.contains_key("type")
1035 && mapping.contains_key("value")
1036 {
1037 return serde_json::from_value(value)
1038 .map_err(|err| format!("invalid tagged Grust value: {err}"));
1039 }
1040
1041 Ok(Value::from_json(value))
1042 }
1043
1044 #[derive(Debug, Serialize, Deserialize)]
1045 pub(super) struct GraphDocOut {
1046 pub(super) nodes: Vec<NodeDocOut>,
1047 pub(super) edges: Vec<EdgeDocOut>,
1048 }
1049}
1050
1051mod yaml {
1052 use crate::{Graph, GrustError};
1053
1054 pub(super) fn graph_from_yaml(yaml: &str) -> super::Result<Graph> {
1055 let doc: super::graph_doc::GraphDoc = serde_yaml::from_str(yaml)
1056 .map_err(|err| GrustError::Serialization(format!("YAML parse error: {err}")))?;
1057 super::graph_doc::graph_from_doc(doc)
1058 }
1059
1060 pub(super) fn graph_to_yaml(graph: &Graph) -> super::Result<String> {
1061 serde_yaml::to_string(&super::graph_doc::graph_to_doc(graph))
1062 .map_err(|err| GrustError::Serialization(format!("YAML serialization error: {err}")))
1063 }
1064}
1065
1066mod json {
1067 use crate::{Graph, GrustError};
1068
1069 pub(super) fn graph_from_json(json: &str) -> super::Result<Graph> {
1070 let doc: super::graph_doc::GraphDoc = serde_json::from_str(json)
1071 .map_err(|err| GrustError::Serialization(format!("JSON parse error: {err}")))?;
1072 super::graph_doc::graph_from_doc(doc)
1073 }
1074
1075 pub(super) fn graph_to_json(graph: &Graph) -> super::Result<String> {
1076 serde_json::to_string_pretty(&super::graph_doc::graph_to_doc(graph))
1077 .map_err(|err| GrustError::Serialization(format!("JSON serialization error: {err}")))
1078 }
1079}
1080
1081mod xml {
1082 use serde::{Deserialize, Serialize};
1083
1084 use crate::{Edge, EdgeId, Graph, GrustError, Label, Node, NodeId, Props, Value};
1085
1086 #[derive(Debug, Serialize, Deserialize)]
1087 #[serde(rename = "graph")]
1088 struct GraphXml {
1089 #[serde(default)]
1090 nodes: NodesXml,
1091 #[serde(default)]
1092 edges: EdgesXml,
1093 }
1094
1095 #[derive(Debug, Default, Serialize, Deserialize)]
1096 struct NodesXml {
1097 #[serde(rename = "node", default)]
1098 items: Vec<NodeXml>,
1099 }
1100
1101 #[derive(Debug, Default, Serialize, Deserialize)]
1102 struct EdgesXml {
1103 #[serde(rename = "edge", default)]
1104 items: Vec<EdgeXml>,
1105 }
1106
1107 #[derive(Debug, Serialize, Deserialize)]
1108 struct NodeXml {
1109 id: NodeId,
1110 label: Label,
1111 #[serde(default)]
1112 props: PropsXml,
1113 }
1114
1115 #[derive(Debug, Serialize, Deserialize)]
1116 struct EdgeXml {
1117 #[serde(default, skip_serializing_if = "Option::is_none")]
1118 id: Option<EdgeId>,
1119 label: Label,
1120 from: NodeId,
1121 to: NodeId,
1122 #[serde(default)]
1123 props: PropsXml,
1124 }
1125
1126 #[derive(Debug, Default, Serialize, Deserialize)]
1127 struct PropsXml {
1128 #[serde(rename = "prop", default)]
1129 items: Vec<PropXml>,
1130 }
1131
1132 #[derive(Debug, Serialize, Deserialize)]
1133 struct PropXml {
1134 key: String,
1135 value: Value,
1136 }
1137
1138 pub(super) fn graph_from_xml(xml: &str) -> super::Result<Graph> {
1139 let doc: GraphXml = quick_xml::de::from_str(xml)
1140 .map_err(|err| GrustError::Serialization(format!("XML parse error: {err}")))?;
1141 super::graph_doc::graph_from_doc(doc.into())
1142 }
1143
1144 pub(super) fn graph_to_xml(graph: &Graph) -> super::Result<String> {
1145 quick_xml::se::to_string(&GraphXml::from(graph))
1146 .map_err(|err| GrustError::Serialization(format!("XML serialization error: {err}")))
1147 }
1148
1149 impl From<GraphXml> for super::graph_doc::GraphDoc {
1150 fn from(value: GraphXml) -> Self {
1151 Self {
1152 nodes: value.nodes.items.into_iter().map(Into::into).collect(),
1153 edges: value.edges.items.into_iter().map(Into::into).collect(),
1154 }
1155 }
1156 }
1157
1158 impl From<NodeXml> for super::graph_doc::NodeDoc {
1159 fn from(value: NodeXml) -> Self {
1160 Self {
1161 id: value.id,
1162 label: value.label,
1163 props: value.props.into(),
1164 }
1165 }
1166 }
1167
1168 impl From<EdgeXml> for super::graph_doc::EdgeDoc {
1169 fn from(value: EdgeXml) -> Self {
1170 Self {
1171 id: value.id,
1172 label: value.label,
1173 from: value.from,
1174 to: value.to,
1175 props: value.props.into(),
1176 }
1177 }
1178 }
1179
1180 impl From<PropsXml> for Props {
1181 fn from(value: PropsXml) -> Self {
1182 value
1183 .items
1184 .into_iter()
1185 .map(|prop| (prop.key, prop.value))
1186 .collect()
1187 }
1188 }
1189
1190 impl From<&Graph> for GraphXml {
1191 fn from(graph: &Graph) -> Self {
1192 Self {
1193 nodes: NodesXml {
1194 items: graph.nodes.iter().map(NodeXml::from).collect(),
1195 },
1196 edges: EdgesXml {
1197 items: graph.edges.iter().map(EdgeXml::from).collect(),
1198 },
1199 }
1200 }
1201 }
1202
1203 impl From<&Node> for NodeXml {
1204 fn from(node: &Node) -> Self {
1205 let props = super::graph_doc::graph_to_doc(&Graph::new(vec![node.clone()], Vec::new()))
1206 .nodes
1207 .into_iter()
1208 .next()
1209 .expect("node exists")
1210 .props;
1211 Self {
1212 id: node.id.clone(),
1213 label: node.label.clone(),
1214 props: props.into(),
1215 }
1216 }
1217 }
1218
1219 impl From<&Edge> for EdgeXml {
1220 fn from(edge: &Edge) -> Self {
1221 Self {
1222 id: edge.id.clone(),
1223 label: edge.label.clone(),
1224 from: edge.from.clone(),
1225 to: edge.to.clone(),
1226 props: edge.props.clone().into(),
1227 }
1228 }
1229 }
1230
1231 impl From<Props> for PropsXml {
1232 fn from(value: Props) -> Self {
1233 Self {
1234 items: value
1235 .into_iter()
1236 .map(|(key, value)| PropXml { key, value })
1237 .collect(),
1238 }
1239 }
1240 }
1241}
1242
1243#[derive(Clone, Debug, Default, Eq, PartialEq)]
1244pub enum EdgePolicy {
1245 AllowDuplicates,
1246 #[default]
1247 DedupeByFromLabelTo,
1248}
1249
1250#[derive(Clone, Debug, Default)]
1251pub struct GraphBuilder {
1252 nodes: BTreeMap<NodeId, Node>,
1253 edges: Vec<Edge>,
1254 edge_keys: BTreeSet<(NodeId, Label, NodeId)>,
1255 edge_policy: EdgePolicy,
1256}
1257
1258impl GraphBuilder {
1259 pub fn new() -> Self {
1260 Self::default()
1261 }
1262
1263 pub fn edge_policy(mut self, edge_policy: EdgePolicy) -> Self {
1264 self.edge_policy = edge_policy;
1265 self
1266 }
1267
1268 pub fn node<'a>(
1269 &'a mut self,
1270 label: impl Into<Label>,
1271 id: impl Into<NodeId>,
1272 ) -> NodeBuilder<'a> {
1273 NodeBuilder {
1274 builder: self,
1275 label: label.into(),
1276 id: id.into(),
1277 props: Props::new(),
1278 }
1279 }
1280
1281 pub fn edge<'a>(
1282 &'a mut self,
1283 label: impl Into<Label>,
1284 from: impl Into<NodeId>,
1285 to: impl Into<NodeId>,
1286 ) -> EdgeBuilder<'a> {
1287 EdgeBuilder {
1288 builder: self,
1289 id: None,
1290 label: label.into(),
1291 from: from.into(),
1292 to: to.into(),
1293 props: Props::new(),
1294 }
1295 }
1296
1297 pub fn add_node(&mut self, node: Node) -> NodeId {
1304 let id = node.id.clone();
1305 self.nodes
1306 .entry(id.clone())
1307 .and_modify(|existing| {
1308 if existing.label == node.label {
1309 existing.props.extend(node.props.clone());
1310 } else {
1311 *existing = node.clone();
1312 }
1313 })
1314 .or_insert(node);
1315 id
1316 }
1317
1318 pub fn add_edge(&mut self, edge: Edge) -> PutOutcome {
1321 match self.edge_policy {
1322 EdgePolicy::AllowDuplicates => {
1323 self.edges.push(edge);
1324 PutOutcome::Inserted
1325 }
1326 EdgePolicy::DedupeByFromLabelTo => {
1327 let key = (edge.from.clone(), edge.label.clone(), edge.to.clone());
1328 if self.edge_keys.insert(key) {
1329 self.edges.push(edge);
1330 PutOutcome::Inserted
1331 } else {
1332 PutOutcome::Deduped
1333 }
1334 }
1335 }
1336 }
1337
1338 pub fn build(self) -> Graph {
1339 Graph {
1340 nodes: self.nodes.into_values().collect(),
1341 edges: self.edges,
1342 }
1343 }
1344}
1345
1346pub struct NodeBuilder<'a> {
1347 builder: &'a mut GraphBuilder,
1348 label: Label,
1349 id: NodeId,
1350 props: Props,
1351}
1352
1353impl<'a> NodeBuilder<'a> {
1354 pub fn prop(mut self, key: impl Into<String>, value: impl Into<Value>) -> Self {
1355 self.props.insert(key.into(), value.into());
1356 self
1357 }
1358
1359 pub fn props(mut self, props: Props) -> Self {
1360 self.props.extend(props);
1361 self
1362 }
1363
1364 pub fn finish(self) -> NodeId {
1365 let node = Node::new(self.label, self.id, self.props);
1366 self.builder.add_node(node)
1367 }
1368}
1369
1370pub struct EdgeBuilder<'a> {
1371 builder: &'a mut GraphBuilder,
1372 id: Option<EdgeId>,
1373 label: Label,
1374 from: NodeId,
1375 to: NodeId,
1376 props: Props,
1377}
1378
1379impl<'a> EdgeBuilder<'a> {
1380 pub fn id(mut self, id: impl Into<EdgeId>) -> Self {
1381 self.id = Some(id.into());
1382 self
1383 }
1384
1385 pub fn prop(mut self, key: impl Into<String>, value: impl Into<Value>) -> Self {
1386 self.props.insert(key.into(), value.into());
1387 self
1388 }
1389
1390 pub fn props(mut self, props: Props) -> Self {
1391 self.props.extend(props);
1392 self
1393 }
1394
1395 pub fn finish(self) -> PutOutcome {
1396 let mut edge = Edge::new(self.label, self.from, self.to, self.props);
1397 edge.id = self.id;
1398 self.builder.add_edge(edge)
1399 }
1400}
1401
1402#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1403pub struct GraphSchema {
1404 pub nodes: Vec<NodeType>,
1405 pub edges: Vec<EdgeType>,
1406}
1407
1408impl GraphSchema {
1409 pub fn builder() -> GraphSchemaBuilder {
1410 GraphSchemaBuilder::default()
1411 }
1412
1413 pub fn node_type(&self, label: &Label) -> Option<&NodeType> {
1414 self.nodes
1415 .iter()
1416 .find(|node_type| &node_type.label == label)
1417 }
1418
1419 pub fn edge_type(&self, label: &Label) -> Option<&EdgeType> {
1420 self.edges
1421 .iter()
1422 .find(|edge_type| &edge_type.label == label)
1423 }
1424
1425 pub fn validate_graph(&self, graph: &Graph) -> Result<()> {
1426 for node in &graph.nodes {
1427 self.validate_node(node)?;
1428 }
1429 let labels: BTreeMap<&NodeId, &Label> = graph
1430 .nodes
1431 .iter()
1432 .map(|node| (&node.id, &node.label))
1433 .collect();
1434 for edge in &graph.edges {
1435 self.validate_edge_with(edge, |id| labels.get(id).copied())?;
1436 }
1437 self.validate_edge_uniqueness(graph)
1438 }
1439
1440 fn validate_edge_uniqueness(&self, graph: &Graph) -> Result<()> {
1443 let mut seen = BTreeSet::new();
1444 for edge in &graph.edges {
1445 let Some(edge_type) = self.edge_type(&edge.label) else {
1446 continue;
1447 };
1448 if edge_type.uniqueness == EdgeUniqueness::None {
1449 continue;
1450 }
1451 let (a, b) = if edge_type.directed || edge.from <= edge.to {
1452 (&edge.from, &edge.to)
1453 } else {
1454 (&edge.to, &edge.from)
1455 };
1456 if !seen.insert((edge.label.clone(), a.clone(), b.clone())) {
1457 return Err(GrustError::Schema(format!(
1458 "duplicate edge '{}' between '{}' and '{}' violates {:?} uniqueness",
1459 edge.label.as_str(),
1460 a.as_str(),
1461 b.as_str(),
1462 edge_type.uniqueness
1463 )));
1464 }
1465 }
1466 Ok(())
1467 }
1468
1469 pub fn validate_node(&self, node: &Node) -> Result<()> {
1470 let node_type = self.node_type(&node.label).ok_or_else(|| {
1471 GrustError::Schema(format!("schema has no node type '{}'", node.label.as_str()))
1472 })?;
1473 validate_props(
1474 &node.props,
1475 &node_type.fields,
1476 &format!("node '{}'", node.id.as_str()),
1477 )
1478 }
1479
1480 pub fn validate_edge(&self, edge: &Edge, graph: &Graph) -> Result<()> {
1481 self.validate_edge_with(edge, |id| {
1482 graph
1483 .nodes
1484 .iter()
1485 .find(|node| &node.id == id)
1486 .map(|node| &node.label)
1487 })
1488 }
1489
1490 pub fn validate_edge_with<'a>(
1493 &self,
1494 edge: &Edge,
1495 lookup: impl Fn(&NodeId) -> Option<&'a Label>,
1496 ) -> Result<()> {
1497 let edge_type = self.edge_type(&edge.label).ok_or_else(|| {
1498 GrustError::Schema(format!("schema has no edge type '{}'", edge.label.as_str()))
1499 })?;
1500
1501 let from_label = lookup(&edge.from).ok_or_else(|| {
1502 GrustError::Schema(format!(
1503 "edge '{}' references unknown from node '{}'",
1504 edge.label.as_str(),
1505 edge.from.as_str()
1506 ))
1507 })?;
1508 let to_label = lookup(&edge.to).ok_or_else(|| {
1509 GrustError::Schema(format!(
1510 "edge '{}' references unknown to node '{}'",
1511 edge.label.as_str(),
1512 edge.to.as_str()
1513 ))
1514 })?;
1515
1516 let from_matches =
1517 |label: &Label| edge_type.from.is_empty() || edge_type.from.contains(label);
1518 let to_matches = |label: &Label| edge_type.to.is_empty() || edge_type.to.contains(label);
1519 let endpoints_ok = (from_matches(from_label) && to_matches(to_label))
1522 || (!edge_type.directed && from_matches(to_label) && to_matches(from_label));
1523 if !endpoints_ok {
1524 if !from_matches(from_label) {
1525 return Err(GrustError::Schema(format!(
1526 "edge '{}' cannot start from node label '{}'",
1527 edge.label.as_str(),
1528 from_label.as_str()
1529 )));
1530 }
1531 return Err(GrustError::Schema(format!(
1532 "edge '{}' cannot end at node label '{}'",
1533 edge.label.as_str(),
1534 to_label.as_str()
1535 )));
1536 }
1537
1538 validate_props(
1539 &edge.props,
1540 &edge_type.fields,
1541 &format!("edge '{}'", edge.label.as_str()),
1542 )
1543 }
1544
1545 pub fn validate_edge_props(&self, edge: &Edge) -> Result<()> {
1549 let edge_type = self.edge_type(&edge.label).ok_or_else(|| {
1550 GrustError::Schema(format!("schema has no edge type '{}'", edge.label.as_str()))
1551 })?;
1552 validate_props(
1553 &edge.props,
1554 &edge_type.fields,
1555 &format!("edge '{}'", edge.label.as_str()),
1556 )
1557 }
1558}
1559
1560#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1561pub struct NodeType {
1562 pub label: Label,
1563 pub fields: Vec<Field>,
1564}
1565
1566#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1567pub struct EdgeType {
1568 pub label: Label,
1569 pub from: Vec<Label>,
1570 pub to: Vec<Label>,
1571 pub fields: Vec<Field>,
1572 pub directed: bool,
1573 pub uniqueness: EdgeUniqueness,
1574}
1575
1576#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1577pub struct Field {
1578 pub name: String,
1579 pub ty: FieldType,
1580 pub required: bool,
1581}
1582
1583#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1584pub enum FieldType {
1585 String,
1586 Int,
1587 Float,
1588 Bool,
1589 DateTime,
1590 StringArray,
1591 IntArray,
1592 FloatArray,
1593 Json,
1594}
1595
1596#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1604pub enum EdgeUniqueness {
1605 None,
1606 FromTo,
1607 FromLabelTo,
1608}
1609
1610#[derive(Clone, Debug, Default)]
1611pub struct GraphSchemaBuilder {
1612 nodes: Vec<NodeType>,
1613 edges: Vec<EdgeType>,
1614}
1615
1616impl GraphSchemaBuilder {
1617 pub fn node(mut self, label: impl Into<Label>, fields: impl Into<Vec<Field>>) -> Self {
1618 self.nodes.push(NodeType {
1619 label: label.into(),
1620 fields: fields.into(),
1621 });
1622 self
1623 }
1624
1625 pub fn edge(
1626 mut self,
1627 label: impl Into<Label>,
1628 from: impl Into<Vec<Label>>,
1629 to: impl Into<Vec<Label>>,
1630 fields: impl Into<Vec<Field>>,
1631 ) -> Self {
1632 self.edges.push(EdgeType {
1633 label: label.into(),
1634 from: from.into(),
1635 to: to.into(),
1636 fields: fields.into(),
1637 directed: true,
1638 uniqueness: EdgeUniqueness::FromLabelTo,
1639 });
1640 self
1641 }
1642
1643 pub fn edge_type(mut self, edge_type: EdgeType) -> Self {
1644 self.edges.push(edge_type);
1645 self
1646 }
1647
1648 pub fn build(self) -> GraphSchema {
1649 GraphSchema {
1650 nodes: self.nodes,
1651 edges: self.edges,
1652 }
1653 }
1654}
1655
1656impl Field {
1657 pub fn required(name: impl Into<String>, ty: FieldType) -> Self {
1658 Self {
1659 name: name.into(),
1660 ty,
1661 required: true,
1662 }
1663 }
1664
1665 pub fn optional(name: impl Into<String>, ty: FieldType) -> Self {
1666 Self {
1667 name: name.into(),
1668 ty,
1669 required: false,
1670 }
1671 }
1672}
1673
1674fn validate_props(props: &Props, fields: &[Field], context: &str) -> Result<()> {
1675 for field in fields {
1676 match props.get(&field.name) {
1677 Some(value) => validate_field_value(value, &field.ty, context, &field.name)?,
1678 None if field.required => {
1679 return Err(GrustError::Schema(format!(
1680 "{context} missing required field '{}'",
1681 field.name
1682 )));
1683 }
1684 None => {}
1685 }
1686 }
1687 Ok(())
1688}
1689
1690fn validate_field_value(
1691 value: &Value,
1692 ty: &FieldType,
1693 context: &str,
1694 field_name: &str,
1695) -> Result<()> {
1696 let matches = match (value, ty) {
1697 (Value::String(_), FieldType::String)
1698 | (Value::Int(_), FieldType::Int)
1699 | (Value::Float(_), FieldType::Float)
1700 | (Value::Bool(_), FieldType::Bool)
1701 | (Value::DateTime(_), FieldType::DateTime)
1702 | (Value::StringArray(_), FieldType::StringArray)
1703 | (Value::IntArray(_), FieldType::IntArray)
1704 | (Value::FloatArray(_), FieldType::FloatArray)
1705 | (_, FieldType::Json) => true,
1706 (Value::String(value), FieldType::DateTime) => is_rfc3339_datetime(value),
1709 _ => false,
1710 };
1711 if matches {
1712 Ok(())
1713 } else {
1714 Err(GrustError::Schema(format!(
1715 "{context} field '{field_name}' expected {ty:?}, got {value:?}"
1716 )))
1717 }
1718}
1719
1720#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1721pub struct Traversal {
1722 pub start: Start,
1723 pub steps: Vec<Step>,
1724 pub limit: Option<u32>,
1725}
1726
1727impl Traversal {
1728 pub fn from_node(id: impl Into<NodeId>) -> Self {
1729 Self {
1730 start: Start::Node(id.into()),
1731 steps: Vec::new(),
1732 limit: None,
1733 }
1734 }
1735
1736 pub fn out(mut self, edge: impl Into<Label>) -> Self {
1737 self.steps.push(Step {
1738 direction: Direction::Out,
1739 edge: Some(edge.into()),
1740 node: None,
1741 });
1742 self
1743 }
1744
1745 pub fn in_(mut self, edge: impl Into<Label>) -> Self {
1746 self.steps.push(Step {
1747 direction: Direction::In,
1748 edge: Some(edge.into()),
1749 node: None,
1750 });
1751 self
1752 }
1753
1754 pub fn both(mut self, edge: impl Into<Label>) -> Self {
1755 self.steps.push(Step {
1756 direction: Direction::Both,
1757 edge: Some(edge.into()),
1758 node: None,
1759 });
1760 self
1761 }
1762
1763 pub fn to(mut self, node: impl Into<Label>) -> Self {
1770 let step = self
1771 .steps
1772 .last_mut()
1773 .expect("Traversal::to() must follow out(), in_(), or both()");
1774 step.node = Some(node.into());
1775 self
1776 }
1777
1778 pub fn limit(mut self, limit: u32) -> Self {
1779 self.limit = Some(limit);
1780 self
1781 }
1782}
1783
1784#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1785pub enum Start {
1786 Node(NodeId),
1787 NodesByLabel(Label),
1788 NodesByProperty {
1789 label: Label,
1790 key: String,
1791 value: Value,
1792 },
1793}
1794
1795#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1796pub struct Step {
1797 pub direction: Direction,
1798 pub edge: Option<Label>,
1799 pub node: Option<Label>,
1800}
1801
1802#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1803pub enum Direction {
1804 Out,
1805 In,
1806 Both,
1807}
1808
1809#[derive(Clone, Debug, Default, PartialEq)]
1810pub struct EdgeQuery {
1811 pub from: Option<NodeId>,
1812 pub to: Option<NodeId>,
1813 pub label: Option<Label>,
1814}
1815
1816#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
1818pub enum PutOutcome {
1819 Inserted,
1821 Updated,
1824 Upserted,
1827 Deduped,
1829}
1830
1831impl PutOutcome {
1832 pub fn written(self) -> bool {
1834 !matches!(self, Self::Deduped)
1835 }
1836}
1837
1838#[derive(Clone, Debug, Default, Eq, PartialEq)]
1841pub struct LoadReport {
1842 pub nodes: usize,
1843 pub edges: usize,
1844}
1845
1846#[async_trait]
1847pub trait GraphStore: Send + Sync {
1848 async fn apply_schema(&self, _schema: &GraphSchema) -> Result<()> {
1849 Ok(())
1850 }
1851
1852 async fn put_node(&self, node: &Node) -> Result<PutOutcome>;
1853 async fn put_edge(&self, edge: &Edge) -> Result<PutOutcome>;
1854
1855 async fn put_graph(&self, graph: &Graph) -> Result<LoadReport> {
1856 let mut report = LoadReport::default();
1857 for node in &graph.nodes {
1858 if self.put_node(node).await?.written() {
1859 report.nodes += 1;
1860 }
1861 }
1862 for edge in &graph.edges {
1863 if self.put_edge(edge).await?.written() {
1864 report.edges += 1;
1865 }
1866 }
1867 Ok(report)
1868 }
1869
1870 async fn put_typed_graph(&self, schema: &GraphSchema, graph: &Graph) -> Result<LoadReport> {
1871 schema.validate_graph(graph)?;
1872 self.apply_schema(schema).await?;
1873 self.put_graph(graph).await
1874 }
1875
1876 async fn get_node(&self, id: &NodeId) -> Result<Option<Node>>;
1877
1878 async fn get_nodes(&self, ids: &[NodeId]) -> Result<Vec<Node>> {
1885 let mut nodes = Vec::new();
1886 for id in ids {
1887 if let Some(node) = self.get_node(id).await? {
1888 nodes.push(node);
1889 }
1890 }
1891 Ok(nodes)
1892 }
1893
1894 async fn get_edges(&self, query: EdgeQuery) -> Result<Vec<Edge>>;
1895 async fn traverse(&self, traversal: Traversal) -> Result<Vec<Node>>;
1896}
1897
1898#[async_trait]
1899pub trait GraphAdminStore: GraphStore {
1900 async fn bootstrap(&self) -> Result<()> {
1901 Ok(())
1902 }
1903
1904 async fn clear(&self) -> Result<()>;
1905}
1906
1907#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1909pub enum GraphMutation {
1910 UpsertNode(Node),
1911 DeleteNode(NodeId),
1912 UpsertEdge(Edge),
1913 DeleteEdge {
1914 id: Option<EdgeId>,
1915 from: NodeId,
1916 label: Label,
1917 to: NodeId,
1918 },
1919}
1920
1921#[async_trait]
1926pub trait GraphMutationStore: GraphStore {
1927 async fn delete_node(&self, id: &NodeId) -> Result<()>;
1929
1930 async fn delete_edge(&self, from: &NodeId, label: &Label, to: &NodeId) -> Result<()>;
1932
1933 async fn apply_mutations(&self, mutations: &[GraphMutation]) -> Result<()> {
1940 for mutation in mutations {
1941 match mutation {
1942 GraphMutation::UpsertNode(node) => {
1943 self.put_node(node).await?;
1944 }
1945 GraphMutation::DeleteNode(id) => self.delete_node(id).await?,
1946 GraphMutation::UpsertEdge(edge) => {
1947 self.put_edge(edge).await?;
1948 }
1949 GraphMutation::DeleteEdge {
1950 from, label, to, ..
1951 } => self.delete_edge(from, label, to).await?,
1952 }
1953 }
1954 Ok(())
1955 }
1956}
1957
1958pub mod prelude {
1959 pub use crate::{
1960 Direction, Edge, EdgeId, EdgePolicy, EdgeQuery, EdgeType, EdgeUniqueness, Field, FieldType,
1961 Graph, GraphAdminStore, GraphBuilder, GraphMutation, GraphMutationStore, GraphSchema,
1962 GraphSchemaBuilder, GraphStore, GrustError, Label, LoadReport, Node, NodeId, NodeType,
1963 Props, PutOutcome, Result, Start, Step, Traversal, Value, edge_key, relationship_type,
1964 schema_identifier,
1965 };
1966
1967 #[cfg(feature = "typed-garde")]
1968 pub use crate::typed::{TypedEdge, TypedGraphBuilder, TypedNode, garde, props_from_serialize};
1969
1970 #[cfg(feature = "typed-zod-rs")]
1971 pub use crate::typed::{parse_typed_json, parse_typed_json_with, zod_rs};
1972}
1973
1974#[cfg(test)]
1975mod tests;