1use super::{UserConfig, UserFieldConfig, UserFieldType};
2use crate::store::schema::resolve_field_name;
3#[cfg(test)]
4use crate::user_fields::{FieldTransforms, UserFieldTransform};
5use crate::{AuthError, AuthResult};
6use crate::{FieldMap, FieldValue as Value};
7
8impl UserConfig {
9 pub async fn organization_storage_fields(
12 &self,
13 core: FieldMap,
14 extras: FieldMap,
15 create: bool,
16 ) -> AuthResult<FieldMap> {
17 self.organization_storage_fields_with_binding(core, extras, create, |_, _, value| Ok(value))
18 .await
19 }
20
21 #[doc(hidden)]
23 pub async fn organization_storage_fields_with_binding(
24 &self,
25 core: FieldMap,
26 extras: FieldMap,
27 create: bool,
28 bind: impl Fn(&str, &UserFieldConfig, Value) -> AuthResult<Value>,
29 ) -> AuthResult<FieldMap> {
30 let mut output = core.clone();
31 output.retain(|name, _| name == "id" || !self.fields().contains_key(name));
32 let mut input = extras;
33 input.extend(core);
34 output.extend(self.storage_fields_async(input, create, true, bind).await?);
35 Ok(output)
36 }
37
38 pub fn parse_organization_input(
40 &self,
41 input: &FieldMap,
42 prefix: &str,
43 partial: bool,
44 ) -> AuthResult<FieldMap> {
45 let mut parsed = FieldMap::new();
46 let mut errors = Vec::new();
47 for (name, field) in self.fields() {
48 match field.validate_organization_input(
49 input.get(name),
50 &format!("{prefix}.{name}"),
51 partial,
52 ) {
53 Ok(Some(value)) => {
54 let _ = parsed.insert(name.clone(), value);
55 }
56 Ok(None) => {}
57 Err(AuthError::FieldInput { message, .. }) => errors.push(message),
58 Err(error) => return Err(error),
59 }
60 }
61 if errors.is_empty() {
62 Ok(parsed)
63 } else {
64 Err(AuthError::FieldInput {
65 code: "VALIDATION_ERROR",
66 message: errors.join("; "),
67 })
68 }
69 }
70
71 pub async fn output_fields(&self, storage: &FieldMap) -> AuthResult<FieldMap> {
73 Ok(self
75 .output_fields_many(std::slice::from_ref(storage))
76 .await?
77 .remove(0))
78 }
79
80 pub async fn output_fields_many(&self, storage: &[FieldMap]) -> AuthResult<Vec<FieldMap>> {
82 self.output_fields_many_with_json(storage, |_| true).await
83 }
84
85 pub(crate) async fn output_memory_fields_many(
86 &self,
87 storage: &[FieldMap],
88 ) -> AuthResult<Vec<FieldMap>> {
89 self.output_fields_many_with_json(storage, UserFieldConfig::references_id)
90 .await
91 }
92
93 async fn output_fields_many_with_json(
94 &self,
95 storage: &[FieldMap],
96 supports_native_json: impl Fn(&UserFieldConfig) -> bool + Sync,
97 ) -> AuthResult<Vec<FieldMap>> {
98 let mut rows: Vec<_> = storage
99 .iter()
100 .map(|storage| (storage, FieldMap::new()))
101 .collect();
102 super::batch::project_fields(
103 &mut rows,
104 self.fields(),
105 |(storage, output), name, field| {
106 let supports_native_json = supports_native_json(field);
107 Box::pin(async move {
108 let value = storage
109 .get(resolve_field_name(field.field_name.as_deref(), name))
110 .cloned();
111 assign_output(
112 output,
113 name,
114 field,
115 field
116 .adapter_output(value.unwrap_or_default(), supports_native_json)
117 .await?,
118 )
119 })
120 },
121 )
122 .await?;
123 Ok(rows.into_iter().map(|(_, output)| output).collect())
124 }
125
126 pub fn filter_returned_fields(&self, fields: &mut FieldMap) {
128 fields.retain(|name, _| self.fields().get(name).is_none_or(|field| field.returned()));
129 }
130}
131
132impl UserFieldConfig {
133 pub fn validate_organization_input(
135 &self,
136 value: Option<&Value>,
137 location: &str,
138 partial: bool,
139 ) -> AuthResult<Option<Value>> {
140 if !self.input() || value.is_none() && (partial || self.required == Some(false)) {
141 return Ok(None);
142 }
143 if matches!(value, Some(Value::Null | Value::Undefined)) && self.required == Some(false) {
144 return Ok(value.cloned());
145 }
146 let expected = match &self.field_type {
147 UserFieldType::Enum(_) => return Ok(value.cloned()),
148 UserFieldType::String => "string",
149 UserFieldType::Number => "number",
150 UserFieldType::Boolean => "boolean",
151 UserFieldType::Date => "date",
152 UserFieldType::Json => "json",
153 UserFieldType::StringArray | UserFieldType::NumberArray => "array",
154 };
155 let mut errors = Vec::new();
156 let valid = match (&self.field_type, value) {
157 (UserFieldType::String, Some(Value::String(_) | Value::Utf16String(_)))
158 | (UserFieldType::Boolean, Some(Value::Bool(_))) => true,
159 (UserFieldType::Number, Some(Value::Number(value))) => value.is_finite(),
160 (UserFieldType::Json, Some(value)) => json_input(value),
161 (UserFieldType::Date, Some(Value::Date(value))) => value.milliseconds().is_finite(),
162 (
163 UserFieldType::StringArray | UserFieldType::NumberArray,
164 Some(Value::Array(values)),
165 ) => {
166 let item_type = if matches!(self.field_type, UserFieldType::StringArray) {
167 "string"
168 } else {
169 "number"
170 };
171 for (index, value) in values.iter().enumerate() {
172 if type_name(Some(value)) != item_type
173 || matches!(value, Value::Number(number) if !number.is_finite())
174 {
175 errors.push(invalid_type(
176 &format!("{location}.{index}"),
177 item_type,
178 Some(value),
179 ));
180 }
181 }
182 true
183 }
184 _ => false,
185 };
186 if !valid {
187 errors.push(if matches!(self.field_type, UserFieldType::Json) {
188 format!("[{location}] Invalid input")
189 } else {
190 invalid_type(location, expected, value)
191 });
192 }
193 if errors.is_empty() {
194 Ok(value.cloned())
195 } else {
196 Err(AuthError::FieldInput {
197 code: "VALIDATION_ERROR",
198 message: errors.join("; "),
199 })
200 }
201 }
202}
203
204fn json_input(value: &Value) -> bool {
205 match value {
206 Value::Null | Value::Bool(_) | Value::String(_) | Value::Utf16String(_) => true,
207 Value::Number(value) => value.is_finite(),
208 Value::Array(values) => values.iter().all(json_input),
209 Value::Object(values) => values.values().all(json_input),
210 Value::Undefined | Value::Date(_) => false,
211 }
212}
213
214fn type_name(value: Option<&Value>) -> &'static str {
215 match value {
216 None | Some(Value::Undefined) => "undefined",
217 Some(Value::Null) => "null",
218 Some(Value::Bool(_)) => "boolean",
219 Some(Value::Number(value)) if value.is_nan() => "NaN",
220 Some(Value::Number(value)) if *value == f64::INFINITY => "Infinity",
221 Some(Value::Number(value)) if *value == f64::NEG_INFINITY => "-Infinity",
222 Some(Value::Number(_)) => "number",
223 Some(Value::String(_) | Value::Utf16String(_)) => "string",
224 Some(Value::Date(_)) => "Date",
225 Some(Value::Array(_)) => "array",
226 Some(Value::Object(_)) => "object",
227 }
228}
229
230fn invalid_type(location: &str, expected: &str, value: Option<&Value>) -> String {
231 format!(
232 "[{location}] Invalid input: expected {expected}, received {}",
233 type_name(value)
234 )
235}
236
237#[cfg(test)]
238mod tests {
239 use super::*;
240 use crate::user_fields::UserFieldConfig;
241 macro_rules! json {
242 ($($token:tt)*) => { Value::from_json(serde_json::json!($($token)*)).expect("valid JSON field") };
243 }
244 use std::sync::Arc;
245
246 #[test]
247 fn native_json_fields_reject_values_that_json_would_omit_or_change() {
248 let field = UserFieldConfig {
249 field_type: UserFieldType::Json,
250 ..Default::default()
251 };
252 for value in [
253 Value::Undefined,
254 Value::from(crate::FieldDate::from_milliseconds(0.0)),
255 Value::from(f64::NAN),
256 Value::from(vec![Value::from(f64::INFINITY)]),
257 Value::from(FieldMap::from([("omitted".into(), Value::Undefined)])),
258 ] {
259 let error = field
260 .validate_organization_input(Some(&value), "body.payload", false)
261 .unwrap_err();
262 assert!(
263 matches!(error, AuthError::FieldInput { code: "VALIDATION_ERROR", message } if message == "[body.payload] Invalid input")
264 );
265 }
266 let lone_surrogate = Value::from(crate::Utf16String::from_units(vec![0xd800]));
267 let object = Value::from(FieldMap::from([("text".into(), lone_surrogate)]));
268 let output = field
269 .validate_organization_input(Some(&object), "body.payload", false)
270 .unwrap()
271 .unwrap();
272 assert!(output.strict_equals(&object));
273 }
274
275 #[test]
276 fn native_schema_validation_preserves_dates_and_rejects_nonfinite_numbers() {
277 let field = UserFieldConfig {
278 field_type: UserFieldType::Date,
279 required: Some(false),
280 ..Default::default()
281 };
282 let date = Value::from(crate::FieldDate::from_milliseconds(0.0));
283 let output = field
284 .validate_organization_input(Some(&date), "body.date", false)
285 .unwrap()
286 .unwrap();
287 assert!(output.strict_equals(&date));
288 assert_eq!(
289 field
290 .validate_organization_input(Some(&Value::Undefined), "body.date", false)
291 .unwrap(),
292 Some(Value::Undefined)
293 );
294 let invalid = Value::from(crate::FieldDate::invalid());
295 assert!(
296 matches!(field.validate_organization_input(Some(&invalid), "body.date", false).unwrap_err(), AuthError::FieldInput { message, .. } if message == "[body.date] Invalid input: expected date, received Date")
297 );
298 let field = UserFieldConfig {
299 field_type: UserFieldType::Number,
300 ..Default::default()
301 };
302 for (number, label) in [
303 (f64::NAN, "NaN"),
304 (f64::INFINITY, "Infinity"),
305 (f64::NEG_INFINITY, "-Infinity"),
306 ] {
307 assert!(
308 matches!(field.validate_organization_input(Some(&Value::from(number)), "body.number", false).unwrap_err(), AuthError::FieldInput { message, .. } if message == format!("[body.number] Invalid input: expected number, received {label}"))
309 );
310 }
311 }
312
313 #[tokio::test]
314 async fn organization_validation_keeps_route_and_adapter_policies_separate() {
315 let schema = UserConfig {
316 additional_fields: Some(
317 [
318 (
319 "implicit".into(),
320 UserFieldConfig {
321 default_value: Some(json!("default")),
322 ..Default::default()
323 },
324 ),
325 (
326 "label".into(),
327 UserFieldConfig {
328 required: Some(false),
329 field_name: Some("stored_label".into()),
330 validator: Some(Arc::new(|_| {
331 Err(AuthError::bad_request("must not run"))
332 })),
333 transform: Some(FieldTransforms {
334 input: Some(UserFieldTransform::new(|value| {
335 Ok(json!(format!("{}:in", value.as_str().unwrap())))
336 })),
337 ..Default::default()
338 }),
339 ..Default::default()
340 },
341 ),
342 (
343 "protected".into(),
344 UserFieldConfig {
345 input: Some(false),
346 default_value: Some(json!("server")),
347 ..Default::default()
348 },
349 ),
350 ]
351 .into(),
352 ),
353 };
354 let error = schema
355 .parse_organization_input(&FieldMap::new(), "body", false)
356 .unwrap_err();
357 assert!(
358 error
359 .to_string()
360 .contains("[body.implicit] Invalid input: expected string, received undefined")
361 );
362 let input =
363 json!({"implicit":"supplied", "label":"raw", "protected":"client", "unknown":true});
364 let parsed = schema
365 .parse_organization_input(input.as_object().unwrap(), "body", false)
366 .unwrap();
367 assert_eq!(
368 parsed,
369 json!({"implicit":"supplied", "label":"raw"})
370 .as_object()
371 .unwrap()
372 .clone()
373 );
374 let stored = schema.storage_fields(parsed, true).await.unwrap();
375 assert_eq!(
376 stored,
377 json!({"implicit":"supplied", "stored_label":"raw:in", "protected":"server"})
378 .as_object()
379 .unwrap()
380 .clone()
381 );
382 assert!(
383 schema
384 .parse_organization_input(&FieldMap::new(), "body.data", true)
385 .unwrap()
386 .is_empty()
387 );
388 }
389
390 #[test]
391 fn organization_partial_fields_keep_nullable_and_array_contracts() {
392 let schema = UserConfig {
393 additional_fields: Some(
394 [
395 (
396 "required".into(),
397 UserFieldConfig {
398 required: Some(true),
399 ..Default::default()
400 },
401 ),
402 (
403 "optional".into(),
404 UserFieldConfig {
405 required: Some(false),
406 ..Default::default()
407 },
408 ),
409 (
410 "tags".into(),
411 UserFieldConfig {
412 required: Some(false),
413 field_type: UserFieldType::StringArray,
414 ..Default::default()
415 },
416 ),
417 (
418 "category".into(),
419 UserFieldConfig {
420 field_type: UserFieldType::Enum(vec!["basic".into()]),
421 ..Default::default()
422 },
423 ),
424 ]
425 .into(),
426 ),
427 };
428 let valid = json!({"optional":null,"category":"unlisted"});
429 assert_eq!(
430 schema
431 .parse_organization_input(valid.as_object().unwrap(), "body.data", true)
432 .unwrap(),
433 valid.as_object().unwrap().clone()
434 );
435 let invalid = json!({"required":null,"tags":["valid",4]});
436 assert!(
437 schema
438 .parse_organization_input(invalid.as_object().unwrap(), "body.data", true)
439 .unwrap_err()
440 .to_string()
441 .contains("[body.data.tags.1] Invalid input: expected string, received number")
442 );
443 }
444}
445
446pub(crate) fn assign_output(
447 output: &mut FieldMap,
448 name: &str,
449 field: &UserFieldConfig,
450 mut value: Value,
451) -> AuthResult<()> {
452 if !field.uses_id_output() {
453 field.normalize_date(&mut value)?;
454 }
455 let _ = output.insert(name.to_owned(), value);
456 Ok(())
457}