Skip to main content

a3s_boot/
serialization.rs

1use crate::{BootError, BootResponse, BoxFuture, ExecutionContext, Interceptor, Result};
2use serde_json::{Map, Value};
3use std::collections::BTreeSet;
4
5/// JSON response shaping options used by [`SerializationInterceptor`].
6#[derive(Debug, Clone, PartialEq, Eq, Default)]
7pub struct SerializationOptions {
8    include_fields: Option<BTreeSet<String>>,
9    exclude_fields: BTreeSet<String>,
10    skip_null_fields: bool,
11}
12
13impl SerializationOptions {
14    pub fn new() -> Self {
15        Self::default()
16    }
17
18    pub fn include_field(mut self, field: impl Into<String>) -> Self {
19        self.include_fields
20            .get_or_insert_with(BTreeSet::new)
21            .insert(field.into());
22        self
23    }
24
25    pub fn include_fields<I, S>(mut self, fields: I) -> Self
26    where
27        I: IntoIterator<Item = S>,
28        S: Into<String>,
29    {
30        for field in fields {
31            self = self.include_field(field);
32        }
33        self
34    }
35
36    pub fn exclude_field(mut self, field: impl Into<String>) -> Self {
37        self.exclude_fields.insert(field.into());
38        self
39    }
40
41    pub fn exclude_fields<I, S>(mut self, fields: I) -> Self
42    where
43        I: IntoIterator<Item = S>,
44        S: Into<String>,
45    {
46        for field in fields {
47            self = self.exclude_field(field);
48        }
49        self
50    }
51
52    pub fn skip_null_fields(mut self) -> Self {
53        self.skip_null_fields = true;
54        self
55    }
56
57    pub fn included_fields(&self) -> Option<impl Iterator<Item = &str>> {
58        self.include_fields
59            .as_ref()
60            .map(|fields| fields.iter().map(String::as_str))
61    }
62
63    pub fn excluded_fields(&self) -> impl Iterator<Item = &str> {
64        self.exclude_fields.iter().map(String::as_str)
65    }
66
67    pub fn skips_null_fields(&self) -> bool {
68        self.skip_null_fields
69    }
70
71    pub fn is_empty(&self) -> bool {
72        self.include_fields.is_none() && self.exclude_fields.is_empty() && !self.skip_null_fields
73    }
74
75    fn merged_with(&self, route_options: &Self) -> Self {
76        let mut merged = self.clone();
77        if route_options.include_fields.is_some() {
78            merged.include_fields = route_options.include_fields.clone();
79        }
80        merged
81            .exclude_fields
82            .extend(route_options.exclude_fields.iter().cloned());
83        merged.skip_null_fields |= route_options.skip_null_fields;
84        merged
85    }
86}
87
88/// Interceptor that shapes JSON response objects using route serialization metadata.
89#[derive(Debug, Clone, Default)]
90pub struct SerializationInterceptor {
91    options: SerializationOptions,
92}
93
94impl SerializationInterceptor {
95    pub fn new() -> Self {
96        Self::default()
97    }
98
99    pub fn with_options(options: SerializationOptions) -> Self {
100        Self { options }
101    }
102
103    pub fn options(&self) -> &SerializationOptions {
104        &self.options
105    }
106}
107
108impl Interceptor for SerializationInterceptor {
109    fn after(
110        &self,
111        context: ExecutionContext,
112        response: BootResponse,
113    ) -> BoxFuture<'static, Result<BootResponse>> {
114        let options = self.options.clone();
115        Box::pin(async move { serialize_response(response, &options, &context.serialization) })
116    }
117}
118
119fn serialize_response(
120    mut response: BootResponse,
121    interceptor_options: &SerializationOptions,
122    route_options: &SerializationOptions,
123) -> Result<BootResponse> {
124    let options = interceptor_options.merged_with(route_options);
125    if options.is_empty()
126        || response.is_streaming()
127        || !response.has_body()
128        || !response.is_json_content_type()
129    {
130        return Ok(response);
131    }
132
133    let had_content_length = !response.header_values("content-length").is_empty();
134    let mut value = serde_json::from_slice::<Value>(&response.body)
135        .map_err(|error| BootError::Internal(format!("invalid JSON response body: {error}")))?;
136    transform_payload(&mut value, &options);
137    response.body = serde_json::to_vec(&value)
138        .map_err(|error| BootError::Internal(format!("failed to serialize response: {error}")))?;
139
140    if had_content_length {
141        response.headers.remove("content-length");
142        response
143            .appended_headers
144            .retain(|(name, _)| !name.eq_ignore_ascii_case("content-length"));
145        let content_length = response.body.len() as u64;
146        response = response.with_content_length(content_length);
147    }
148
149    Ok(response)
150}
151
152fn transform_payload(value: &mut Value, options: &SerializationOptions) {
153    match value {
154        Value::Object(object) => transform_object(object, options),
155        Value::Array(values) => {
156            for value in values {
157                if let Value::Object(object) = value {
158                    transform_object(object, options);
159                }
160            }
161        }
162        _ => {}
163    }
164}
165
166fn transform_object(object: &mut Map<String, Value>, options: &SerializationOptions) {
167    if let Some(include_fields) = &options.include_fields {
168        object.retain(|field, _| include_fields.contains(field));
169    }
170
171    for field in &options.exclude_fields {
172        object.remove(field);
173    }
174
175    if options.skip_null_fields {
176        object.retain(|_, value| !value.is_null());
177    }
178}