fraiseql_core/validation/
scalar_validator.rs1use serde_json::Value;
6
7use super::custom_scalar::CustomScalar;
8use crate::error::{FraiseQLError, Result};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12#[non_exhaustive]
13pub enum ValidationContext {
14 Serialize,
16
17 ParseValue,
19
20 ParseLiteral,
22}
23
24impl ValidationContext {
25 pub const fn as_str(&self) -> &'static str {
27 match self {
28 Self::Serialize => "serialize",
29 Self::ParseValue => "parseValue",
30 Self::ParseLiteral => "parseLiteral",
31 }
32 }
33}
34
35#[derive(Debug, Clone)]
37pub struct ScalarValidationError {
38 pub scalar_name: String,
40
41 pub context: String,
43
44 pub message: String,
46}
47
48impl ScalarValidationError {
49 pub fn new(
51 scalar_name: impl Into<String>,
52 context: impl Into<String>,
53 message: impl Into<String>,
54 ) -> Self {
55 Self {
56 scalar_name: scalar_name.into(),
57 context: context.into(),
58 message: message.into(),
59 }
60 }
61
62 pub fn into_fraiseql_error(self) -> FraiseQLError {
64 FraiseQLError::validation(self.to_string())
65 }
66}
67
68impl std::fmt::Display for ScalarValidationError {
69 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
70 write!(
71 f,
72 "Scalar \"{}\" validation failed in {}: {}",
73 self.scalar_name, self.context, self.message
74 )
75 }
76}
77
78impl std::error::Error for ScalarValidationError {}
79
80pub fn validate_custom_scalar(
121 scalar: &dyn CustomScalar,
122 value: &Value,
123 context: ValidationContext,
124) -> Result<Value> {
125 match context {
126 ValidationContext::Serialize => scalar.serialize(value).map_err(|e| {
127 FraiseQLError::validation(format!(
128 "Scalar \"{}\" validation failed in serialize: {}",
129 scalar.name(),
130 e
131 ))
132 }),
133
134 ValidationContext::ParseValue => scalar.parse_value(value).map_err(|e| {
135 FraiseQLError::validation(format!(
136 "Scalar \"{}\" validation failed in parseValue: {}",
137 scalar.name(),
138 e
139 ))
140 }),
141
142 ValidationContext::ParseLiteral => scalar.parse_literal(value).map_err(|e| {
143 FraiseQLError::validation(format!(
144 "Scalar \"{}\" validation failed in parseLiteral: {}",
145 scalar.name(),
146 e
147 ))
148 }),
149 }
150}
151
152pub fn validate_custom_scalar_parse_value(
158 scalar: &dyn CustomScalar,
159 value: &Value,
160) -> Result<Value> {
161 validate_custom_scalar(scalar, value, ValidationContext::ParseValue)
162}
163
164#[cfg(test)]
165mod tests {
166 #![allow(clippy::unwrap_used)] use serde_json::{Value, json};
169
170 use super::*;
171 use crate::error::{FraiseQLError, Result};
172
173 #[derive(Debug)]
175 struct PassthroughScalar;
176
177 #[allow(clippy::unnecessary_literal_bound)] impl CustomScalar for PassthroughScalar {
179 fn name(&self) -> &str {
180 "Passthrough"
181 }
182
183 fn serialize(&self, value: &Value) -> Result<Value> {
184 Ok(value.clone())
185 }
186
187 fn parse_value(&self, value: &Value) -> Result<Value> {
188 Ok(value.clone())
189 }
190
191 fn parse_literal(&self, ast: &Value) -> Result<Value> {
192 Ok(ast.clone())
193 }
194 }
195
196 #[derive(Debug)]
198 struct FailScalar;
199
200 #[allow(clippy::unnecessary_literal_bound)] impl CustomScalar for FailScalar {
202 fn name(&self) -> &str {
203 "AlwaysFail"
204 }
205
206 fn serialize(&self, _: &Value) -> Result<Value> {
207 Err(FraiseQLError::validation("serialize always fails"))
208 }
209
210 fn parse_value(&self, _: &Value) -> Result<Value> {
211 Err(FraiseQLError::validation("parse_value always fails"))
212 }
213
214 fn parse_literal(&self, _: &Value) -> Result<Value> {
215 Err(FraiseQLError::validation("parse_literal always fails"))
216 }
217 }
218
219 #[test]
222 fn test_validation_context_as_str_serialize() {
223 assert_eq!(ValidationContext::Serialize.as_str(), "serialize");
224 }
225
226 #[test]
227 fn test_validation_context_as_str_parse_value() {
228 assert_eq!(ValidationContext::ParseValue.as_str(), "parseValue");
229 }
230
231 #[test]
232 fn test_validation_context_as_str_parse_literal() {
233 assert_eq!(ValidationContext::ParseLiteral.as_str(), "parseLiteral");
234 }
235
236 #[test]
237 fn test_validation_context_eq() {
238 assert_eq!(ValidationContext::Serialize, ValidationContext::Serialize);
239 assert_ne!(ValidationContext::Serialize, ValidationContext::ParseValue);
240 }
241
242 #[test]
245 fn test_scalar_validation_error_new() {
246 let err = ScalarValidationError::new("Email", "parseValue", "not an email");
247 assert_eq!(err.scalar_name, "Email");
248 assert_eq!(err.context, "parseValue");
249 assert_eq!(err.message, "not an email");
250 }
251
252 #[test]
253 fn test_scalar_validation_error_display() {
254 let err = ScalarValidationError::new("Email", "parseValue", "bad input");
255 let s = format!("{err}");
256 assert!(s.contains("Email"), "missing scalar name: {s}");
257 assert!(s.contains("parseValue"), "missing context: {s}");
258 assert!(s.contains("bad input"), "missing message: {s}");
259 }
260
261 #[test]
262 fn test_scalar_validation_error_into_fraiseql_error() {
263 let err = ScalarValidationError::new("T", "serialize", "oops");
264 let fraiseql_err = err.into_fraiseql_error();
265 let msg = format!("{fraiseql_err}");
266 assert!(msg.contains("oops"), "error message lost: {msg}");
267 }
268
269 #[test]
272 fn test_validate_serialize_success() {
273 let scalar = PassthroughScalar;
274 let v = json!("hello");
275 let result = validate_custom_scalar(&scalar, &v, ValidationContext::Serialize);
276 assert_eq!(result.unwrap(), v);
277 }
278
279 #[test]
280 fn test_validate_parse_value_success() {
281 let scalar = PassthroughScalar;
282 let v = json!(42);
283 let result = validate_custom_scalar(&scalar, &v, ValidationContext::ParseValue);
284 assert_eq!(result.unwrap(), v);
285 }
286
287 #[test]
288 fn test_validate_parse_literal_success() {
289 let scalar = PassthroughScalar;
290 let v = json!(true);
291 let result = validate_custom_scalar(&scalar, &v, ValidationContext::ParseLiteral);
292 assert_eq!(result.unwrap(), v);
293 }
294
295 #[test]
296 fn test_validate_serialize_failure_wraps_error() {
297 let scalar = FailScalar;
298 let err =
299 validate_custom_scalar(&scalar, &json!("x"), ValidationContext::Serialize).unwrap_err();
300 let msg = format!("{err}");
301 assert!(
302 msg.contains("AlwaysFail") || msg.contains("serialize"),
303 "unexpected error message: {msg}"
304 );
305 }
306
307 #[test]
308 fn test_validate_parse_value_failure_wraps_error() {
309 let scalar = FailScalar;
310 let err = validate_custom_scalar(&scalar, &json!("x"), ValidationContext::ParseValue)
311 .unwrap_err();
312 let msg = format!("{err}");
313 assert!(
314 msg.contains("AlwaysFail") || msg.contains("parseValue"),
315 "unexpected error message: {msg}"
316 );
317 }
318
319 #[test]
320 fn test_validate_parse_literal_failure_wraps_error() {
321 let scalar = FailScalar;
322 let err = validate_custom_scalar(&scalar, &json!("x"), ValidationContext::ParseLiteral)
323 .unwrap_err();
324 let msg = format!("{err}");
325 assert!(
326 msg.contains("AlwaysFail") || msg.contains("parseLiteral"),
327 "unexpected error message: {msg}"
328 );
329 }
330
331 #[test]
334 fn test_convenience_fn_success() {
335 let scalar = PassthroughScalar;
336 let v = json!("text");
337 assert_eq!(validate_custom_scalar_parse_value(&scalar, &v).unwrap(), v);
338 }
339
340 #[test]
341 fn test_convenience_fn_failure() {
342 let scalar = FailScalar;
343 let result = validate_custom_scalar_parse_value(&scalar, &json!("x"));
344 assert!(
345 matches!(result, Err(FraiseQLError::Validation { .. })),
346 "expected Validation error, got: {result:?}"
347 );
348 }
349}