fraiseql_core/validation/scalar_validator.rs
1//! Validation engine for custom GraphQL scalars.
2//!
3//! Provides utilities to validate custom scalar values in different contexts.
4
5use serde_json::Value;
6
7use super::custom_scalar::CustomScalar;
8use crate::error::{FraiseQLError, Result};
9
10/// Validation context for custom scalar operations.
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum ValidationContext {
13 /// Serialize a database value to GraphQL response.
14 Serialize,
15
16 /// Parse a variable value from GraphQL operation.
17 ParseValue,
18
19 /// Parse a literal value from GraphQL query string.
20 ParseLiteral,
21}
22
23impl ValidationContext {
24 /// Get the string representation of this context.
25 pub fn as_str(&self) -> &'static str {
26 match self {
27 Self::Serialize => "serialize",
28 Self::ParseValue => "parseValue",
29 Self::ParseLiteral => "parseLiteral",
30 }
31 }
32}
33
34/// Error returned when custom scalar validation fails.
35#[derive(Debug, Clone)]
36pub struct ScalarValidationError {
37 /// Name of the scalar that failed validation.
38 pub scalar_name: String,
39
40 /// Context in which validation occurred.
41 pub context: String,
42
43 /// Underlying error message.
44 pub message: String,
45}
46
47impl ScalarValidationError {
48 /// Create a new scalar validation error.
49 pub fn new(
50 scalar_name: impl Into<String>,
51 context: impl Into<String>,
52 message: impl Into<String>,
53 ) -> Self {
54 Self {
55 scalar_name: scalar_name.into(),
56 context: context.into(),
57 message: message.into(),
58 }
59 }
60
61 /// Convert to FraiseQLError.
62 pub fn into_fraiseql_error(self) -> FraiseQLError {
63 FraiseQLError::validation(self.to_string())
64 }
65}
66
67impl std::fmt::Display for ScalarValidationError {
68 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69 write!(
70 f,
71 "Scalar \"{}\" validation failed in {}: {}",
72 self.scalar_name, self.context, self.message
73 )
74 }
75}
76
77impl std::error::Error for ScalarValidationError {}
78
79/// Validate a custom scalar value in a given context.
80///
81/// # Arguments
82///
83/// * `scalar` - The custom scalar implementation
84/// * `value` - The value to validate
85/// * `context` - The validation context
86///
87/// # Errors
88///
89/// Returns `ScalarValidationError` if validation fails.
90///
91/// # Example
92///
93/// ```ignore
94/// use fraiseql_core::validation::{validate_custom_scalar, ValidationContext, CustomScalar};
95/// use serde_json::json;
96///
97/// struct Email;
98/// impl CustomScalar for Email {
99/// fn name(&self) -> &str { "Email" }
100/// fn serialize(&self, value: &serde_json::Value) -> Result<serde_json::Value> { Ok(value.clone()) }
101/// fn parse_value(&self, value: &serde_json::Value) -> Result<serde_json::Value> {
102/// let str_val = value.as_str().unwrap();
103/// if !str_val.contains('@') {
104/// return Err(crate::error::FraiseQLError::validation("invalid email"));
105/// }
106/// Ok(value.clone())
107/// }
108/// fn parse_literal(&self, ast: &serde_json::Value) -> Result<serde_json::Value> {
109/// self.parse_value(ast)
110/// }
111/// }
112///
113/// let email = Email;
114/// let result = validate_custom_scalar(&email, &json!("test@example.com"), ValidationContext::ParseValue)?;
115/// assert_eq!(result, json!("test@example.com"));
116/// ```
117pub fn validate_custom_scalar(
118 scalar: &dyn CustomScalar,
119 value: &Value,
120 context: ValidationContext,
121) -> Result<Value> {
122 match context {
123 ValidationContext::Serialize => scalar.serialize(value).map_err(|e| {
124 FraiseQLError::validation(format!(
125 "Scalar \"{}\" validation failed in serialize: {}",
126 scalar.name(),
127 e
128 ))
129 }),
130
131 ValidationContext::ParseValue => scalar.parse_value(value).map_err(|e| {
132 FraiseQLError::validation(format!(
133 "Scalar \"{}\" validation failed in parseValue: {}",
134 scalar.name(),
135 e
136 ))
137 }),
138
139 ValidationContext::ParseLiteral => scalar.parse_literal(value).map_err(|e| {
140 FraiseQLError::validation(format!(
141 "Scalar \"{}\" validation failed in parseLiteral: {}",
142 scalar.name(),
143 e
144 ))
145 }),
146 }
147}
148
149/// Convenience function that defaults context to ParseValue.
150pub fn validate_custom_scalar_parse_value(
151 scalar: &dyn CustomScalar,
152 value: &Value,
153) -> Result<Value> {
154 validate_custom_scalar(scalar, value, ValidationContext::ParseValue)
155}