1use std::{
2 collections::{BTreeMap, BTreeSet, HashMap},
3 fmt,
4};
5
6use async_trait::async_trait;
7use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
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, Eq, PartialEq, Ord, PartialOrd, Hash)]
91pub struct RfcDate(String);
92
93impl RfcDate {
94 pub fn parse(value: impl Into<String>) -> Result<Self> {
97 let value = value.into();
98 if is_rfc3339_datetime(&value) {
99 Ok(Self(value))
100 } else {
101 Err(GrustError::Schema(format!(
102 "'{value}' is not an RFC 3339 date-time"
103 )))
104 }
105 }
106
107 pub fn as_str(&self) -> &str {
108 &self.0
109 }
110
111 pub fn into_string(self) -> String {
112 self.0
113 }
114}
115
116impl fmt::Display for RfcDate {
117 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
118 f.write_str(&self.0)
119 }
120}
121
122impl Serialize for RfcDate {
123 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
124 where
125 S: Serializer,
126 {
127 serializer.serialize_str(self.as_str())
128 }
129}
130
131impl<'de> Deserialize<'de> for RfcDate {
132 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
133 where
134 D: Deserializer<'de>,
135 {
136 let value = String::deserialize(deserializer)?;
137 Self::parse(value).map_err(de::Error::custom)
138 }
139}
140
141#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
142#[serde(tag = "type", content = "value", rename_all = "snake_case")]
143pub enum Value {
144 Null,
145 Bool(bool),
146 Int(i64),
147 Float(f64),
148 String(String),
149 DateTime(RfcDate),
152 StringArray(Vec<String>),
153 IntArray(Vec<i64>),
154 FloatArray(Vec<f64>),
155 Json(serde_json::Value),
156}
157
158impl Value {
159 pub fn datetime(value: impl Into<String>) -> Result<Self> {
163 RfcDate::parse(value).map(Self::DateTime)
164 }
165
166 pub fn as_str(&self) -> Option<&str> {
167 match self {
168 Self::String(value) => Some(value),
169 _ => None,
170 }
171 }
172
173 pub fn as_datetime(&self) -> Option<&str> {
174 match self {
175 Self::DateTime(value) => Some(value.as_str()),
176 _ => None,
177 }
178 }
179
180 pub fn as_string_array(&self) -> Option<&[String]> {
181 match self {
182 Self::StringArray(values) => Some(values),
183 _ => None,
184 }
185 }
186
187 pub fn as_int_array(&self) -> Option<&[i64]> {
188 match self {
189 Self::IntArray(values) => Some(values),
190 _ => None,
191 }
192 }
193
194 pub fn as_float_array(&self) -> Option<&[f64]> {
195 match self {
196 Self::FloatArray(values) => Some(values),
197 _ => None,
198 }
199 }
200
201 pub fn to_json(&self) -> serde_json::Value {
204 match self {
205 Self::Null => serde_json::Value::Null,
206 Self::Bool(value) => serde_json::Value::Bool(*value),
207 Self::Int(value) => serde_json::Value::from(*value),
208 Self::Float(value) => serde_json::Value::from(*value),
209 Self::String(value) => serde_json::Value::String(value.clone()),
210 Self::DateTime(value) => serde_json::Value::String(value.as_str().to_string()),
211 Self::StringArray(values) => serde_json::Value::from(values.clone()),
212 Self::IntArray(values) => serde_json::Value::from(values.clone()),
213 Self::FloatArray(values) => serde_json::Value::from(values.clone()),
214 Self::Json(value) => value.clone(),
215 }
216 }
217
218 pub fn from_json(value: serde_json::Value) -> Self {
222 if let serde_json::Value::Object(mapping) = &value
223 && mapping.contains_key("type")
224 && mapping.contains_key("value")
225 && let Ok(tagged) = serde_json::from_value(value.clone())
226 {
227 return tagged;
228 }
229 Self::from(value)
230 }
231}
232
233fn is_rfc3339_datetime(value: &str) -> bool {
235 let bytes = value.as_bytes();
236 if bytes.len() < 20 {
237 return false;
238 }
239 let digit = |i: usize| bytes[i].is_ascii_digit();
240 let all_digits = |range: std::ops::Range<usize>| range.clone().all(digit);
241 let pair = |i: usize| (bytes[i] - b'0') * 10 + (bytes[i + 1] - b'0');
242
243 if !(all_digits(0..4)
244 && bytes[4] == b'-'
245 && all_digits(5..7)
246 && bytes[7] == b'-'
247 && all_digits(8..10)
248 && bytes[10] == b'T'
249 && all_digits(11..13)
250 && bytes[13] == b':'
251 && all_digits(14..16)
252 && bytes[16] == b':'
253 && all_digits(17..19))
254 {
255 return false;
256 }
257 let year = bytes[0..4]
258 .iter()
259 .fold(0u16, |acc, digit| acc * 10 + u16::from(digit - b'0'));
260 let month = pair(5);
261 let day = pair(8);
262 let leap_year = (year % 4 == 0 && year % 100 != 0) || year % 400 == 0;
263 let max_day = match month {
264 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
265 4 | 6 | 9 | 11 => 30,
266 2 if leap_year => 29,
267 2 => 28,
268 _ => return false,
269 };
270 if !(day >= 1 && day <= max_day && pair(11) < 24 && pair(14) < 60 && pair(17) <= 60) {
271 return false;
272 }
273
274 let mut i = 19;
275 if bytes[i] == b'.' {
276 let start = i + 1;
277 i = start;
278 while i < bytes.len() && bytes[i].is_ascii_digit() {
279 i += 1;
280 }
281 if i == start {
282 return false;
283 }
284 }
285 match bytes.get(i) {
286 Some(b'Z') => i + 1 == bytes.len(),
287 Some(b'+' | b'-') => {
288 i + 6 == bytes.len()
289 && all_digits(i + 1..i + 3)
290 && bytes[i + 3] == b':'
291 && all_digits(i + 4..i + 6)
292 && pair(i + 1) < 24
293 && pair(i + 4) < 60
294 }
295 _ => false,
296 }
297}
298
299impl From<String> for Value {
300 fn from(value: String) -> Self {
301 Self::String(value)
302 }
303}
304
305impl From<&str> for Value {
306 fn from(value: &str) -> Self {
307 Self::String(value.to_string())
308 }
309}
310
311impl From<&String> for Value {
312 fn from(value: &String) -> Self {
313 Self::String(value.clone())
314 }
315}
316
317impl From<Vec<String>> for Value {
318 fn from(value: Vec<String>) -> Self {
319 Self::StringArray(value)
320 }
321}
322
323impl From<Vec<i64>> for Value {
324 fn from(value: Vec<i64>) -> Self {
325 Self::IntArray(value)
326 }
327}
328
329impl From<Vec<f64>> for Value {
330 fn from(value: Vec<f64>) -> Self {
331 Self::FloatArray(value)
332 }
333}
334
335impl From<bool> for Value {
336 fn from(value: bool) -> Self {
337 Self::Bool(value)
338 }
339}
340
341impl From<i64> for Value {
342 fn from(value: i64) -> Self {
343 Self::Int(value)
344 }
345}
346
347impl From<i32> for Value {
348 fn from(value: i32) -> Self {
349 Self::Int(i64::from(value))
350 }
351}
352
353impl From<usize> for Value {
354 fn from(value: usize) -> Self {
355 Self::Int(value as i64)
356 }
357}
358
359impl From<f64> for Value {
360 fn from(value: f64) -> Self {
361 Self::Float(value)
362 }
363}
364
365impl From<serde_json::Value> for Value {
366 fn from(value: serde_json::Value) -> Self {
367 match value {
368 serde_json::Value::Null => Self::Null,
369 serde_json::Value::Bool(value) => Self::Bool(value),
370 serde_json::Value::Number(value) => {
371 if let Some(value) = value.as_i64() {
372 Self::Int(value)
373 } else if let Some(value) = value.as_f64() {
374 Self::Float(value)
375 } else {
376 Self::Json(serde_json::Value::Number(value))
377 }
378 }
379 serde_json::Value::String(value) => Self::String(value),
380 serde_json::Value::Array(values) => {
381 let ints = values
382 .iter()
383 .filter_map(serde_json::Value::as_i64)
384 .collect::<Vec<_>>();
385 if !values.is_empty() && ints.len() == values.len() {
386 return Self::IntArray(ints);
387 }
388 let floats = values
389 .iter()
390 .filter_map(serde_json::Value::as_f64)
391 .collect::<Vec<_>>();
392 if !values.is_empty() && floats.len() == values.len() {
393 return Self::FloatArray(floats);
394 }
395 let strings = values
396 .iter()
397 .filter_map(|value| value.as_str().map(ToString::to_string))
398 .collect::<Vec<_>>();
399 if strings.len() == values.len() {
400 Self::StringArray(strings)
401 } else {
402 Self::Json(serde_json::Value::Array(values))
403 }
404 }
405 serde_json::Value::Object(value) => Self::Json(serde_json::Value::Object(value)),
406 }
407 }
408}
409
410#[cfg(feature = "typed-garde")]
411pub mod typed {
412 use serde::{Serialize, de::DeserializeOwned};
413
414 use crate::{
415 Edge, EdgeId, Graph, GraphBuilder, GrustError, Node, NodeId, Props, PutOutcome, Result,
416 Value,
417 };
418
419 pub use garde;
420 #[cfg(feature = "typed-zod-rs")]
421 pub use zod_rs;
422
423 pub trait TypedNode: garde::Validate + Serialize {
424 const LABEL: &'static str;
425
426 fn node_id(&self) -> NodeId;
427
428 fn node_props(&self) -> Result<Props> {
429 props_from_serialize(self)
430 }
431
432 fn from_node(node: &Node) -> Result<Self>
433 where
434 Self: Sized + DeserializeOwned,
435 Self::Context: Default,
436 {
437 let ctx = Self::Context::default();
438 Self::from_node_with(node, &ctx)
439 }
440
441 fn from_node_with(node: &Node, ctx: &Self::Context) -> Result<Self>
442 where
443 Self: Sized + DeserializeOwned,
444 {
445 if node.label.as_str() != Self::LABEL {
446 return Err(GrustError::Schema(format!(
447 "node '{}' has label '{}', expected '{}'",
448 node.id.as_str(),
449 node.label.as_str(),
450 Self::LABEL
451 )));
452 }
453 let typed: Self =
454 serde_json::from_value(props_to_plain_json(&node.props)).map_err(|err| {
455 GrustError::Serialization(format!("typed node decode error: {err}"))
456 })?;
457 typed
458 .validate_with(ctx)
459 .map_err(|err| validation_error(Self::LABEL, err))?;
460 let typed_id = typed.node_id();
461 if typed_id != node.id {
462 return Err(GrustError::Schema(format!(
463 "typed node '{}' decoded id '{}', expected '{}'",
464 Self::LABEL,
465 typed_id.as_str(),
466 node.id.as_str()
467 )));
468 }
469 Ok(typed)
470 }
471 }
472
473 pub trait TypedEdge: garde::Validate + Serialize {
474 const LABEL: &'static str;
475
476 fn source_node_id(&self) -> NodeId;
477
478 fn target_node_id(&self) -> NodeId;
479
480 fn edge_id(&self) -> Option<EdgeId> {
481 None
482 }
483
484 fn edge_props(&self) -> Result<Props> {
485 props_from_serialize(self)
486 }
487
488 fn from_edge(edge: &Edge) -> Result<Self>
489 where
490 Self: Sized + DeserializeOwned,
491 Self::Context: Default,
492 {
493 let ctx = Self::Context::default();
494 Self::from_edge_with(edge, &ctx)
495 }
496
497 fn from_edge_with(edge: &Edge, ctx: &Self::Context) -> Result<Self>
498 where
499 Self: Sized + DeserializeOwned,
500 {
501 if edge.label.as_str() != Self::LABEL {
502 return Err(GrustError::Schema(format!(
503 "edge from '{}' to '{}' has label '{}', expected '{}'",
504 edge.from.as_str(),
505 edge.to.as_str(),
506 edge.label.as_str(),
507 Self::LABEL
508 )));
509 }
510 let typed: Self =
511 serde_json::from_value(props_to_plain_json(&edge.props)).map_err(|err| {
512 GrustError::Serialization(format!("typed edge decode error: {err}"))
513 })?;
514 typed
515 .validate_with(ctx)
516 .map_err(|err| validation_error(Self::LABEL, err))?;
517 if typed.source_node_id() != edge.from || typed.target_node_id() != edge.to {
518 return Err(GrustError::Schema(format!(
519 "typed edge '{}' decoded endpoints '{}' -> '{}', expected '{}' -> '{}'",
520 Self::LABEL,
521 typed.source_node_id().as_str(),
522 typed.target_node_id().as_str(),
523 edge.from.as_str(),
524 edge.to.as_str()
525 )));
526 }
527 if let Some(decoded_id) = typed.edge_id()
528 && edge
529 .id
530 .as_ref()
531 .is_some_and(|edge_id| edge_id != &decoded_id)
532 {
533 return Err(GrustError::Schema(format!(
534 "typed edge '{}' decoded id '{}', expected '{}'",
535 Self::LABEL,
536 decoded_id.as_str(),
537 edge.id.as_ref().expect("edge id checked").as_str()
538 )));
539 }
540 Ok(typed)
541 }
542 }
543
544 #[derive(Clone, Debug, Default)]
545 pub struct TypedGraphBuilder {
546 builder: GraphBuilder,
547 }
548
549 impl TypedGraphBuilder {
550 pub fn new() -> Self {
551 Self::default()
552 }
553
554 pub fn from_builder(builder: GraphBuilder) -> Self {
555 Self { builder }
556 }
557
558 pub fn from_graph(graph: Graph) -> Self {
559 let mut builder = GraphBuilder::new();
560 for node in graph.nodes {
561 builder.add_node(node);
562 }
563 for edge in graph.edges {
564 builder.add_edge(edge);
565 }
566 Self { builder }
567 }
568
569 pub fn add_raw_node(&mut self, node: Node) -> NodeId {
570 self.builder.add_node(node)
571 }
572
573 pub fn add_raw_edge(&mut self, edge: Edge) -> PutOutcome {
574 self.builder.add_edge(edge)
575 }
576
577 pub fn add_node<T>(&mut self, node: &T) -> Result<NodeId>
578 where
579 T: TypedNode,
580 T::Context: Default,
581 {
582 node.validate()
583 .map_err(|err| validation_error(T::LABEL, err))?;
584 self.add_validated_node(node)
585 }
586
587 pub fn add_node_with<T>(&mut self, node: &T, ctx: &T::Context) -> Result<NodeId>
588 where
589 T: TypedNode,
590 {
591 node.validate_with(ctx)
592 .map_err(|err| validation_error(T::LABEL, err))?;
593 self.add_validated_node(node)
594 }
595
596 pub fn add_edge<T>(&mut self, edge: &T) -> Result<PutOutcome>
597 where
598 T: TypedEdge,
599 T::Context: Default,
600 {
601 edge.validate()
602 .map_err(|err| validation_error(T::LABEL, err))?;
603 self.add_validated_edge(edge)
604 }
605
606 pub fn add_edge_with<T>(&mut self, edge: &T, ctx: &T::Context) -> Result<PutOutcome>
607 where
608 T: TypedEdge,
609 {
610 edge.validate_with(ctx)
611 .map_err(|err| validation_error(T::LABEL, err))?;
612 self.add_validated_edge(edge)
613 }
614
615 #[cfg(feature = "typed-zod-rs")]
616 pub fn add_node_from_json<T, S>(
617 &mut self,
618 schema: &S,
619 value: &serde_json::Value,
620 ) -> Result<NodeId>
621 where
622 T: TypedNode + DeserializeOwned,
623 T::Context: Default,
624 S: zod_rs::Schema<serde_json::Value>,
625 {
626 let node = parse_typed_json::<T, S>(schema, value)?;
627 self.add_validated_node(&node)
628 }
629
630 #[cfg(feature = "typed-zod-rs")]
631 pub fn add_node_from_json_with<T, S>(
632 &mut self,
633 schema: &S,
634 value: &serde_json::Value,
635 ctx: &T::Context,
636 ) -> Result<NodeId>
637 where
638 T: TypedNode + DeserializeOwned,
639 S: zod_rs::Schema<serde_json::Value>,
640 {
641 let node = parse_typed_json_with::<T, S>(schema, value, ctx)?;
642 self.add_validated_node(&node)
643 }
644
645 #[cfg(feature = "typed-zod-rs")]
646 pub fn add_edge_from_json<T, S>(
647 &mut self,
648 schema: &S,
649 value: &serde_json::Value,
650 ) -> Result<PutOutcome>
651 where
652 T: TypedEdge + DeserializeOwned,
653 T::Context: Default,
654 S: zod_rs::Schema<serde_json::Value>,
655 {
656 let edge = parse_typed_json::<T, S>(schema, value)?;
657 self.add_validated_edge(&edge)
658 }
659
660 #[cfg(feature = "typed-zod-rs")]
661 pub fn add_edge_from_json_with<T, S>(
662 &mut self,
663 schema: &S,
664 value: &serde_json::Value,
665 ctx: &T::Context,
666 ) -> Result<PutOutcome>
667 where
668 T: TypedEdge + DeserializeOwned,
669 S: zod_rs::Schema<serde_json::Value>,
670 {
671 let edge = parse_typed_json_with::<T, S>(schema, value, ctx)?;
672 self.add_validated_edge(&edge)
673 }
674
675 #[must_use = "discarding this means the typed graph was not built"]
676 pub fn build(self) -> Graph {
677 self.builder.build()
678 }
679
680 pub fn into_builder(self) -> GraphBuilder {
681 self.builder
682 }
683
684 fn add_validated_node<T>(&mut self, node: &T) -> Result<NodeId>
685 where
686 T: TypedNode,
687 {
688 let node_id = node.node_id();
689 let mut props = node.node_props()?;
690 props
691 .entry("id".to_string())
692 .or_insert_with(|| Value::from(node_id.as_str()));
693 let graph_node = Node::new(T::LABEL, node_id, props);
694 Ok(self.builder.add_node(graph_node))
695 }
696
697 fn add_validated_edge<T>(&mut self, edge: &T) -> Result<PutOutcome>
698 where
699 T: TypedEdge,
700 {
701 let mut graph_edge = Edge::new(
702 T::LABEL,
703 edge.source_node_id(),
704 edge.target_node_id(),
705 edge.edge_props()?,
706 );
707 graph_edge.id = edge.edge_id();
708 Ok(self.builder.add_edge(graph_edge))
709 }
710 }
711
712 pub fn props_from_serialize<T>(value: &T) -> Result<Props>
713 where
714 T: Serialize + ?Sized,
715 {
716 let serialized = serde_json::to_value(value)
717 .map_err(|err| GrustError::Serialization(format!("typed props error: {err}")))?;
718 let serde_json::Value::Object(fields) = serialized else {
719 return Err(GrustError::Schema(
720 "typed graph values must serialize as JSON objects".to_string(),
721 ));
722 };
723
724 Ok(fields
725 .into_iter()
726 .map(|(key, value)| (key, Value::from(value)))
727 .collect())
728 }
729
730 fn props_to_plain_json(props: &Props) -> serde_json::Value {
731 serde_json::Value::Object(
732 props
733 .iter()
734 .map(|(key, value)| (key.clone(), value.to_json()))
735 .collect(),
736 )
737 }
738
739 #[cfg(feature = "typed-zod-rs")]
740 pub fn parse_typed_json<T, S>(schema: &S, value: &serde_json::Value) -> Result<T>
741 where
742 T: DeserializeOwned + garde::Validate,
743 T::Context: Default,
744 S: zod_rs::Schema<serde_json::Value>,
745 {
746 let ctx = T::Context::default();
747 parse_typed_json_with(schema, value, &ctx)
748 }
749
750 #[cfg(feature = "typed-zod-rs")]
751 pub fn parse_typed_json_with<T, S>(
752 schema: &S,
753 value: &serde_json::Value,
754 ctx: &T::Context,
755 ) -> Result<T>
756 where
757 T: DeserializeOwned + garde::Validate,
758 S: zod_rs::Schema<serde_json::Value>,
759 {
760 schema
761 .safe_parse(value)
762 .map_err(|err| GrustError::Schema(format!("zod-rs validation failed: {err}")))?;
763 let typed: T = serde_json::from_value(value.clone())
764 .map_err(|err| GrustError::Serialization(format!("typed JSON decode error: {err}")))?;
765 typed
766 .validate_with(ctx)
767 .map_err(|err| GrustError::Schema(format!("typed validation failed: {err}")))?;
768 Ok(typed)
769 }
770
771 fn validation_error(label: &str, err: garde::Report) -> GrustError {
772 GrustError::Schema(format!("{label} validation failed: {err}"))
773 }
774}
775
776#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
777pub struct Node {
778 pub id: NodeId,
779 pub label: Label,
780 pub props: Props,
781}
782
783impl Node {
784 pub fn new(label: impl Into<Label>, id: impl Into<NodeId>, props: impl Into<Props>) -> Self {
785 let id = id.into();
786 let mut props = props.into();
787 props
788 .entry("id".to_string())
789 .or_insert_with(|| Value::from(id.as_str()));
790 Self {
791 id,
792 label: label.into(),
793 props,
794 }
795 }
796}
797
798#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
799pub struct Edge {
800 pub id: Option<EdgeId>,
801 pub from: NodeId,
802 pub to: NodeId,
803 pub label: Label,
804 pub props: Props,
805}
806
807impl Edge {
808 pub fn new(
809 label: impl Into<Label>,
810 from: impl Into<NodeId>,
811 to: impl Into<NodeId>,
812 props: impl Into<Props>,
813 ) -> Self {
814 Self {
815 id: None,
816 from: from.into(),
817 to: to.into(),
818 label: label.into(),
819 props: props.into(),
820 }
821 }
822
823 pub fn with_id(mut self, id: impl Into<EdgeId>) -> Self {
824 self.id = Some(id.into());
825 self
826 }
827}
828
829pub fn relationship_type(value: &str) -> String {
834 let relationship = value
835 .chars()
836 .map(|ch| {
837 if ch.is_ascii_alphanumeric() {
838 ch.to_ascii_uppercase()
839 } else {
840 '_'
841 }
842 })
843 .collect::<String>();
844 if relationship.is_empty() {
845 "RELATED_TO".to_string()
846 } else {
847 relationship
848 }
849}
850
851pub fn schema_identifier(value: &str) -> Result<String> {
856 let identifier = value
857 .chars()
858 .map(|ch| {
859 if ch.is_ascii_alphanumeric() {
860 ch.to_ascii_lowercase()
861 } else {
862 '_'
863 }
864 })
865 .collect::<String>();
866 if identifier.is_empty()
867 || identifier
868 .chars()
869 .next()
870 .is_some_and(|ch| ch.is_ascii_digit())
871 {
872 return Err(GrustError::Schema(format!(
873 "invalid schema identifier '{value}'"
874 )));
875 }
876 Ok(identifier)
877}
878
879pub fn edge_key(edge: &Edge) -> String {
885 edge.id
886 .as_ref()
887 .map(EdgeId::as_str)
888 .map(ToString::to_string)
889 .unwrap_or_else(|| {
890 let from = edge.from.as_str();
891 let label = edge.label.as_str();
892 let to = edge.to.as_str();
893 let mut key = String::with_capacity(from.len() + label.len() + to.len() + 2);
894 key.push_str(from);
895 key.push('\u{1f}');
896 key.push_str(label);
897 key.push('\u{1f}');
898 key.push_str(to);
899 key
900 })
901}
902
903#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
904pub struct Graph {
905 pub nodes: Vec<Node>,
906 pub edges: Vec<Edge>,
907}
908
909impl Graph {
910 pub fn new(nodes: Vec<Node>, edges: Vec<Edge>) -> Self {
911 Self { nodes, edges }
912 }
913
914 pub fn from_yaml(yaml: &str) -> Result<Self> {
915 yaml::graph_from_yaml(yaml)
916 }
917
918 pub fn to_yaml(&self) -> Result<String> {
919 yaml::graph_to_yaml(self)
920 }
921
922 pub fn from_json(json: &str) -> Result<Self> {
923 json::graph_from_json(json)
924 }
925
926 pub fn to_json(&self) -> Result<String> {
927 json::graph_to_json(self)
928 }
929
930 pub fn from_xml(xml: &str) -> Result<Self> {
931 xml::graph_from_xml(xml)
932 }
933
934 pub fn to_xml(&self) -> Result<String> {
935 xml::graph_to_xml(self)
936 }
937
938 pub fn builder() -> GraphBuilder {
939 GraphBuilder::new()
940 }
941}
942
943#[derive(Clone, Debug, PartialEq)]
950pub struct GraphIndex {
951 vertex_by_id: HashMap<NodeId, usize>,
952 outgoing_by_vertex: Vec<Vec<usize>>,
953 incoming_by_vertex: Vec<Vec<usize>>,
954 edge_endpoints: Vec<(usize, usize)>,
955}
956
957impl GraphIndex {
958 pub fn new(graph: &Graph) -> Result<Self> {
959 let mut vertex_by_id = HashMap::with_capacity(graph.nodes.len());
960 for (index, vertex) in graph.nodes.iter().enumerate() {
961 if vertex_by_id.insert(vertex.id.clone(), index).is_some() {
962 return Err(GrustError::Schema(format!(
963 "duplicate vertex id '{}'",
964 vertex.id.as_str()
965 )));
966 }
967 }
968
969 let mut outgoing_by_vertex = vec![Vec::<usize>::new(); graph.nodes.len()];
970 let mut incoming_by_vertex = vec![Vec::<usize>::new(); graph.nodes.len()];
971 let mut edge_endpoints = Vec::<(usize, usize)>::with_capacity(graph.edges.len());
972
973 for (edge_index, edge) in graph.edges.iter().enumerate() {
974 let Some(&from_index) = vertex_by_id.get(&edge.from) else {
975 return Err(GrustError::Schema(format!(
976 "edge source '{}' is not present in vertices",
977 edge.from.as_str()
978 )));
979 };
980 let Some(&to_index) = vertex_by_id.get(&edge.to) else {
981 return Err(GrustError::Schema(format!(
982 "edge destination '{}' is not present in vertices",
983 edge.to.as_str()
984 )));
985 };
986
987 outgoing_by_vertex[from_index].push(edge_index);
988 incoming_by_vertex[to_index].push(edge_index);
989 edge_endpoints.push((from_index, to_index));
990 }
991
992 Ok(Self {
993 vertex_by_id,
994 outgoing_by_vertex,
995 incoming_by_vertex,
996 edge_endpoints,
997 })
998 }
999
1000 pub fn vertex_index(&self, id: &NodeId) -> Option<usize> {
1001 self.vertex_by_id.get(id).copied()
1002 }
1003
1004 pub fn require_vertex_index(&self, id: &NodeId) -> Result<usize> {
1005 self.vertex_index(id)
1006 .ok_or_else(|| GrustError::Schema(format!("vertex '{}' is not present", id.as_str())))
1007 }
1008
1009 pub fn outgoing_edges(&self, id: &NodeId) -> &[usize] {
1010 self.vertex_index(id)
1011 .map(|index| self.outgoing_by_vertex(index))
1012 .unwrap_or(&[])
1013 }
1014
1015 pub fn incoming_edges(&self, id: &NodeId) -> &[usize] {
1016 self.vertex_index(id)
1017 .map(|index| self.incoming_by_vertex(index))
1018 .unwrap_or(&[])
1019 }
1020
1021 pub fn outgoing_by_vertex(&self, index: usize) -> &[usize] {
1022 self.outgoing_by_vertex
1023 .get(index)
1024 .map(Vec::as_slice)
1025 .unwrap_or(&[])
1026 }
1027
1028 pub fn incoming_by_vertex(&self, index: usize) -> &[usize] {
1029 self.incoming_by_vertex
1030 .get(index)
1031 .map(Vec::as_slice)
1032 .unwrap_or(&[])
1033 }
1034
1035 pub fn edge_endpoints(&self, edge_index: usize) -> (usize, usize) {
1036 self.edge_endpoints[edge_index]
1037 }
1038
1039 pub fn edge_endpoints_slice(&self) -> &[(usize, usize)] {
1040 &self.edge_endpoints
1041 }
1042
1043 pub fn out_degree(&self, index: usize) -> usize {
1044 self.outgoing_by_vertex[index].len()
1045 }
1046
1047 pub fn in_degree(&self, index: usize) -> usize {
1048 self.incoming_by_vertex[index].len()
1049 }
1050
1051 pub fn degree(&self, index: usize) -> usize {
1052 self.in_degree(index) + self.out_degree(index)
1053 }
1054}
1055
1056mod graph_doc {
1057 use std::collections::{BTreeMap, BTreeSet};
1058
1059 use serde::{Deserialize, Serialize};
1060
1061 use crate::{Edge, EdgeId, Graph, GrustError, Label, Node, NodeId, Props, Value};
1062
1063 #[derive(Debug, Serialize, Deserialize)]
1064 pub(super) struct GraphDoc {
1065 #[serde(default)]
1066 pub(super) nodes: Vec<NodeDoc>,
1067 #[serde(default)]
1068 pub(super) edges: Vec<EdgeDoc>,
1069 }
1070
1071 #[derive(Debug, Serialize, Deserialize)]
1072 pub(super) struct NodeDoc {
1073 pub(super) id: NodeId,
1074 pub(super) label: Label,
1075 #[serde(default, deserialize_with = "deserialize_props")]
1076 pub(super) props: Props,
1077 }
1078
1079 #[derive(Debug, Serialize, Deserialize)]
1080 pub(super) struct NodeDocOut {
1081 pub(super) id: NodeId,
1082 pub(super) label: Label,
1083 #[serde(default)]
1084 pub(super) props: Props,
1085 }
1086
1087 #[derive(Debug, Serialize, Deserialize)]
1088 pub(super) struct EdgeDoc {
1089 #[serde(default)]
1090 pub(super) id: Option<EdgeId>,
1091 pub(super) label: Label,
1092 pub(super) from: NodeId,
1093 pub(super) to: NodeId,
1094 #[serde(default, deserialize_with = "deserialize_props")]
1095 pub(super) props: Props,
1096 }
1097
1098 #[derive(Debug, Serialize, Deserialize)]
1099 pub(super) struct EdgeDocOut {
1100 #[serde(default)]
1101 pub(super) id: Option<EdgeId>,
1102 pub(super) label: Label,
1103 pub(super) from: NodeId,
1104 pub(super) to: NodeId,
1105 #[serde(default)]
1106 pub(super) props: Props,
1107 }
1108
1109 pub(super) fn graph_from_doc(doc: GraphDoc) -> super::Result<Graph> {
1110 let mut ids = BTreeSet::new();
1111 for node in &doc.nodes {
1112 if !ids.insert(node.id.clone()) {
1113 return Err(GrustError::Schema(format!(
1114 "duplicate node id '{}'",
1115 node.id
1116 )));
1117 }
1118 }
1119
1120 let mut edges = Vec::with_capacity(doc.edges.len());
1121 for edge in doc.edges {
1122 if !ids.contains(&edge.from) {
1123 return Err(GrustError::Schema(format!(
1124 "edge '{}' references unknown from node '{}'",
1125 edge.label, edge.from
1126 )));
1127 }
1128 if !ids.contains(&edge.to) {
1129 return Err(GrustError::Schema(format!(
1130 "edge '{}' references unknown to node '{}'",
1131 edge.label, edge.to
1132 )));
1133 }
1134
1135 let mut graph_edge = Edge::new(edge.label, edge.from, edge.to, edge.props);
1136 graph_edge.id = edge.id;
1137 edges.push(graph_edge);
1138 }
1139
1140 let nodes = doc
1141 .nodes
1142 .into_iter()
1143 .map(|node| Node::new(node.label, node.id, node.props))
1144 .collect();
1145
1146 Ok(Graph::new(nodes, edges))
1147 }
1148
1149 pub(super) fn graph_to_doc(graph: &Graph) -> GraphDocOut {
1150 GraphDocOut {
1151 nodes: graph
1152 .nodes
1153 .iter()
1154 .map(|node| NodeDocOut {
1155 id: node.id.clone(),
1156 label: node.label.clone(),
1157 props: without_generated_id(&node.props, &node.id),
1158 })
1159 .collect(),
1160 edges: graph
1161 .edges
1162 .iter()
1163 .map(|edge| EdgeDocOut {
1164 id: edge.id.clone(),
1165 label: edge.label.clone(),
1166 from: edge.from.clone(),
1167 to: edge.to.clone(),
1168 props: edge.props.clone(),
1169 })
1170 .collect(),
1171 }
1172 }
1173
1174 fn without_generated_id(props: &Props, id: &NodeId) -> Props {
1175 let mut props = props.clone();
1176 if props.get("id") == Some(&Value::from(id.as_str())) {
1177 props.remove("id");
1178 }
1179 props
1180 }
1181
1182 fn deserialize_props<'de, D>(deserializer: D) -> std::result::Result<Props, D::Error>
1183 where
1184 D: serde::Deserializer<'de>,
1185 {
1186 let raw = BTreeMap::<String, serde_json::Value>::deserialize(deserializer)?;
1187 raw.into_iter()
1188 .map(|(key, value)| {
1189 value_from_json(value)
1190 .map(|value| (key, value))
1191 .map_err(serde::de::Error::custom)
1192 })
1193 .collect()
1194 }
1195
1196 fn value_from_json(value: serde_json::Value) -> std::result::Result<Value, String> {
1197 if let serde_json::Value::Object(mapping) = &value
1198 && mapping.contains_key("type")
1199 && mapping.contains_key("value")
1200 {
1201 return serde_json::from_value(value)
1202 .map_err(|err| format!("invalid tagged Grust value: {err}"));
1203 }
1204
1205 Ok(Value::from_json(value))
1206 }
1207
1208 #[derive(Debug, Serialize, Deserialize)]
1209 pub(super) struct GraphDocOut {
1210 pub(super) nodes: Vec<NodeDocOut>,
1211 pub(super) edges: Vec<EdgeDocOut>,
1212 }
1213}
1214
1215mod yaml {
1216 use crate::{Graph, GrustError};
1217
1218 pub(super) fn graph_from_yaml(yaml: &str) -> super::Result<Graph> {
1219 let doc: super::graph_doc::GraphDoc = serde_yaml::from_str(yaml)
1220 .map_err(|err| GrustError::Serialization(format!("YAML parse error: {err}")))?;
1221 super::graph_doc::graph_from_doc(doc)
1222 }
1223
1224 pub(super) fn graph_to_yaml(graph: &Graph) -> super::Result<String> {
1225 serde_yaml::to_string(&super::graph_doc::graph_to_doc(graph))
1226 .map_err(|err| GrustError::Serialization(format!("YAML serialization error: {err}")))
1227 }
1228}
1229
1230mod json {
1231 use crate::{Graph, GrustError};
1232
1233 pub(super) fn graph_from_json(json: &str) -> super::Result<Graph> {
1234 let doc: super::graph_doc::GraphDoc = serde_json::from_str(json)
1235 .map_err(|err| GrustError::Serialization(format!("JSON parse error: {err}")))?;
1236 super::graph_doc::graph_from_doc(doc)
1237 }
1238
1239 pub(super) fn graph_to_json(graph: &Graph) -> super::Result<String> {
1240 serde_json::to_string_pretty(&super::graph_doc::graph_to_doc(graph))
1241 .map_err(|err| GrustError::Serialization(format!("JSON serialization error: {err}")))
1242 }
1243}
1244
1245mod xml {
1246 use serde::{Deserialize, Serialize};
1247
1248 use crate::{Edge, EdgeId, Graph, GrustError, Label, Node, NodeId, Props, Value};
1249
1250 #[derive(Debug, Serialize, Deserialize)]
1251 #[serde(rename = "graph")]
1252 struct GraphXml {
1253 #[serde(default)]
1254 nodes: NodesXml,
1255 #[serde(default)]
1256 edges: EdgesXml,
1257 }
1258
1259 #[derive(Debug, Default, Serialize, Deserialize)]
1260 struct NodesXml {
1261 #[serde(rename = "node", default)]
1262 items: Vec<NodeXml>,
1263 }
1264
1265 #[derive(Debug, Default, Serialize, Deserialize)]
1266 struct EdgesXml {
1267 #[serde(rename = "edge", default)]
1268 items: Vec<EdgeXml>,
1269 }
1270
1271 #[derive(Debug, Serialize, Deserialize)]
1272 struct NodeXml {
1273 id: NodeId,
1274 label: Label,
1275 #[serde(default)]
1276 props: PropsXml,
1277 }
1278
1279 #[derive(Debug, Serialize, Deserialize)]
1280 struct EdgeXml {
1281 #[serde(default, skip_serializing_if = "Option::is_none")]
1282 id: Option<EdgeId>,
1283 label: Label,
1284 from: NodeId,
1285 to: NodeId,
1286 #[serde(default)]
1287 props: PropsXml,
1288 }
1289
1290 #[derive(Debug, Default, Serialize, Deserialize)]
1291 struct PropsXml {
1292 #[serde(rename = "prop", default)]
1293 items: Vec<PropXml>,
1294 }
1295
1296 #[derive(Debug, Serialize, Deserialize)]
1297 struct PropXml {
1298 key: String,
1299 value: Value,
1300 }
1301
1302 pub(super) fn graph_from_xml(xml: &str) -> super::Result<Graph> {
1303 let doc: GraphXml = quick_xml::de::from_str(xml)
1304 .map_err(|err| GrustError::Serialization(format!("XML parse error: {err}")))?;
1305 super::graph_doc::graph_from_doc(doc.into())
1306 }
1307
1308 pub(super) fn graph_to_xml(graph: &Graph) -> super::Result<String> {
1309 quick_xml::se::to_string(&GraphXml::from(graph))
1310 .map_err(|err| GrustError::Serialization(format!("XML serialization error: {err}")))
1311 }
1312
1313 impl From<GraphXml> for super::graph_doc::GraphDoc {
1314 fn from(value: GraphXml) -> Self {
1315 Self {
1316 nodes: value.nodes.items.into_iter().map(Into::into).collect(),
1317 edges: value.edges.items.into_iter().map(Into::into).collect(),
1318 }
1319 }
1320 }
1321
1322 impl From<NodeXml> for super::graph_doc::NodeDoc {
1323 fn from(value: NodeXml) -> Self {
1324 Self {
1325 id: value.id,
1326 label: value.label,
1327 props: value.props.into(),
1328 }
1329 }
1330 }
1331
1332 impl From<EdgeXml> for super::graph_doc::EdgeDoc {
1333 fn from(value: EdgeXml) -> Self {
1334 Self {
1335 id: value.id,
1336 label: value.label,
1337 from: value.from,
1338 to: value.to,
1339 props: value.props.into(),
1340 }
1341 }
1342 }
1343
1344 impl From<PropsXml> for Props {
1345 fn from(value: PropsXml) -> Self {
1346 value
1347 .items
1348 .into_iter()
1349 .map(|prop| (prop.key, prop.value))
1350 .collect()
1351 }
1352 }
1353
1354 impl From<&Graph> for GraphXml {
1355 fn from(graph: &Graph) -> Self {
1356 Self {
1357 nodes: NodesXml {
1358 items: graph.nodes.iter().map(NodeXml::from).collect(),
1359 },
1360 edges: EdgesXml {
1361 items: graph.edges.iter().map(EdgeXml::from).collect(),
1362 },
1363 }
1364 }
1365 }
1366
1367 impl From<&Node> for NodeXml {
1368 fn from(node: &Node) -> Self {
1369 let props = super::graph_doc::graph_to_doc(&Graph::new(vec![node.clone()], Vec::new()))
1370 .nodes
1371 .into_iter()
1372 .next()
1373 .expect("node exists")
1374 .props;
1375 Self {
1376 id: node.id.clone(),
1377 label: node.label.clone(),
1378 props: props.into(),
1379 }
1380 }
1381 }
1382
1383 impl From<&Edge> for EdgeXml {
1384 fn from(edge: &Edge) -> Self {
1385 Self {
1386 id: edge.id.clone(),
1387 label: edge.label.clone(),
1388 from: edge.from.clone(),
1389 to: edge.to.clone(),
1390 props: edge.props.clone().into(),
1391 }
1392 }
1393 }
1394
1395 impl From<Props> for PropsXml {
1396 fn from(value: Props) -> Self {
1397 Self {
1398 items: value
1399 .into_iter()
1400 .map(|(key, value)| PropXml { key, value })
1401 .collect(),
1402 }
1403 }
1404 }
1405}
1406
1407#[derive(Clone, Debug, Default, Eq, PartialEq)]
1408pub enum EdgePolicy {
1409 AllowDuplicates,
1410 #[default]
1411 DedupeByFromLabelTo,
1412}
1413
1414#[derive(Clone, Debug, Default)]
1415pub struct GraphBuilder {
1416 nodes: BTreeMap<NodeId, Node>,
1417 edges: Vec<Edge>,
1418 edge_keys: BTreeSet<(NodeId, Label, NodeId)>,
1419 edge_policy: EdgePolicy,
1420}
1421
1422impl GraphBuilder {
1423 pub fn new() -> Self {
1424 Self::default()
1425 }
1426
1427 pub fn edge_policy(mut self, edge_policy: EdgePolicy) -> Self {
1428 self.edge_policy = edge_policy;
1429 self
1430 }
1431
1432 pub fn node<'a>(
1433 &'a mut self,
1434 label: impl Into<Label>,
1435 id: impl Into<NodeId>,
1436 ) -> NodeBuilder<'a> {
1437 NodeBuilder {
1438 builder: self,
1439 label: label.into(),
1440 id: id.into(),
1441 props: Props::new(),
1442 }
1443 }
1444
1445 pub fn edge<'a>(
1446 &'a mut self,
1447 label: impl Into<Label>,
1448 from: impl Into<NodeId>,
1449 to: impl Into<NodeId>,
1450 ) -> EdgeBuilder<'a> {
1451 EdgeBuilder {
1452 builder: self,
1453 id: None,
1454 label: label.into(),
1455 from: from.into(),
1456 to: to.into(),
1457 props: Props::new(),
1458 }
1459 }
1460
1461 pub fn add_node(&mut self, node: Node) -> NodeId {
1468 let id = node.id.clone();
1469 self.nodes
1470 .entry(id.clone())
1471 .and_modify(|existing| {
1472 if existing.label == node.label {
1473 existing.props.extend(node.props.clone());
1474 } else {
1475 *existing = node.clone();
1476 }
1477 })
1478 .or_insert(node);
1479 id
1480 }
1481
1482 pub fn add_edge(&mut self, edge: Edge) -> PutOutcome {
1485 match self.edge_policy {
1486 EdgePolicy::AllowDuplicates => {
1487 self.edges.push(edge);
1488 PutOutcome::Inserted
1489 }
1490 EdgePolicy::DedupeByFromLabelTo => {
1491 let key = (edge.from.clone(), edge.label.clone(), edge.to.clone());
1492 if self.edge_keys.insert(key) {
1493 self.edges.push(edge);
1494 PutOutcome::Inserted
1495 } else {
1496 PutOutcome::Deduped
1497 }
1498 }
1499 }
1500 }
1501
1502 #[must_use = "discarding this means the graph was not built"]
1503 pub fn build(self) -> Graph {
1504 Graph {
1505 nodes: self.nodes.into_values().collect(),
1506 edges: self.edges,
1507 }
1508 }
1509}
1510
1511pub struct NodeBuilder<'a> {
1512 builder: &'a mut GraphBuilder,
1513 label: Label,
1514 id: NodeId,
1515 props: Props,
1516}
1517
1518impl<'a> NodeBuilder<'a> {
1519 pub fn prop(mut self, key: impl Into<String>, value: impl Into<Value>) -> Self {
1520 self.props.insert(key.into(), value.into());
1521 self
1522 }
1523
1524 pub fn props(mut self, props: Props) -> Self {
1525 self.props.extend(props);
1526 self
1527 }
1528
1529 #[must_use = "discarding this means the node was not added to the builder"]
1530 pub fn finish(self) -> NodeId {
1531 let node = Node::new(self.label, self.id, self.props);
1532 self.builder.add_node(node)
1533 }
1534}
1535
1536pub struct EdgeBuilder<'a> {
1537 builder: &'a mut GraphBuilder,
1538 id: Option<EdgeId>,
1539 label: Label,
1540 from: NodeId,
1541 to: NodeId,
1542 props: Props,
1543}
1544
1545impl<'a> EdgeBuilder<'a> {
1546 pub fn id(mut self, id: impl Into<EdgeId>) -> Self {
1547 self.id = Some(id.into());
1548 self
1549 }
1550
1551 pub fn prop(mut self, key: impl Into<String>, value: impl Into<Value>) -> Self {
1552 self.props.insert(key.into(), value.into());
1553 self
1554 }
1555
1556 pub fn props(mut self, props: Props) -> Self {
1557 self.props.extend(props);
1558 self
1559 }
1560
1561 #[must_use = "discarding this means the edge was not added to the builder"]
1562 pub fn finish(self) -> PutOutcome {
1563 let mut edge = Edge::new(self.label, self.from, self.to, self.props);
1564 edge.id = self.id;
1565 self.builder.add_edge(edge)
1566 }
1567}
1568
1569#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1570pub struct GraphSchema {
1571 pub nodes: Vec<NodeType>,
1572 pub edges: Vec<EdgeType>,
1573}
1574
1575impl GraphSchema {
1576 pub fn builder() -> GraphSchemaBuilder {
1577 GraphSchemaBuilder::default()
1578 }
1579
1580 pub fn node_type(&self, label: &Label) -> Option<&NodeType> {
1581 self.nodes
1582 .iter()
1583 .find(|node_type| &node_type.label == label)
1584 }
1585
1586 pub fn edge_type(&self, label: &Label) -> Option<&EdgeType> {
1587 self.edges
1588 .iter()
1589 .find(|edge_type| &edge_type.label == label)
1590 }
1591
1592 pub fn validate_graph(&self, graph: &Graph) -> Result<()> {
1593 for node in &graph.nodes {
1594 self.validate_node(node)?;
1595 }
1596 let labels: BTreeMap<&NodeId, &Label> = graph
1597 .nodes
1598 .iter()
1599 .map(|node| (&node.id, &node.label))
1600 .collect();
1601 for edge in &graph.edges {
1602 self.validate_edge_with(edge, |id| labels.get(id).copied())?;
1603 }
1604 self.validate_edge_uniqueness(graph)
1605 }
1606
1607 fn validate_edge_uniqueness(&self, graph: &Graph) -> Result<()> {
1610 let mut seen = BTreeSet::new();
1611 for edge in &graph.edges {
1612 let Some(edge_type) = self.edge_type(&edge.label) else {
1613 continue;
1614 };
1615 if edge_type.uniqueness == EdgeUniqueness::None {
1616 continue;
1617 }
1618 let (a, b) = if edge_type.directed || edge.from <= edge.to {
1619 (&edge.from, &edge.to)
1620 } else {
1621 (&edge.to, &edge.from)
1622 };
1623 if !seen.insert((edge.label.clone(), a.clone(), b.clone())) {
1624 return Err(GrustError::Schema(format!(
1625 "duplicate edge '{}' between '{}' and '{}' violates {:?} uniqueness",
1626 edge.label.as_str(),
1627 a.as_str(),
1628 b.as_str(),
1629 edge_type.uniqueness
1630 )));
1631 }
1632 }
1633 Ok(())
1634 }
1635
1636 pub fn validate_node(&self, node: &Node) -> Result<()> {
1637 let node_type = self.node_type(&node.label).ok_or_else(|| {
1638 GrustError::Schema(format!("schema has no node type '{}'", node.label.as_str()))
1639 })?;
1640 validate_props(
1641 &node.props,
1642 &node_type.fields,
1643 &format!("node '{}'", node.id.as_str()),
1644 )
1645 }
1646
1647 pub fn validate_edge(&self, edge: &Edge, graph: &Graph) -> Result<()> {
1648 self.validate_edge_with(edge, |id| {
1649 graph
1650 .nodes
1651 .iter()
1652 .find(|node| &node.id == id)
1653 .map(|node| &node.label)
1654 })
1655 }
1656
1657 pub fn validate_edge_with<'a>(
1660 &self,
1661 edge: &Edge,
1662 lookup: impl Fn(&NodeId) -> Option<&'a Label>,
1663 ) -> Result<()> {
1664 let edge_type = self.edge_type(&edge.label).ok_or_else(|| {
1665 GrustError::Schema(format!("schema has no edge type '{}'", edge.label.as_str()))
1666 })?;
1667
1668 let from_label = lookup(&edge.from).ok_or_else(|| {
1669 GrustError::Schema(format!(
1670 "edge '{}' references unknown from node '{}'",
1671 edge.label.as_str(),
1672 edge.from.as_str()
1673 ))
1674 })?;
1675 let to_label = lookup(&edge.to).ok_or_else(|| {
1676 GrustError::Schema(format!(
1677 "edge '{}' references unknown to node '{}'",
1678 edge.label.as_str(),
1679 edge.to.as_str()
1680 ))
1681 })?;
1682
1683 let from_matches =
1684 |label: &Label| edge_type.from.is_empty() || edge_type.from.contains(label);
1685 let to_matches = |label: &Label| edge_type.to.is_empty() || edge_type.to.contains(label);
1686 let endpoints_ok = (from_matches(from_label) && to_matches(to_label))
1689 || (!edge_type.directed && from_matches(to_label) && to_matches(from_label));
1690 if !endpoints_ok {
1691 if !from_matches(from_label) {
1692 return Err(GrustError::Schema(format!(
1693 "edge '{}' cannot start from node label '{}'",
1694 edge.label.as_str(),
1695 from_label.as_str()
1696 )));
1697 }
1698 return Err(GrustError::Schema(format!(
1699 "edge '{}' cannot end at node label '{}'",
1700 edge.label.as_str(),
1701 to_label.as_str()
1702 )));
1703 }
1704
1705 validate_props(
1706 &edge.props,
1707 &edge_type.fields,
1708 &format!("edge '{}'", edge.label.as_str()),
1709 )
1710 }
1711
1712 pub fn validate_edge_props(&self, edge: &Edge) -> Result<()> {
1716 let edge_type = self.edge_type(&edge.label).ok_or_else(|| {
1717 GrustError::Schema(format!("schema has no edge type '{}'", edge.label.as_str()))
1718 })?;
1719 validate_props(
1720 &edge.props,
1721 &edge_type.fields,
1722 &format!("edge '{}'", edge.label.as_str()),
1723 )
1724 }
1725}
1726
1727#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1728pub struct NodeType {
1729 pub label: Label,
1730 pub fields: Vec<Field>,
1731}
1732
1733#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1734pub struct EdgeType {
1735 pub label: Label,
1736 pub from: Vec<Label>,
1737 pub to: Vec<Label>,
1738 pub fields: Vec<Field>,
1739 pub directed: bool,
1740 pub uniqueness: EdgeUniqueness,
1741}
1742
1743#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1744pub struct Field {
1745 pub name: String,
1746 pub ty: FieldType,
1747 pub required: bool,
1748}
1749
1750#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1751pub enum FieldType {
1752 String,
1753 Int,
1754 Float,
1755 Bool,
1756 DateTime,
1757 StringArray,
1758 IntArray,
1759 FloatArray,
1760 Json,
1761}
1762
1763#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1771pub enum EdgeUniqueness {
1772 None,
1773 FromTo,
1774 FromLabelTo,
1775}
1776
1777#[derive(Clone, Debug, Default)]
1778pub struct GraphSchemaBuilder {
1779 nodes: Vec<NodeType>,
1780 edges: Vec<EdgeType>,
1781}
1782
1783impl GraphSchemaBuilder {
1784 pub fn node(mut self, label: impl Into<Label>, fields: impl Into<Vec<Field>>) -> Self {
1785 self.nodes.push(NodeType {
1786 label: label.into(),
1787 fields: fields.into(),
1788 });
1789 self
1790 }
1791
1792 pub fn edge(
1793 mut self,
1794 label: impl Into<Label>,
1795 from: impl Into<Vec<Label>>,
1796 to: impl Into<Vec<Label>>,
1797 fields: impl Into<Vec<Field>>,
1798 ) -> Self {
1799 self.edges.push(EdgeType {
1800 label: label.into(),
1801 from: from.into(),
1802 to: to.into(),
1803 fields: fields.into(),
1804 directed: true,
1805 uniqueness: EdgeUniqueness::FromLabelTo,
1806 });
1807 self
1808 }
1809
1810 pub fn edge_type(mut self, edge_type: EdgeType) -> Self {
1811 self.edges.push(edge_type);
1812 self
1813 }
1814
1815 pub fn build(self) -> GraphSchema {
1816 GraphSchema {
1817 nodes: self.nodes,
1818 edges: self.edges,
1819 }
1820 }
1821}
1822
1823impl Field {
1824 pub fn required(name: impl Into<String>, ty: FieldType) -> Self {
1825 Self {
1826 name: name.into(),
1827 ty,
1828 required: true,
1829 }
1830 }
1831
1832 pub fn optional(name: impl Into<String>, ty: FieldType) -> Self {
1833 Self {
1834 name: name.into(),
1835 ty,
1836 required: false,
1837 }
1838 }
1839}
1840
1841fn validate_props(props: &Props, fields: &[Field], context: &str) -> Result<()> {
1842 for field in fields {
1843 match props.get(&field.name) {
1844 Some(value) => validate_field_value(value, &field.ty, context, &field.name)?,
1845 None if field.required => {
1846 return Err(GrustError::Schema(format!(
1847 "{context} missing required field '{}'",
1848 field.name
1849 )));
1850 }
1851 None => {}
1852 }
1853 }
1854 Ok(())
1855}
1856
1857fn validate_field_value(
1858 value: &Value,
1859 ty: &FieldType,
1860 context: &str,
1861 field_name: &str,
1862) -> Result<()> {
1863 let matches = match (value, ty) {
1864 (Value::String(_), FieldType::String)
1865 | (Value::Int(_), FieldType::Int)
1866 | (Value::Float(_), FieldType::Float)
1867 | (Value::Bool(_), FieldType::Bool)
1868 | (Value::DateTime(_), FieldType::DateTime)
1869 | (Value::StringArray(_), FieldType::StringArray)
1870 | (Value::IntArray(_), FieldType::IntArray)
1871 | (Value::FloatArray(_), FieldType::FloatArray)
1872 | (_, FieldType::Json) => true,
1873 (Value::String(value), FieldType::DateTime) => is_rfc3339_datetime(value),
1876 _ => false,
1877 };
1878 if matches {
1879 Ok(())
1880 } else {
1881 Err(GrustError::Schema(format!(
1882 "{context} field '{field_name}' expected {ty:?}, got {value:?}"
1883 )))
1884 }
1885}
1886
1887#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1888pub struct Traversal {
1889 pub start: Start,
1890 pub steps: Vec<Step>,
1891 pub limit: Option<u32>,
1892}
1893
1894impl Traversal {
1895 pub fn from_node(id: impl Into<NodeId>) -> Self {
1896 Self {
1897 start: Start::Node(id.into()),
1898 steps: Vec::new(),
1899 limit: None,
1900 }
1901 }
1902
1903 pub fn out(mut self, edge: impl Into<Label>) -> Self {
1904 self.steps.push(Step {
1905 direction: Direction::Out,
1906 edge: Some(edge.into()),
1907 node: None,
1908 });
1909 self
1910 }
1911
1912 pub fn in_(mut self, edge: impl Into<Label>) -> Self {
1913 self.steps.push(Step {
1914 direction: Direction::In,
1915 edge: Some(edge.into()),
1916 node: None,
1917 });
1918 self
1919 }
1920
1921 pub fn both(mut self, edge: impl Into<Label>) -> Self {
1922 self.steps.push(Step {
1923 direction: Direction::Both,
1924 edge: Some(edge.into()),
1925 node: None,
1926 });
1927 self
1928 }
1929
1930 pub fn to(mut self, node: impl Into<Label>) -> Self {
1937 let step = self
1938 .steps
1939 .last_mut()
1940 .expect("Traversal::to() must follow out(), in_(), or both()");
1941 step.node = Some(node.into());
1942 self
1943 }
1944
1945 pub fn limit(mut self, limit: u32) -> Self {
1946 self.limit = Some(limit);
1947 self
1948 }
1949}
1950
1951#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1952pub enum Start {
1953 Node(NodeId),
1954 NodesByLabel(Label),
1955 NodesByProperty {
1956 label: Label,
1957 key: String,
1958 value: Value,
1959 },
1960}
1961
1962#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1963pub struct Step {
1964 pub direction: Direction,
1965 pub edge: Option<Label>,
1966 pub node: Option<Label>,
1967}
1968
1969#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1970pub enum Direction {
1971 Out,
1972 In,
1973 Both,
1974}
1975
1976#[derive(Clone, Debug, Default, PartialEq)]
1977pub struct EdgeQuery {
1978 pub from: Option<NodeId>,
1979 pub to: Option<NodeId>,
1980 pub label: Option<Label>,
1981}
1982
1983#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
1993pub enum PutOutcome {
1994 Inserted,
1996 Updated,
1999 Upserted,
2002 Deduped,
2004}
2005
2006impl PutOutcome {
2007 pub fn written(self) -> bool {
2009 !matches!(self, Self::Deduped)
2010 }
2011}
2012
2013#[derive(Clone, Debug, Default, Eq, PartialEq)]
2016pub struct LoadReport {
2017 pub nodes: usize,
2018 pub edges: usize,
2019}
2020
2021#[async_trait]
2022pub trait GraphStore: Send + Sync {
2023 async fn apply_schema(&self, _schema: &GraphSchema) -> Result<()> {
2038 Ok(())
2039 }
2040
2041 async fn put_node(&self, node: &Node) -> Result<PutOutcome>;
2047
2048 async fn put_edge(&self, edge: &Edge) -> Result<PutOutcome>;
2054
2055 async fn put_graph(&self, graph: &Graph) -> Result<LoadReport> {
2056 let mut report = LoadReport::default();
2057 for node in &graph.nodes {
2058 if self.put_node(node).await?.written() {
2059 report.nodes += 1;
2060 }
2061 }
2062 for edge in &graph.edges {
2063 if self.put_edge(edge).await?.written() {
2064 report.edges += 1;
2065 }
2066 }
2067 Ok(report)
2068 }
2069
2070 async fn put_typed_graph(&self, schema: &GraphSchema, graph: &Graph) -> Result<LoadReport> {
2071 schema.validate_graph(graph)?;
2072 self.apply_schema(schema).await?;
2073 self.put_graph(graph).await
2074 }
2075
2076 async fn get_node(&self, id: &NodeId) -> Result<Option<Node>>;
2077
2078 async fn get_nodes(&self, ids: &[NodeId]) -> Result<Vec<Node>> {
2085 let mut nodes = Vec::new();
2086 for id in ids {
2087 if let Some(node) = self.get_node(id).await? {
2088 nodes.push(node);
2089 }
2090 }
2091 Ok(nodes)
2092 }
2093
2094 async fn get_edges(&self, query: EdgeQuery) -> Result<Vec<Edge>>;
2095 async fn traverse(&self, traversal: Traversal) -> Result<Vec<Node>>;
2096}
2097
2098#[async_trait]
2099pub trait GraphAdminStore: GraphStore {
2100 async fn bootstrap(&self) -> Result<()> {
2101 Ok(())
2102 }
2103
2104 async fn clear(&self) -> Result<()>;
2105}
2106
2107#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
2109pub enum GraphMutation {
2110 UpsertNode(Node),
2111 DeleteNode(NodeId),
2112 UpsertEdge(Edge),
2113 DeleteEdge {
2114 from: NodeId,
2115 label: Label,
2116 to: NodeId,
2117 },
2118}
2119
2120#[async_trait]
2125pub trait GraphMutationStore: GraphStore {
2126 async fn delete_node(&self, id: &NodeId) -> Result<()>;
2128
2129 async fn delete_edge(&self, from: &NodeId, label: &Label, to: &NodeId) -> Result<()>;
2131
2132 async fn apply_mutations(&self, mutations: &[GraphMutation]) -> Result<()> {
2139 for mutation in mutations {
2140 match mutation {
2141 GraphMutation::UpsertNode(node) => {
2142 self.put_node(node).await?;
2143 }
2144 GraphMutation::DeleteNode(id) => self.delete_node(id).await?,
2145 GraphMutation::UpsertEdge(edge) => {
2146 self.put_edge(edge).await?;
2147 }
2148 GraphMutation::DeleteEdge { from, label, to } => {
2149 self.delete_edge(from, label, to).await?
2150 }
2151 }
2152 }
2153 Ok(())
2154 }
2155}
2156
2157pub mod prelude {
2158 pub use crate::{
2159 Direction, Edge, EdgeId, EdgePolicy, EdgeQuery, EdgeType, EdgeUniqueness, Field, FieldType,
2160 Graph, GraphAdminStore, GraphBuilder, GraphIndex, GraphMutation, GraphMutationStore,
2161 GraphSchema, GraphSchemaBuilder, GraphStore, GrustError, Label, LoadReport, Node, NodeId,
2162 NodeType, Props, PutOutcome, Result, RfcDate, Start, Step, Traversal, Value, edge_key,
2163 relationship_type, schema_identifier,
2164 };
2165
2166 #[cfg(feature = "typed-garde")]
2167 pub use crate::typed::{TypedEdge, TypedGraphBuilder, TypedNode, garde, props_from_serialize};
2168
2169 #[cfg(feature = "typed-zod-rs")]
2170 pub use crate::typed::{parse_typed_json, parse_typed_json_with, zod_rs};
2171}
2172
2173#[cfg(test)]
2174mod tests;