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