1use std::{
2 collections::{BTreeMap, BTreeSet},
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 format!(
891 "{}\u{1f}{}\u{1f}{}",
892 edge.from.as_str(),
893 edge.label.as_str(),
894 edge.to.as_str()
895 )
896 })
897}
898
899#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
900pub struct Graph {
901 pub nodes: Vec<Node>,
902 pub edges: Vec<Edge>,
903}
904
905impl Graph {
906 pub fn new(nodes: Vec<Node>, edges: Vec<Edge>) -> Self {
907 Self { nodes, edges }
908 }
909
910 pub fn from_yaml(yaml: &str) -> Result<Self> {
911 yaml::graph_from_yaml(yaml)
912 }
913
914 pub fn to_yaml(&self) -> Result<String> {
915 yaml::graph_to_yaml(self)
916 }
917
918 pub fn from_json(json: &str) -> Result<Self> {
919 json::graph_from_json(json)
920 }
921
922 pub fn to_json(&self) -> Result<String> {
923 json::graph_to_json(self)
924 }
925
926 pub fn from_xml(xml: &str) -> Result<Self> {
927 xml::graph_from_xml(xml)
928 }
929
930 pub fn to_xml(&self) -> Result<String> {
931 xml::graph_to_xml(self)
932 }
933
934 pub fn builder() -> GraphBuilder {
935 GraphBuilder::new()
936 }
937}
938
939mod graph_doc {
940 use std::collections::{BTreeMap, BTreeSet};
941
942 use serde::{Deserialize, Serialize};
943
944 use crate::{Edge, EdgeId, Graph, GrustError, Label, Node, NodeId, Props, Value};
945
946 #[derive(Debug, Serialize, Deserialize)]
947 pub(super) struct GraphDoc {
948 #[serde(default)]
949 pub(super) nodes: Vec<NodeDoc>,
950 #[serde(default)]
951 pub(super) edges: Vec<EdgeDoc>,
952 }
953
954 #[derive(Debug, Serialize, Deserialize)]
955 pub(super) struct NodeDoc {
956 pub(super) id: NodeId,
957 pub(super) label: Label,
958 #[serde(default, deserialize_with = "deserialize_props")]
959 pub(super) props: Props,
960 }
961
962 #[derive(Debug, Serialize, Deserialize)]
963 pub(super) struct NodeDocOut {
964 pub(super) id: NodeId,
965 pub(super) label: Label,
966 #[serde(default)]
967 pub(super) props: Props,
968 }
969
970 #[derive(Debug, Serialize, Deserialize)]
971 pub(super) struct EdgeDoc {
972 #[serde(default)]
973 pub(super) id: Option<EdgeId>,
974 pub(super) label: Label,
975 pub(super) from: NodeId,
976 pub(super) to: NodeId,
977 #[serde(default, deserialize_with = "deserialize_props")]
978 pub(super) props: Props,
979 }
980
981 #[derive(Debug, Serialize, Deserialize)]
982 pub(super) struct EdgeDocOut {
983 #[serde(default)]
984 pub(super) id: Option<EdgeId>,
985 pub(super) label: Label,
986 pub(super) from: NodeId,
987 pub(super) to: NodeId,
988 #[serde(default)]
989 pub(super) props: Props,
990 }
991
992 pub(super) fn graph_from_doc(doc: GraphDoc) -> super::Result<Graph> {
993 let mut ids = BTreeSet::new();
994 for node in &doc.nodes {
995 if !ids.insert(node.id.clone()) {
996 return Err(GrustError::Schema(format!(
997 "duplicate node id '{}'",
998 node.id
999 )));
1000 }
1001 }
1002
1003 let mut edges = Vec::with_capacity(doc.edges.len());
1004 for edge in doc.edges {
1005 if !ids.contains(&edge.from) {
1006 return Err(GrustError::Schema(format!(
1007 "edge '{}' references unknown from node '{}'",
1008 edge.label, edge.from
1009 )));
1010 }
1011 if !ids.contains(&edge.to) {
1012 return Err(GrustError::Schema(format!(
1013 "edge '{}' references unknown to node '{}'",
1014 edge.label, edge.to
1015 )));
1016 }
1017
1018 let mut graph_edge = Edge::new(edge.label, edge.from, edge.to, edge.props);
1019 graph_edge.id = edge.id;
1020 edges.push(graph_edge);
1021 }
1022
1023 let nodes = doc
1024 .nodes
1025 .into_iter()
1026 .map(|node| Node::new(node.label, node.id, node.props))
1027 .collect();
1028
1029 Ok(Graph::new(nodes, edges))
1030 }
1031
1032 pub(super) fn graph_to_doc(graph: &Graph) -> GraphDocOut {
1033 GraphDocOut {
1034 nodes: graph
1035 .nodes
1036 .iter()
1037 .map(|node| NodeDocOut {
1038 id: node.id.clone(),
1039 label: node.label.clone(),
1040 props: without_generated_id(&node.props, &node.id),
1041 })
1042 .collect(),
1043 edges: graph
1044 .edges
1045 .iter()
1046 .map(|edge| EdgeDocOut {
1047 id: edge.id.clone(),
1048 label: edge.label.clone(),
1049 from: edge.from.clone(),
1050 to: edge.to.clone(),
1051 props: edge.props.clone(),
1052 })
1053 .collect(),
1054 }
1055 }
1056
1057 fn without_generated_id(props: &Props, id: &NodeId) -> Props {
1058 let mut props = props.clone();
1059 if props.get("id") == Some(&Value::from(id.as_str())) {
1060 props.remove("id");
1061 }
1062 props
1063 }
1064
1065 fn deserialize_props<'de, D>(deserializer: D) -> std::result::Result<Props, D::Error>
1066 where
1067 D: serde::Deserializer<'de>,
1068 {
1069 let raw = BTreeMap::<String, serde_json::Value>::deserialize(deserializer)?;
1070 raw.into_iter()
1071 .map(|(key, value)| {
1072 value_from_json(value)
1073 .map(|value| (key, value))
1074 .map_err(serde::de::Error::custom)
1075 })
1076 .collect()
1077 }
1078
1079 fn value_from_json(value: serde_json::Value) -> std::result::Result<Value, String> {
1080 if let serde_json::Value::Object(mapping) = &value
1081 && mapping.contains_key("type")
1082 && mapping.contains_key("value")
1083 {
1084 return serde_json::from_value(value)
1085 .map_err(|err| format!("invalid tagged Grust value: {err}"));
1086 }
1087
1088 Ok(Value::from_json(value))
1089 }
1090
1091 #[derive(Debug, Serialize, Deserialize)]
1092 pub(super) struct GraphDocOut {
1093 pub(super) nodes: Vec<NodeDocOut>,
1094 pub(super) edges: Vec<EdgeDocOut>,
1095 }
1096}
1097
1098mod yaml {
1099 use crate::{Graph, GrustError};
1100
1101 pub(super) fn graph_from_yaml(yaml: &str) -> super::Result<Graph> {
1102 let doc: super::graph_doc::GraphDoc = serde_yaml::from_str(yaml)
1103 .map_err(|err| GrustError::Serialization(format!("YAML parse error: {err}")))?;
1104 super::graph_doc::graph_from_doc(doc)
1105 }
1106
1107 pub(super) fn graph_to_yaml(graph: &Graph) -> super::Result<String> {
1108 serde_yaml::to_string(&super::graph_doc::graph_to_doc(graph))
1109 .map_err(|err| GrustError::Serialization(format!("YAML serialization error: {err}")))
1110 }
1111}
1112
1113mod json {
1114 use crate::{Graph, GrustError};
1115
1116 pub(super) fn graph_from_json(json: &str) -> super::Result<Graph> {
1117 let doc: super::graph_doc::GraphDoc = serde_json::from_str(json)
1118 .map_err(|err| GrustError::Serialization(format!("JSON parse error: {err}")))?;
1119 super::graph_doc::graph_from_doc(doc)
1120 }
1121
1122 pub(super) fn graph_to_json(graph: &Graph) -> super::Result<String> {
1123 serde_json::to_string_pretty(&super::graph_doc::graph_to_doc(graph))
1124 .map_err(|err| GrustError::Serialization(format!("JSON serialization error: {err}")))
1125 }
1126}
1127
1128mod xml {
1129 use serde::{Deserialize, Serialize};
1130
1131 use crate::{Edge, EdgeId, Graph, GrustError, Label, Node, NodeId, Props, Value};
1132
1133 #[derive(Debug, Serialize, Deserialize)]
1134 #[serde(rename = "graph")]
1135 struct GraphXml {
1136 #[serde(default)]
1137 nodes: NodesXml,
1138 #[serde(default)]
1139 edges: EdgesXml,
1140 }
1141
1142 #[derive(Debug, Default, Serialize, Deserialize)]
1143 struct NodesXml {
1144 #[serde(rename = "node", default)]
1145 items: Vec<NodeXml>,
1146 }
1147
1148 #[derive(Debug, Default, Serialize, Deserialize)]
1149 struct EdgesXml {
1150 #[serde(rename = "edge", default)]
1151 items: Vec<EdgeXml>,
1152 }
1153
1154 #[derive(Debug, Serialize, Deserialize)]
1155 struct NodeXml {
1156 id: NodeId,
1157 label: Label,
1158 #[serde(default)]
1159 props: PropsXml,
1160 }
1161
1162 #[derive(Debug, Serialize, Deserialize)]
1163 struct EdgeXml {
1164 #[serde(default, skip_serializing_if = "Option::is_none")]
1165 id: Option<EdgeId>,
1166 label: Label,
1167 from: NodeId,
1168 to: NodeId,
1169 #[serde(default)]
1170 props: PropsXml,
1171 }
1172
1173 #[derive(Debug, Default, Serialize, Deserialize)]
1174 struct PropsXml {
1175 #[serde(rename = "prop", default)]
1176 items: Vec<PropXml>,
1177 }
1178
1179 #[derive(Debug, Serialize, Deserialize)]
1180 struct PropXml {
1181 key: String,
1182 value: Value,
1183 }
1184
1185 pub(super) fn graph_from_xml(xml: &str) -> super::Result<Graph> {
1186 let doc: GraphXml = quick_xml::de::from_str(xml)
1187 .map_err(|err| GrustError::Serialization(format!("XML parse error: {err}")))?;
1188 super::graph_doc::graph_from_doc(doc.into())
1189 }
1190
1191 pub(super) fn graph_to_xml(graph: &Graph) -> super::Result<String> {
1192 quick_xml::se::to_string(&GraphXml::from(graph))
1193 .map_err(|err| GrustError::Serialization(format!("XML serialization error: {err}")))
1194 }
1195
1196 impl From<GraphXml> for super::graph_doc::GraphDoc {
1197 fn from(value: GraphXml) -> Self {
1198 Self {
1199 nodes: value.nodes.items.into_iter().map(Into::into).collect(),
1200 edges: value.edges.items.into_iter().map(Into::into).collect(),
1201 }
1202 }
1203 }
1204
1205 impl From<NodeXml> for super::graph_doc::NodeDoc {
1206 fn from(value: NodeXml) -> Self {
1207 Self {
1208 id: value.id,
1209 label: value.label,
1210 props: value.props.into(),
1211 }
1212 }
1213 }
1214
1215 impl From<EdgeXml> for super::graph_doc::EdgeDoc {
1216 fn from(value: EdgeXml) -> Self {
1217 Self {
1218 id: value.id,
1219 label: value.label,
1220 from: value.from,
1221 to: value.to,
1222 props: value.props.into(),
1223 }
1224 }
1225 }
1226
1227 impl From<PropsXml> for Props {
1228 fn from(value: PropsXml) -> Self {
1229 value
1230 .items
1231 .into_iter()
1232 .map(|prop| (prop.key, prop.value))
1233 .collect()
1234 }
1235 }
1236
1237 impl From<&Graph> for GraphXml {
1238 fn from(graph: &Graph) -> Self {
1239 Self {
1240 nodes: NodesXml {
1241 items: graph.nodes.iter().map(NodeXml::from).collect(),
1242 },
1243 edges: EdgesXml {
1244 items: graph.edges.iter().map(EdgeXml::from).collect(),
1245 },
1246 }
1247 }
1248 }
1249
1250 impl From<&Node> for NodeXml {
1251 fn from(node: &Node) -> Self {
1252 let props = super::graph_doc::graph_to_doc(&Graph::new(vec![node.clone()], Vec::new()))
1253 .nodes
1254 .into_iter()
1255 .next()
1256 .expect("node exists")
1257 .props;
1258 Self {
1259 id: node.id.clone(),
1260 label: node.label.clone(),
1261 props: props.into(),
1262 }
1263 }
1264 }
1265
1266 impl From<&Edge> for EdgeXml {
1267 fn from(edge: &Edge) -> Self {
1268 Self {
1269 id: edge.id.clone(),
1270 label: edge.label.clone(),
1271 from: edge.from.clone(),
1272 to: edge.to.clone(),
1273 props: edge.props.clone().into(),
1274 }
1275 }
1276 }
1277
1278 impl From<Props> for PropsXml {
1279 fn from(value: Props) -> Self {
1280 Self {
1281 items: value
1282 .into_iter()
1283 .map(|(key, value)| PropXml { key, value })
1284 .collect(),
1285 }
1286 }
1287 }
1288}
1289
1290#[derive(Clone, Debug, Default, Eq, PartialEq)]
1291pub enum EdgePolicy {
1292 AllowDuplicates,
1293 #[default]
1294 DedupeByFromLabelTo,
1295}
1296
1297#[derive(Clone, Debug, Default)]
1298pub struct GraphBuilder {
1299 nodes: BTreeMap<NodeId, Node>,
1300 edges: Vec<Edge>,
1301 edge_keys: BTreeSet<(NodeId, Label, NodeId)>,
1302 edge_policy: EdgePolicy,
1303}
1304
1305impl GraphBuilder {
1306 pub fn new() -> Self {
1307 Self::default()
1308 }
1309
1310 pub fn edge_policy(mut self, edge_policy: EdgePolicy) -> Self {
1311 self.edge_policy = edge_policy;
1312 self
1313 }
1314
1315 pub fn node<'a>(
1316 &'a mut self,
1317 label: impl Into<Label>,
1318 id: impl Into<NodeId>,
1319 ) -> NodeBuilder<'a> {
1320 NodeBuilder {
1321 builder: self,
1322 label: label.into(),
1323 id: id.into(),
1324 props: Props::new(),
1325 }
1326 }
1327
1328 pub fn edge<'a>(
1329 &'a mut self,
1330 label: impl Into<Label>,
1331 from: impl Into<NodeId>,
1332 to: impl Into<NodeId>,
1333 ) -> EdgeBuilder<'a> {
1334 EdgeBuilder {
1335 builder: self,
1336 id: None,
1337 label: label.into(),
1338 from: from.into(),
1339 to: to.into(),
1340 props: Props::new(),
1341 }
1342 }
1343
1344 pub fn add_node(&mut self, node: Node) -> NodeId {
1351 let id = node.id.clone();
1352 self.nodes
1353 .entry(id.clone())
1354 .and_modify(|existing| {
1355 if existing.label == node.label {
1356 existing.props.extend(node.props.clone());
1357 } else {
1358 *existing = node.clone();
1359 }
1360 })
1361 .or_insert(node);
1362 id
1363 }
1364
1365 pub fn add_edge(&mut self, edge: Edge) -> PutOutcome {
1368 match self.edge_policy {
1369 EdgePolicy::AllowDuplicates => {
1370 self.edges.push(edge);
1371 PutOutcome::Inserted
1372 }
1373 EdgePolicy::DedupeByFromLabelTo => {
1374 let key = (edge.from.clone(), edge.label.clone(), edge.to.clone());
1375 if self.edge_keys.insert(key) {
1376 self.edges.push(edge);
1377 PutOutcome::Inserted
1378 } else {
1379 PutOutcome::Deduped
1380 }
1381 }
1382 }
1383 }
1384
1385 #[must_use = "discarding this means the graph was not built"]
1386 pub fn build(self) -> Graph {
1387 Graph {
1388 nodes: self.nodes.into_values().collect(),
1389 edges: self.edges,
1390 }
1391 }
1392}
1393
1394pub struct NodeBuilder<'a> {
1395 builder: &'a mut GraphBuilder,
1396 label: Label,
1397 id: NodeId,
1398 props: Props,
1399}
1400
1401impl<'a> NodeBuilder<'a> {
1402 pub fn prop(mut self, key: impl Into<String>, value: impl Into<Value>) -> Self {
1403 self.props.insert(key.into(), value.into());
1404 self
1405 }
1406
1407 pub fn props(mut self, props: Props) -> Self {
1408 self.props.extend(props);
1409 self
1410 }
1411
1412 #[must_use = "discarding this means the node was not added to the builder"]
1413 pub fn finish(self) -> NodeId {
1414 let node = Node::new(self.label, self.id, self.props);
1415 self.builder.add_node(node)
1416 }
1417}
1418
1419pub struct EdgeBuilder<'a> {
1420 builder: &'a mut GraphBuilder,
1421 id: Option<EdgeId>,
1422 label: Label,
1423 from: NodeId,
1424 to: NodeId,
1425 props: Props,
1426}
1427
1428impl<'a> EdgeBuilder<'a> {
1429 pub fn id(mut self, id: impl Into<EdgeId>) -> Self {
1430 self.id = Some(id.into());
1431 self
1432 }
1433
1434 pub fn prop(mut self, key: impl Into<String>, value: impl Into<Value>) -> Self {
1435 self.props.insert(key.into(), value.into());
1436 self
1437 }
1438
1439 pub fn props(mut self, props: Props) -> Self {
1440 self.props.extend(props);
1441 self
1442 }
1443
1444 #[must_use = "discarding this means the edge was not added to the builder"]
1445 pub fn finish(self) -> PutOutcome {
1446 let mut edge = Edge::new(self.label, self.from, self.to, self.props);
1447 edge.id = self.id;
1448 self.builder.add_edge(edge)
1449 }
1450}
1451
1452#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1453pub struct GraphSchema {
1454 pub nodes: Vec<NodeType>,
1455 pub edges: Vec<EdgeType>,
1456}
1457
1458impl GraphSchema {
1459 pub fn builder() -> GraphSchemaBuilder {
1460 GraphSchemaBuilder::default()
1461 }
1462
1463 pub fn node_type(&self, label: &Label) -> Option<&NodeType> {
1464 self.nodes
1465 .iter()
1466 .find(|node_type| &node_type.label == label)
1467 }
1468
1469 pub fn edge_type(&self, label: &Label) -> Option<&EdgeType> {
1470 self.edges
1471 .iter()
1472 .find(|edge_type| &edge_type.label == label)
1473 }
1474
1475 pub fn validate_graph(&self, graph: &Graph) -> Result<()> {
1476 for node in &graph.nodes {
1477 self.validate_node(node)?;
1478 }
1479 let labels: BTreeMap<&NodeId, &Label> = graph
1480 .nodes
1481 .iter()
1482 .map(|node| (&node.id, &node.label))
1483 .collect();
1484 for edge in &graph.edges {
1485 self.validate_edge_with(edge, |id| labels.get(id).copied())?;
1486 }
1487 self.validate_edge_uniqueness(graph)
1488 }
1489
1490 fn validate_edge_uniqueness(&self, graph: &Graph) -> Result<()> {
1493 let mut seen = BTreeSet::new();
1494 for edge in &graph.edges {
1495 let Some(edge_type) = self.edge_type(&edge.label) else {
1496 continue;
1497 };
1498 if edge_type.uniqueness == EdgeUniqueness::None {
1499 continue;
1500 }
1501 let (a, b) = if edge_type.directed || edge.from <= edge.to {
1502 (&edge.from, &edge.to)
1503 } else {
1504 (&edge.to, &edge.from)
1505 };
1506 if !seen.insert((edge.label.clone(), a.clone(), b.clone())) {
1507 return Err(GrustError::Schema(format!(
1508 "duplicate edge '{}' between '{}' and '{}' violates {:?} uniqueness",
1509 edge.label.as_str(),
1510 a.as_str(),
1511 b.as_str(),
1512 edge_type.uniqueness
1513 )));
1514 }
1515 }
1516 Ok(())
1517 }
1518
1519 pub fn validate_node(&self, node: &Node) -> Result<()> {
1520 let node_type = self.node_type(&node.label).ok_or_else(|| {
1521 GrustError::Schema(format!("schema has no node type '{}'", node.label.as_str()))
1522 })?;
1523 validate_props(
1524 &node.props,
1525 &node_type.fields,
1526 &format!("node '{}'", node.id.as_str()),
1527 )
1528 }
1529
1530 pub fn validate_edge(&self, edge: &Edge, graph: &Graph) -> Result<()> {
1531 self.validate_edge_with(edge, |id| {
1532 graph
1533 .nodes
1534 .iter()
1535 .find(|node| &node.id == id)
1536 .map(|node| &node.label)
1537 })
1538 }
1539
1540 pub fn validate_edge_with<'a>(
1543 &self,
1544 edge: &Edge,
1545 lookup: impl Fn(&NodeId) -> Option<&'a Label>,
1546 ) -> Result<()> {
1547 let edge_type = self.edge_type(&edge.label).ok_or_else(|| {
1548 GrustError::Schema(format!("schema has no edge type '{}'", edge.label.as_str()))
1549 })?;
1550
1551 let from_label = lookup(&edge.from).ok_or_else(|| {
1552 GrustError::Schema(format!(
1553 "edge '{}' references unknown from node '{}'",
1554 edge.label.as_str(),
1555 edge.from.as_str()
1556 ))
1557 })?;
1558 let to_label = lookup(&edge.to).ok_or_else(|| {
1559 GrustError::Schema(format!(
1560 "edge '{}' references unknown to node '{}'",
1561 edge.label.as_str(),
1562 edge.to.as_str()
1563 ))
1564 })?;
1565
1566 let from_matches =
1567 |label: &Label| edge_type.from.is_empty() || edge_type.from.contains(label);
1568 let to_matches = |label: &Label| edge_type.to.is_empty() || edge_type.to.contains(label);
1569 let endpoints_ok = (from_matches(from_label) && to_matches(to_label))
1572 || (!edge_type.directed && from_matches(to_label) && to_matches(from_label));
1573 if !endpoints_ok {
1574 if !from_matches(from_label) {
1575 return Err(GrustError::Schema(format!(
1576 "edge '{}' cannot start from node label '{}'",
1577 edge.label.as_str(),
1578 from_label.as_str()
1579 )));
1580 }
1581 return Err(GrustError::Schema(format!(
1582 "edge '{}' cannot end at node label '{}'",
1583 edge.label.as_str(),
1584 to_label.as_str()
1585 )));
1586 }
1587
1588 validate_props(
1589 &edge.props,
1590 &edge_type.fields,
1591 &format!("edge '{}'", edge.label.as_str()),
1592 )
1593 }
1594
1595 pub fn validate_edge_props(&self, edge: &Edge) -> Result<()> {
1599 let edge_type = self.edge_type(&edge.label).ok_or_else(|| {
1600 GrustError::Schema(format!("schema has no edge type '{}'", edge.label.as_str()))
1601 })?;
1602 validate_props(
1603 &edge.props,
1604 &edge_type.fields,
1605 &format!("edge '{}'", edge.label.as_str()),
1606 )
1607 }
1608}
1609
1610#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1611pub struct NodeType {
1612 pub label: Label,
1613 pub fields: Vec<Field>,
1614}
1615
1616#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1617pub struct EdgeType {
1618 pub label: Label,
1619 pub from: Vec<Label>,
1620 pub to: Vec<Label>,
1621 pub fields: Vec<Field>,
1622 pub directed: bool,
1623 pub uniqueness: EdgeUniqueness,
1624}
1625
1626#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1627pub struct Field {
1628 pub name: String,
1629 pub ty: FieldType,
1630 pub required: bool,
1631}
1632
1633#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1634pub enum FieldType {
1635 String,
1636 Int,
1637 Float,
1638 Bool,
1639 DateTime,
1640 StringArray,
1641 IntArray,
1642 FloatArray,
1643 Json,
1644}
1645
1646#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1654pub enum EdgeUniqueness {
1655 None,
1656 FromTo,
1657 FromLabelTo,
1658}
1659
1660#[derive(Clone, Debug, Default)]
1661pub struct GraphSchemaBuilder {
1662 nodes: Vec<NodeType>,
1663 edges: Vec<EdgeType>,
1664}
1665
1666impl GraphSchemaBuilder {
1667 pub fn node(mut self, label: impl Into<Label>, fields: impl Into<Vec<Field>>) -> Self {
1668 self.nodes.push(NodeType {
1669 label: label.into(),
1670 fields: fields.into(),
1671 });
1672 self
1673 }
1674
1675 pub fn edge(
1676 mut self,
1677 label: impl Into<Label>,
1678 from: impl Into<Vec<Label>>,
1679 to: impl Into<Vec<Label>>,
1680 fields: impl Into<Vec<Field>>,
1681 ) -> Self {
1682 self.edges.push(EdgeType {
1683 label: label.into(),
1684 from: from.into(),
1685 to: to.into(),
1686 fields: fields.into(),
1687 directed: true,
1688 uniqueness: EdgeUniqueness::FromLabelTo,
1689 });
1690 self
1691 }
1692
1693 pub fn edge_type(mut self, edge_type: EdgeType) -> Self {
1694 self.edges.push(edge_type);
1695 self
1696 }
1697
1698 pub fn build(self) -> GraphSchema {
1699 GraphSchema {
1700 nodes: self.nodes,
1701 edges: self.edges,
1702 }
1703 }
1704}
1705
1706impl Field {
1707 pub fn required(name: impl Into<String>, ty: FieldType) -> Self {
1708 Self {
1709 name: name.into(),
1710 ty,
1711 required: true,
1712 }
1713 }
1714
1715 pub fn optional(name: impl Into<String>, ty: FieldType) -> Self {
1716 Self {
1717 name: name.into(),
1718 ty,
1719 required: false,
1720 }
1721 }
1722}
1723
1724fn validate_props(props: &Props, fields: &[Field], context: &str) -> Result<()> {
1725 for field in fields {
1726 match props.get(&field.name) {
1727 Some(value) => validate_field_value(value, &field.ty, context, &field.name)?,
1728 None if field.required => {
1729 return Err(GrustError::Schema(format!(
1730 "{context} missing required field '{}'",
1731 field.name
1732 )));
1733 }
1734 None => {}
1735 }
1736 }
1737 Ok(())
1738}
1739
1740fn validate_field_value(
1741 value: &Value,
1742 ty: &FieldType,
1743 context: &str,
1744 field_name: &str,
1745) -> Result<()> {
1746 let matches = match (value, ty) {
1747 (Value::String(_), FieldType::String)
1748 | (Value::Int(_), FieldType::Int)
1749 | (Value::Float(_), FieldType::Float)
1750 | (Value::Bool(_), FieldType::Bool)
1751 | (Value::DateTime(_), FieldType::DateTime)
1752 | (Value::StringArray(_), FieldType::StringArray)
1753 | (Value::IntArray(_), FieldType::IntArray)
1754 | (Value::FloatArray(_), FieldType::FloatArray)
1755 | (_, FieldType::Json) => true,
1756 (Value::String(value), FieldType::DateTime) => is_rfc3339_datetime(value),
1759 _ => false,
1760 };
1761 if matches {
1762 Ok(())
1763 } else {
1764 Err(GrustError::Schema(format!(
1765 "{context} field '{field_name}' expected {ty:?}, got {value:?}"
1766 )))
1767 }
1768}
1769
1770#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1771pub struct Traversal {
1772 pub start: Start,
1773 pub steps: Vec<Step>,
1774 pub limit: Option<u32>,
1775}
1776
1777impl Traversal {
1778 pub fn from_node(id: impl Into<NodeId>) -> Self {
1779 Self {
1780 start: Start::Node(id.into()),
1781 steps: Vec::new(),
1782 limit: None,
1783 }
1784 }
1785
1786 pub fn out(mut self, edge: impl Into<Label>) -> Self {
1787 self.steps.push(Step {
1788 direction: Direction::Out,
1789 edge: Some(edge.into()),
1790 node: None,
1791 });
1792 self
1793 }
1794
1795 pub fn in_(mut self, edge: impl Into<Label>) -> Self {
1796 self.steps.push(Step {
1797 direction: Direction::In,
1798 edge: Some(edge.into()),
1799 node: None,
1800 });
1801 self
1802 }
1803
1804 pub fn both(mut self, edge: impl Into<Label>) -> Self {
1805 self.steps.push(Step {
1806 direction: Direction::Both,
1807 edge: Some(edge.into()),
1808 node: None,
1809 });
1810 self
1811 }
1812
1813 pub fn to(mut self, node: impl Into<Label>) -> Self {
1820 let step = self
1821 .steps
1822 .last_mut()
1823 .expect("Traversal::to() must follow out(), in_(), or both()");
1824 step.node = Some(node.into());
1825 self
1826 }
1827
1828 pub fn limit(mut self, limit: u32) -> Self {
1829 self.limit = Some(limit);
1830 self
1831 }
1832}
1833
1834#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1835pub enum Start {
1836 Node(NodeId),
1837 NodesByLabel(Label),
1838 NodesByProperty {
1839 label: Label,
1840 key: String,
1841 value: Value,
1842 },
1843}
1844
1845#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1846pub struct Step {
1847 pub direction: Direction,
1848 pub edge: Option<Label>,
1849 pub node: Option<Label>,
1850}
1851
1852#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1853pub enum Direction {
1854 Out,
1855 In,
1856 Both,
1857}
1858
1859#[derive(Clone, Debug, Default, PartialEq)]
1860pub struct EdgeQuery {
1861 pub from: Option<NodeId>,
1862 pub to: Option<NodeId>,
1863 pub label: Option<Label>,
1864}
1865
1866#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
1876pub enum PutOutcome {
1877 Inserted,
1879 Updated,
1882 Upserted,
1885 Deduped,
1887}
1888
1889impl PutOutcome {
1890 pub fn written(self) -> bool {
1892 !matches!(self, Self::Deduped)
1893 }
1894}
1895
1896#[derive(Clone, Debug, Default, Eq, PartialEq)]
1899pub struct LoadReport {
1900 pub nodes: usize,
1901 pub edges: usize,
1902}
1903
1904#[async_trait]
1905pub trait GraphStore: Send + Sync {
1906 async fn apply_schema(&self, _schema: &GraphSchema) -> Result<()> {
1921 Ok(())
1922 }
1923
1924 async fn put_node(&self, node: &Node) -> Result<PutOutcome>;
1930
1931 async fn put_edge(&self, edge: &Edge) -> Result<PutOutcome>;
1937
1938 async fn put_graph(&self, graph: &Graph) -> Result<LoadReport> {
1939 let mut report = LoadReport::default();
1940 for node in &graph.nodes {
1941 if self.put_node(node).await?.written() {
1942 report.nodes += 1;
1943 }
1944 }
1945 for edge in &graph.edges {
1946 if self.put_edge(edge).await?.written() {
1947 report.edges += 1;
1948 }
1949 }
1950 Ok(report)
1951 }
1952
1953 async fn put_typed_graph(&self, schema: &GraphSchema, graph: &Graph) -> Result<LoadReport> {
1954 schema.validate_graph(graph)?;
1955 self.apply_schema(schema).await?;
1956 self.put_graph(graph).await
1957 }
1958
1959 async fn get_node(&self, id: &NodeId) -> Result<Option<Node>>;
1960
1961 async fn get_nodes(&self, ids: &[NodeId]) -> Result<Vec<Node>> {
1968 let mut nodes = Vec::new();
1969 for id in ids {
1970 if let Some(node) = self.get_node(id).await? {
1971 nodes.push(node);
1972 }
1973 }
1974 Ok(nodes)
1975 }
1976
1977 async fn get_edges(&self, query: EdgeQuery) -> Result<Vec<Edge>>;
1978 async fn traverse(&self, traversal: Traversal) -> Result<Vec<Node>>;
1979}
1980
1981#[async_trait]
1982pub trait GraphAdminStore: GraphStore {
1983 async fn bootstrap(&self) -> Result<()> {
1984 Ok(())
1985 }
1986
1987 async fn clear(&self) -> Result<()>;
1988}
1989
1990#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1992pub enum GraphMutation {
1993 UpsertNode(Node),
1994 DeleteNode(NodeId),
1995 UpsertEdge(Edge),
1996 DeleteEdge {
1997 from: NodeId,
1998 label: Label,
1999 to: NodeId,
2000 },
2001}
2002
2003#[async_trait]
2008pub trait GraphMutationStore: GraphStore {
2009 async fn delete_node(&self, id: &NodeId) -> Result<()>;
2011
2012 async fn delete_edge(&self, from: &NodeId, label: &Label, to: &NodeId) -> Result<()>;
2014
2015 async fn apply_mutations(&self, mutations: &[GraphMutation]) -> Result<()> {
2022 for mutation in mutations {
2023 match mutation {
2024 GraphMutation::UpsertNode(node) => {
2025 self.put_node(node).await?;
2026 }
2027 GraphMutation::DeleteNode(id) => self.delete_node(id).await?,
2028 GraphMutation::UpsertEdge(edge) => {
2029 self.put_edge(edge).await?;
2030 }
2031 GraphMutation::DeleteEdge { from, label, to } => {
2032 self.delete_edge(from, label, to).await?
2033 }
2034 }
2035 }
2036 Ok(())
2037 }
2038}
2039
2040pub mod prelude {
2041 pub use crate::{
2042 Direction, Edge, EdgeId, EdgePolicy, EdgeQuery, EdgeType, EdgeUniqueness, Field, FieldType,
2043 Graph, GraphAdminStore, GraphBuilder, GraphMutation, GraphMutationStore, GraphSchema,
2044 GraphSchemaBuilder, GraphStore, GrustError, Label, LoadReport, Node, NodeId, NodeType,
2045 Props, PutOutcome, Result, RfcDate, Start, Step, Traversal, Value, edge_key,
2046 relationship_type, schema_identifier,
2047 };
2048
2049 #[cfg(feature = "typed-garde")]
2050 pub use crate::typed::{TypedEdge, TypedGraphBuilder, TypedNode, garde, props_from_serialize};
2051
2052 #[cfg(feature = "typed-zod-rs")]
2053 pub use crate::typed::{parse_typed_json, parse_typed_json_with, zod_rs};
2054}
2055
2056#[cfg(test)]
2057mod tests;