fraiseql_core/validation/
input_processor.rs1use std::collections::HashSet;
10
11use serde_json::{Map, Value};
12
13use super::id_policy::{IDPolicy, validate_id};
14
15#[derive(Debug, Clone)]
17pub struct InputProcessingConfig {
18 pub id_policy: IDPolicy,
20
21 pub validate_ids: bool,
23
24 pub id_field_names: HashSet<String>,
27}
28
29impl Default for InputProcessingConfig {
30 fn default() -> Self {
31 Self {
32 id_policy: IDPolicy::default(),
33 validate_ids: true,
34 id_field_names: Self::default_id_field_names(),
35 }
36 }
37}
38
39impl InputProcessingConfig {
40 fn default_id_field_names() -> HashSet<String> {
42 [
43 "id",
44 "userId",
45 "user_id",
46 "postId",
47 "post_id",
48 "commentId",
49 "comment_id",
50 "authorId",
51 "author_id",
52 "ownerId",
53 "owner_id",
54 "creatorId",
55 "creator_id",
56 "tenantId",
57 "tenant_id",
58 ]
59 .iter()
60 .map(|s| (*s).to_string())
61 .collect()
62 }
63
64 pub fn add_id_field(&mut self, field_name: String) {
66 self.id_field_names.insert(field_name);
67 }
68
69 #[must_use]
71 pub fn strict_uuid() -> Self {
72 Self {
73 id_policy: IDPolicy::UUID,
74 validate_ids: true,
75 id_field_names: Self::default_id_field_names(),
76 }
77 }
78
79 #[must_use]
81 pub fn opaque() -> Self {
82 Self {
83 id_policy: IDPolicy::OPAQUE,
84 validate_ids: false, id_field_names: Self::default_id_field_names(),
86 }
87 }
88}
89
90pub fn process_variables(
124 variables: &Value,
125 config: &InputProcessingConfig,
126) -> Result<Value, ProcessingError> {
127 if !config.validate_ids {
128 return Ok(variables.clone());
129 }
130
131 match variables {
132 Value::Object(obj) => {
133 let mut result = Map::new();
134
135 for (key, value) in obj {
136 let processed_value = process_value(value, config, key)?;
137 result.insert(key.clone(), processed_value);
138 }
139
140 Ok(Value::Object(result))
141 },
142 Value::Null => Ok(Value::Null),
143 other => Ok(other.clone()),
144 }
145}
146
147fn process_value(
149 value: &Value,
150 config: &InputProcessingConfig,
151 field_name: &str,
152) -> Result<Value, ProcessingError> {
153 match value {
154 Value::String(s)
157 if {
158 let base_field = field_name.split('[').next().unwrap_or(field_name);
159 config.id_field_names.contains(base_field)
160 } =>
161 {
162 validate_id(s, config.id_policy).map_err(|e| ProcessingError {
163 field_path: field_name.to_string(),
164 reason: format!("Invalid ID value: {e}"),
165 })?;
166 Ok(Value::String(s.clone()))
167 },
168
169 Value::Object(obj) => {
171 let mut result = Map::new();
172
173 for (key, nested_value) in obj {
174 let processed = process_value(nested_value, config, key)?;
175 result.insert(key.clone(), processed);
176 }
177
178 Ok(Value::Object(result))
179 },
180
181 Value::Array(arr) => {
183 let processed_items: Result<Vec<_>, _> = arr
184 .iter()
185 .enumerate()
186 .map(|(idx, item)| {
187 let array_field = format!("{field_name}[{idx}]");
188 process_value(item, config, &array_field)
189 })
190 .collect();
191
192 Ok(Value::Array(processed_items?))
193 },
194
195 other => Ok(other.clone()),
197 }
198}
199
200#[derive(Debug, Clone, PartialEq, Eq)]
202pub struct ProcessingError {
203 pub field_path: String,
205 pub reason: String,
207}
208
209impl std::fmt::Display for ProcessingError {
210 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
211 write!(f, "Error in field '{}': {}", self.field_path, self.reason)
212 }
213}
214
215impl std::error::Error for ProcessingError {}