Skip to main content

alibi_core/
field_policy.rs

1//! Application and plugin field policies at the session input boundary.
2use crate::AdapterOutput;
3use crate::utils::json::JsValue;
4use crate::{AuthError, AuthResult};
5use indexmap::{IndexMap, IndexSet};
6use serde_json::Value;
7use std::{fmt, future::Future, pin::Pin, sync::Arc};
8
9/// Values retain JavaScript numbers until the actual database binding.
10#[derive(Clone, Default)]
11pub struct FieldValues {
12    values: IndexMap<String, JsValue>,
13    adapter_fields: Option<Arc<FieldConfigs>>,
14    undefined_input_keys: IndexSet<String>,
15    transform_omitted: bool,
16    binding_names: IndexMap<String, String>,
17}
18
19impl FieldValues {
20    #[must_use]
21    pub fn new() -> Self {
22        Self::default()
23    }
24    /// Whether parsing supplied any keys, including a transform returning undefined.
25    /// Unlike `is_empty`, this reflects JavaScript Object.keys before adapter omission.
26    #[must_use]
27    pub fn has_input_fields(&self) -> bool {
28        !self.values.is_empty() || !self.undefined_input_keys.is_empty()
29    }
30    /// Resolve an immutable configured physical column at the binding boundary.
31    #[must_use]
32    pub fn binding_name<'a>(&'a self, name: &'a str) -> &'a str {
33        self.binding_names.get(name).map_or(name, String::as_str)
34    }
35    /// Consume pending adapter callbacks once using current values after database hooks.
36    /// Custom stores must call this immediately before binding configured session fields.
37    ///
38    /// # Errors
39    ///
40    /// Propagates errors from configured adapter field transforms.
41    pub fn apply_adapter_transforms(&mut self) -> AuthResult<()> {
42        if self.adapter_fields.as_ref().is_some_and(|fields| {
43            fields
44                .values()
45                .any(|field| field.adapter_input_transform.is_some())
46        }) {
47            return Err(AuthError::config(
48                "Asynchronous adapter transforms require apply_adapter_transforms_async",
49            ));
50        }
51        if let Some(fields) = self.adapter_fields.take() {
52            for (name, field) in &*fields {
53                if self.prepare_adapter_value(name, field)
54                    && let Some(transform) = &field.transform
55                {
56                    let value = transform(self.values.get(name))?;
57                    self.set_adapter_value(name, value);
58                }
59            }
60        }
61        self.finish_adapter_transforms();
62        Ok(())
63    }
64
65    /// Await genuine storage-stage callbacks after database before hooks.
66    /// Synchronous endpoint parsing is a separate phase.
67    ///
68    /// # Errors
69    /// Propagates configured callback errors.
70    pub async fn apply_adapter_transforms_async(&mut self) -> AuthResult<()> {
71        if let Some(fields) = self.adapter_fields.take() {
72            for (name, field) in &*fields {
73                if !self.prepare_adapter_value(name, field) {
74                    continue;
75                }
76                let value = if let Some(transform) = &field.adapter_input_transform {
77                    transform(self.values.get(name).cloned())
78                        .await
79                        .map_err(crate::store::adapter::callback_error)?
80                } else if let Some(transform) = &field.transform {
81                    transform(self.values.get(name))?
82                } else {
83                    continue;
84                };
85                self.set_adapter_value(name, value);
86            }
87        }
88        self.finish_adapter_transforms();
89        Ok(())
90    }
91
92    fn prepare_adapter_value(&mut self, name: &str, field: &FieldConfig) -> bool {
93        if !self.transform_omitted && !self.values.contains_key(name) && field.on_update.is_none() {
94            return false;
95        }
96        if self.transform_omitted
97            && (!self.values.contains_key(name)
98                || (field.required && self.values.get(name).is_some_and(JsValue::is_null)))
99            && let Some(default) = &field.default
100        {
101            _ = self.values.insert(name.to_owned(), default.value());
102        }
103        if !self.transform_omitted
104            && !self.values.contains_key(name)
105            && let Some(update) = &field.on_update
106        {
107            _ = self.values.insert(name.to_owned(), update());
108        }
109        true
110    }
111    fn set_adapter_value(&mut self, name: &str, value: Option<JsValue>) {
112        match value {
113            Some(value) => {
114                _ = self.values.insert(name.to_owned(), value);
115            }
116            None => {
117                _ = self.values.shift_remove(name);
118            }
119        }
120    }
121    fn finish_adapter_transforms(&mut self) {
122        self.undefined_input_keys.clear();
123        self.transform_omitted = false;
124    }
125    /// Preserve an existing trusted creation value before adapter defaults are applied.
126    /// Database hooks may already have supplied a mapped replacement, which wins.
127    pub fn preserve_creation_value(&mut self, name: &str, value: JsValue) {
128        if self
129            .adapter_fields
130            .as_ref()
131            .is_some_and(|fields| fields.contains_key(name))
132        {
133            _ = self.values.entry(name.to_owned()).or_insert(value);
134        }
135    }
136}
137
138impl fmt::Debug for FieldValues {
139    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
140        self.values.fmt(f)
141    }
142}
143
144impl std::ops::Deref for FieldValues {
145    type Target = IndexMap<String, JsValue>;
146    fn deref(&self) -> &Self::Target {
147        &self.values
148    }
149}
150
151impl std::ops::DerefMut for FieldValues {
152    fn deref_mut(&mut self) -> &mut Self::Target {
153        &mut self.values
154    }
155}
156
157impl From<IndexMap<String, JsValue>> for FieldValues {
158    fn from(values: IndexMap<String, JsValue>) -> Self {
159        Self {
160            values,
161            ..Self::default()
162        }
163    }
164}
165
166impl FromIterator<(String, JsValue)> for FieldValues {
167    fn from_iter<T: IntoIterator<Item = (String, JsValue)>>(iter: T) -> Self {
168        IndexMap::from_iter(iter).into()
169    }
170}
171
172impl IntoIterator for FieldValues {
173    type Item = (String, JsValue);
174    type IntoIter = indexmap::map::IntoIter<String, JsValue>;
175    fn into_iter(self) -> Self::IntoIter {
176        self.values.into_iter()
177    }
178}
179
180impl<'a> IntoIterator for &'a FieldValues {
181    type Item = (&'a String, &'a JsValue);
182    type IntoIter = indexmap::map::Iter<'a, String, JsValue>;
183    fn into_iter(self) -> Self::IntoIter {
184        self.values.iter()
185    }
186}
187
188impl<'a> IntoIterator for &'a mut FieldValues {
189    type Item = (&'a String, &'a mut JsValue);
190    type IntoIter = indexmap::map::IterMut<'a, String, JsValue>;
191    fn into_iter(self) -> Self::IntoIter {
192        self.values.iter_mut()
193    }
194}
195
196impl serde::Serialize for FieldValues {
197    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
198        self.values.serialize(serializer)
199    }
200}
201
202impl<'de> serde::Deserialize<'de> for FieldValues {
203    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
204        IndexMap::deserialize(deserializer).map(Self::from)
205    }
206}
207
208pub type FieldOutput = serde_json::Map<String, Value>;
209
210pub type FieldConfigs = IndexMap<String, FieldConfig>;
211
212pub type FieldValidator = Arc<dyn Fn(&JsValue) -> Result<JsValue, String> + Send + Sync>;
213pub type AsyncFieldValidator = Arc<
214    dyn Fn(JsValue) -> Pin<Box<dyn Future<Output = Result<JsValue, String>> + Send>> + Send + Sync,
215>;
216
217/// `None` is JavaScript undefined: absent input or omitted adapter output.
218pub type FieldTransform =
219    Arc<dyn Fn(Option<&JsValue>) -> AuthResult<Option<JsValue>> + Send + Sync>;
220
221/// Adapter output callbacks receive an owned actual storage value and are awaited.
222pub type FieldOutputTransform = Arc<
223    dyn Fn(Option<JsValue>) -> Pin<Box<dyn Future<Output = AuthResult<Option<JsValue>>> + Send>>
224        + Send
225        + Sync,
226>;
227
228#[derive(Clone)]
229pub enum FieldDefault {
230    Value(JsValue),
231    Callback(Arc<dyn Fn() -> JsValue + Send + Sync>),
232}
233
234impl fmt::Debug for FieldDefault {
235    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
236        match self {
237            Self::Value(value) => f.debug_tuple("Value").field(value).finish(),
238            Self::Callback(_) => f.write_str("Callback(..)"),
239        }
240    }
241}
242
243impl FieldDefault {
244    #[must_use]
245    pub fn value(&self) -> JsValue {
246        match self {
247            Self::Value(value) => value.clone(),
248            Self::Callback(callback) => callback(),
249        }
250    }
251}
252
253/// Type/schema metadata does not implicitly validate input values.
254#[derive(Clone)]
255pub struct FieldConfig {
256    pub schema: Value,
257    pub required: bool,
258    pub input: bool,
259    pub returned: bool,
260    pub default: Option<FieldDefault>,
261    pub validator: Option<FieldValidator>,
262    pub async_validator: Option<AsyncFieldValidator>,
263    /// Source declares output validators but does not invoke them at this stage.
264    pub output_validator: Option<FieldValidator>,
265    pub adapter_input_transform: Option<FieldOutputTransform>,
266    pub transform: Option<FieldTransform>,
267    pub output_transform: Option<FieldOutputTransform>,
268    pub on_update: Option<Arc<dyn Fn() -> JsValue + Send + Sync>>,
269    /// Physical column binding. Logical input/output names remain registry keys.
270    pub field_name: Option<String>,
271}
272
273impl fmt::Debug for FieldConfig {
274    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
275        f.debug_struct("FieldConfig")
276            .field("schema", &self.schema)
277            .field("required", &self.required)
278            .field("input", &self.input)
279            .field("returned", &self.returned)
280            .field("default", &self.default)
281            .field("validator", &self.validator.as_ref().map(|_| "callback"))
282            .field(
283                "async_validator",
284                &self.async_validator.as_ref().map(|_| "callback"),
285            )
286            .field(
287                "output_validator",
288                &self.output_validator.as_ref().map(|_| "callback"),
289            )
290            .field(
291                "adapter_input_transform",
292                &self.adapter_input_transform.as_ref().map(|_| "callback"),
293            )
294            .field("transform", &self.transform.as_ref().map(|_| "callback"))
295            .field(
296                "output_transform",
297                &self.output_transform.as_ref().map(|_| "callback"),
298            )
299            .field("on_update", &self.on_update.as_ref().map(|_| "callback"))
300            .field("field_name", &self.field_name)
301            .finish()
302    }
303}
304
305impl FieldConfig {
306    #[must_use]
307    pub fn new(schema: Value) -> Self {
308        Self {
309            schema,
310            required: false,
311            input: true,
312            returned: true,
313            default: None,
314            validator: None,
315            async_validator: None,
316            output_validator: None,
317            adapter_input_transform: None,
318            transform: None,
319            output_transform: None,
320            on_update: None,
321            field_name: None,
322        }
323    }
324    #[must_use]
325    pub fn default_value(mut self, value: impl Into<JsValue>) -> Self {
326        self.default = Some(FieldDefault::Value(value.into()));
327        self
328    }
329    #[must_use]
330    pub fn default_callback(
331        mut self,
332        callback: impl Fn() -> JsValue + Send + Sync + 'static,
333    ) -> Self {
334        self.default = Some(FieldDefault::Callback(Arc::new(callback)));
335        self
336    }
337    #[must_use]
338    pub const fn read_only(mut self) -> Self {
339        self.input = false;
340        self
341    }
342    #[must_use]
343    pub const fn hidden(mut self) -> Self {
344        self.returned = false;
345        self
346    }
347    #[must_use]
348    pub fn validate(
349        mut self,
350        validator: impl Fn(&JsValue) -> Result<JsValue, String> + Send + Sync + 'static,
351    ) -> Self {
352        self.validator = Some(Arc::new(validator));
353        self.async_validator = None;
354        self
355    }
356    #[must_use]
357    pub fn transform(
358        mut self,
359        transform: impl Fn(Option<&JsValue>) -> AuthResult<Option<JsValue>> + Send + Sync + 'static,
360    ) -> Self {
361        self.transform = Some(Arc::new(transform));
362        self
363    }
364
365    /// A Promise-style validator is invoked but rejected by endpoint parsing,
366    /// matching the published `ASYNC_VALIDATION_NOT_SUPPORTED` contract.
367    #[must_use]
368    pub fn validate_async<F, Fut>(mut self, validate: F) -> Self
369    where
370        F: Fn(JsValue) -> Fut + Send + Sync + 'static,
371        Fut: Future<Output = Result<JsValue, String>> + Send + 'static,
372    {
373        self.async_validator = Some(Arc::new(move |value| Box::pin(validate(value))));
374        self.validator = None;
375        self
376    }
377
378    #[must_use]
379    pub fn validate_output(
380        mut self,
381        validate: impl Fn(&JsValue) -> Result<JsValue, String> + Send + Sync + 'static,
382    ) -> Self {
383        self.output_validator = Some(Arc::new(validate));
384        self
385    }
386
387    /// Await this callback only at the actual adapter binding boundary. For a
388    /// Source input transform shared with endpoint parsing, use a synchronous
389    /// validator to select its parsed value before the awaited storage phase.
390    #[must_use]
391    pub fn transform_adapter_input<F, Fut>(mut self, transform: F) -> Self
392    where
393        F: Fn(Option<JsValue>) -> Fut + Send + Sync + 'static,
394        Fut: Future<Output = AuthResult<Option<JsValue>>> + Send + 'static,
395    {
396        self.adapter_input_transform = Some(Arc::new(move |value| Box::pin(transform(value))));
397        self
398    }
399
400    #[must_use]
401    pub fn transform_output<F, Fut>(mut self, transform: F) -> Self
402    where
403        F: Fn(Option<JsValue>) -> Fut + Send + Sync + 'static,
404        Fut: Future<Output = AuthResult<Option<JsValue>>> + Send + 'static,
405    {
406        self.output_transform = Some(Arc::new(move |value| Box::pin(transform(value))));
407        self
408    }
409
410    #[must_use]
411    pub fn on_update(mut self, callback: impl Fn() -> JsValue + Send + Sync + 'static) -> Self {
412        self.on_update = Some(Arc::new(callback));
413        self
414    }
415
416    #[must_use]
417    pub fn field_name(mut self, name: impl Into<String>) -> Self {
418        self.field_name = Some(name.into());
419        self
420    }
421}
422
423#[derive(Debug)]
424pub enum FieldInputError {
425    Validation { code: &'static str, message: String },
426    Transform(crate::AuthError),
427}
428
429/// Immutable instance policies: configuration first, registered plugins last.
430#[derive(Debug, Clone, Default)]
431pub struct SessionFields(pub IndexMap<String, FieldConfig>);
432
433impl SessionFields {
434    pub fn defaults(&self, values: &mut FieldValues) {
435        for (name, field) in &self.0 {
436            if let Some(default) = &field.default {
437                _ = values
438                    .entry(name.clone())
439                    .or_insert_with(|| default.value());
440            }
441        }
442    }
443    /// # Errors
444    ///
445    /// Returns a validation error for disallowed fields or rejected field validators.
446    pub fn parse_update(
447        &self,
448        input: &IndexMap<String, JsValue>,
449    ) -> Result<FieldValues, FieldInputError> {
450        self.parse(input, false)
451    }
452
453    /// Parse application input before database hooks. Creation defaults and
454    /// required fields apply here; adapter binding remains a separate phase.
455    pub fn parse_create(
456        &self,
457        input: &IndexMap<String, JsValue>,
458    ) -> Result<FieldValues, FieldInputError> {
459        self.parse(input, true)
460    }
461
462    fn parse(
463        &self,
464        input: &IndexMap<String, JsValue>,
465        creation: bool,
466    ) -> Result<FieldValues, FieldInputError> {
467        let mut parsed = FieldValues::new();
468        for (name, field) in &self.0 {
469            let Some(value) = input.get(name) else {
470                if creation {
471                    if let Some(default) = &field.default {
472                        _ = parsed.insert(name.clone(), default.value());
473                    } else if field.required {
474                        return Err(FieldInputError::Validation {
475                            code: "MISSING_FIELD",
476                            message: format!("{name} is required"),
477                        });
478                    }
479                }
480                continue;
481            };
482            if !field.input {
483                if creation && let Some(default) = &field.default {
484                    _ = parsed.insert(name.clone(), default.value());
485                    continue;
486                }
487                if value.is_truthy() {
488                    return Err(FieldInputError::Validation {
489                        code: "FIELD_NOT_ALLOWED",
490                        message: format!("{name} is not allowed to be set"),
491                    });
492                }
493                continue;
494            }
495            if let Some(validator) = &field.async_validator {
496                _ = validator(value.clone());
497                return Err(FieldInputError::Transform(AuthError::Upstream {
498                    status: 500,
499                    code: "ASYNC_VALIDATION_NOT_SUPPORTED",
500                    message: "Async validation is not supported",
501                }));
502            }
503            let value = if let Some(validator) = &field.validator {
504                Some(
505                    validator(value).map_err(|message| FieldInputError::Validation {
506                        code: "VALIDATION_ERROR",
507                        message: if message.is_empty() {
508                            "Validation Error".into()
509                        } else {
510                            message
511                        },
512                    })?,
513                )
514            } else if let Some(transform) = &field.transform {
515                transform(Some(value)).map_err(FieldInputError::Transform)?
516            } else {
517                Some(value.clone())
518            };
519            match value {
520                Some(value) => {
521                    _ = parsed.insert(name.clone(), value);
522                }
523                None => {
524                    _ = parsed.undefined_input_keys.insert(name.clone());
525                }
526            }
527        }
528        Ok(parsed)
529    }
530}
531
532/// Immutable policies for application/plugin user fields.
533#[derive(Debug, Clone, Default)]
534pub struct UserFields(pub SessionFields);
535
536/// Immutable policies for application/plugin account fields.
537#[derive(Debug, Clone, Default)]
538pub struct AccountFields(pub SessionFields);
539
540/// Adapter precedence remains distinct from endpoint/public projection policies.
541#[derive(Debug, Clone, Default)]
542pub struct AdapterFieldPolicies {
543    pub user: SessionAdapterFields,
544    pub account: SessionAdapterFields,
545}
546
547/// Immutable adapter schema policies: plugins first, application configuration last.
548/// This differs from input/output `SessionFields`, matching getAuthTables precedence.
549#[derive(Debug, Clone, Default)]
550pub struct SessionAdapterFields(pub Arc<FieldConfigs>);
551
552impl SessionAdapterFields {
553    pub(crate) fn attach(&self, values: &mut FieldValues, creation: bool) {
554        values.transform_omitted = creation;
555        values.adapter_fields = Some(Arc::clone(&self.0));
556        values.binding_names = self
557            .0
558            .iter()
559            .filter_map(|(name, field)| {
560                field
561                    .field_name
562                    .as_ref()
563                    .map(|physical| (name.clone(), physical.clone()))
564            })
565            .collect();
566    }
567
568    /// Resolve declared fields from physical canonical getters and custom columns
569    /// before applying their output policies. The base projection alone controls
570    /// undeclared fields, so unrelated physical columns never become observable.
571    pub(crate) async fn record_output(
572        &self,
573        canonical: Value,
574        mut additional: FieldOutput,
575        base: Value,
576    ) -> AuthResult<AdapterOutput> {
577        let (Value::Object(canonical), Value::Object(mut base)) = (canonical, base) else {
578            return Err(AuthError::internal("Adapter output must be an object"));
579        };
580        additional.extend(canonical);
581        for name in self.0.keys() {
582            // Typed DTOs omit optional core fields during serialization. The
583            // actual initialized projection still distinguishes a stored null
584            // from an absent property; retain it before applying output policy.
585            if !additional.contains_key(name)
586                && let Some(value) = base.get(name)
587            {
588                _ = additional.insert(name.clone(), value.clone());
589            }
590            _ = base.remove(name);
591        }
592        let mut output = AdapterOutput::from_values(base);
593        output.extend(self.output(additional).await?);
594        Ok(output)
595    }
596
597    /// Transform only declared additional fields, retaining their omission.
598    pub(crate) async fn output(&self, values: FieldOutput) -> AuthResult<AdapterOutput> {
599        let mut output = AdapterOutput::default();
600        for (name, field) in &*self.0 {
601            let value = values
602                .get(name)
603                .or_else(|| {
604                    field
605                        .field_name
606                        .as_ref()
607                        .and_then(|physical| values.get(physical))
608                })
609                .cloned()
610                .map(JsValue::from);
611            let value = if let Some(transform) = &field.output_transform {
612                transform(value).await.map_err(|error| match error {
613                    AuthError::Api { .. } | AuthError::Upstream { .. } => error,
614                    error => AuthError::CallbackFailure(Box::new(error)),
615                })?
616            } else {
617                value
618            };
619            output.insert(
620                name.clone(),
621                value.map(|value| value.to_json_value()).transpose()?,
622            );
623        }
624        Ok(output)
625    }
626}