Skip to main content

facet_urlencoded/
lib.rs

1#![warn(missing_docs)]
2//!
3//! [![Coverage Status](https://coveralls.io/repos/github/facet-rs/facet-urlencoded/badge.svg?branch=main)](https://coveralls.io/github/facet-rs/facet?branch=main)
4//! [![crates.io](https://img.shields.io/crates/v/facet-urlencoded.svg)](https://crates.io/crates/facet-urlencoded)
5//! [![documentation](https://docs.rs/facet-urlencoded/badge.svg)](https://docs.rs/facet-urlencoded)
6//! [![MIT/Apache-2.0 licensed](https://img.shields.io/crates/l/facet-urlencoded.svg)](./LICENSE)
7//! [![Discord](https://img.shields.io/discord/1379550208551026748?logo=discord&label=discord)](https://discord.gg/JhD7CwCJ8F)
8//!
9//! Provides URL-encoded form data deserialization for Facet types.
10//!
11#![doc = include_str!("../readme-footer.md")]
12
13use facet_core::{Def, Facet, Type, UserType};
14use facet_reflect::{AllocError, Partial, ReflectError, ShapeMismatchError, TypePlan};
15use log::*;
16
17#[cfg(test)]
18mod tests;
19
20mod form;
21pub use form::Form;
22
23mod query;
24pub use query::Query;
25
26#[cfg(feature = "axum")]
27mod axum;
28#[cfg(feature = "axum")]
29pub use self::axum::{FormRejection, QueryRejection};
30
31/// Deserializes a URL encoded form data string into a value of type `T` that implements `Facet`.
32///
33/// This function supports parsing both flat structures and nested structures using the common
34/// bracket notation. For example, a form field like `user[name]` will be deserialized into
35/// a struct with a field named `user` that contains a field named `name`.
36///
37/// # Nested Structure Format
38///
39/// For nested structures, the library supports the standard bracket notation used in most web frameworks:
40/// - Simple nested objects: `object[field]=value`
41/// - Deeply nested objects: `object[field1][field2]=value`
42///
43/// # Basic Example
44///
45/// ```
46/// use facet::Facet;
47/// use facet_urlencoded::from_str;
48///
49/// #[derive(Debug, Facet, PartialEq)]
50/// struct SearchParams {
51///     query: String,
52///     page: u64,
53/// }
54///
55/// let query_string = "query=rust+programming&page=2";
56///
57/// let params: SearchParams = from_str(query_string).expect("Failed to parse URL encoded data");
58/// assert_eq!(params, SearchParams { query: "rust programming".to_string(), page: 2 });
59/// ```
60///
61/// # Nested Structure Example
62///
63/// ```
64/// use facet::Facet;
65/// use facet_urlencoded::from_str;
66///
67/// #[derive(Debug, Facet, PartialEq)]
68/// struct Address {
69///     street: String,
70///     city: String,
71/// }
72///
73/// #[derive(Debug, Facet, PartialEq)]
74/// struct User {
75///     name: String,
76///     address: Address,
77/// }
78///
79/// let query_string = "name=John+Doe&address[street]=123+Main+St&address[city]=Anytown";
80///
81/// let user: User = from_str(query_string).expect("Failed to parse URL encoded data");
82/// assert_eq!(user, User {
83///     name: "John Doe".to_string(),
84///     address: Address {
85///         street: "123 Main St".to_string(),
86///         city: "Anytown".to_string(),
87///     },
88/// });
89/// ```
90pub fn from_str<'input: 'facet, 'facet, T: Facet<'facet>>(
91    urlencoded: &'input str,
92) -> Result<T, UrlEncodedError> {
93    let plan = TypePlan::<T>::build()?;
94    let partial = plan.partial()?;
95    let partial = from_str_value(partial, urlencoded)?;
96    let result: T = partial.build()?.materialize()?;
97    Ok(result)
98}
99
100/// Deserializes a URL encoded form data string into an owned value of type `T`.
101///
102/// This is similar to [`from_str`] but works with types that implement `Facet<'static>`,
103/// which means they don't borrow from the input. This is useful when the input is
104/// temporary (e.g., from an HTTP request body) but you need an owned result.
105///
106/// # Example
107///
108/// ```
109/// use facet::Facet;
110/// use facet_urlencoded::from_str_owned;
111///
112/// #[derive(Debug, Facet, PartialEq)]
113/// struct SearchParams {
114///     query: String,
115///     page: u64,
116/// }
117///
118/// let query_string = "query=rust&page=1";
119/// let params: SearchParams = from_str_owned(query_string).expect("Failed to parse");
120/// assert_eq!(params, SearchParams { query: "rust".to_string(), page: 1 });
121/// ```
122pub fn from_str_owned<T: Facet<'static>>(urlencoded: &str) -> Result<T, UrlEncodedError> {
123    let plan = TypePlan::<T>::build()?;
124    let partial = plan.partial_owned()?;
125    let partial = from_str_value(partial, urlencoded)?;
126    let result: T = partial.build()?.materialize()?;
127    Ok(result)
128}
129
130/// Deserializes a URL encoded form data string into an heap-allocated value.
131///
132/// This is the lower-level function that works with `Partial` directly.
133fn from_str_value<'facet, const BORROW: bool>(
134    mut wip: Partial<'facet, BORROW>,
135    urlencoded: &str,
136) -> Result<Partial<'facet, BORROW>, UrlEncodedError> {
137    trace!("Starting URL encoded form data deserialization");
138
139    // Parse the URL encoded string into key-value pairs
140    let pairs = form_urlencoded::parse(urlencoded.as_bytes());
141
142    // Process the input into a nested structure
143    let mut nested_values = NestedValues::new();
144    for (key, value) in pairs {
145        nested_values.insert(&key, value.to_string());
146    }
147
148    // Create pre-initialized structure so that we have all the required fields
149    // for better error reporting when fields are missing
150    initialize_nested_structures(&mut nested_values);
151
152    // Process the deserialization
153    wip = deserialize_value(wip, &nested_values)?;
154    Ok(wip)
155}
156
157/// Ensures that all nested structures have entries in the NestedValues
158/// This helps ensure we get better error reporting when fields are missing
159fn initialize_nested_structures(nested: &mut NestedValues) {
160    // Go through each nested value and recursively initialize it
161    for nested_value in nested.nested.values_mut() {
162        initialize_nested_structures(nested_value);
163    }
164}
165
166/// Internal helper struct to represent nested values from URL-encoded data
167struct NestedValues {
168    // Root level key-value pairs
169    flat: std::collections::HashMap<String, String>,
170    // Nested structures: key -> nested map
171    nested: std::collections::HashMap<String, NestedValues>,
172}
173
174impl NestedValues {
175    fn new() -> Self {
176        Self {
177            flat: std::collections::HashMap::new(),
178            nested: std::collections::HashMap::new(),
179        }
180    }
181
182    fn insert(&mut self, key: &str, value: String) {
183        // For bracket notation like user[name] or user[address][city]
184        if let Some(open_bracket) = key.find('[')
185            && let Some(close_bracket) = key.find(']')
186            && open_bracket < close_bracket
187        {
188            let parent_key = &key[0..open_bracket];
189            let nested_key = &key[(open_bracket + 1)..close_bracket];
190            let remainder = &key[(close_bracket + 1)..];
191
192            let nested = self
193                .nested
194                .entry(parent_key.to_string())
195                .or_insert_with(NestedValues::new);
196
197            if remainder.is_empty() {
198                // Simple case: user[name]=value
199                nested.flat.insert(nested_key.to_string(), value);
200            } else {
201                // Handle deeply nested case like user[address][city]=value
202                let new_key = format!("{nested_key}{remainder}");
203                nested.insert(&new_key, value);
204            }
205            return;
206        }
207
208        // If we get here, it's a flat key-value pair
209        self.flat.insert(key.to_string(), value);
210    }
211
212    fn get(&self, key: &str) -> Option<&String> {
213        self.flat.get(key)
214    }
215
216    #[expect(dead_code)]
217    fn get_nested(&self, key: &str) -> Option<&NestedValues> {
218        self.nested.get(key)
219    }
220
221    fn keys(&self) -> impl Iterator<Item = &String> {
222        self.flat.keys()
223    }
224
225    #[expect(dead_code)]
226    fn nested_keys(&self) -> impl Iterator<Item = &String> {
227        self.nested.keys()
228    }
229}
230
231/// Deserialize a value recursively using the nested values
232fn deserialize_value<'facet, const BORROW: bool>(
233    mut wip: Partial<'facet, BORROW>,
234    values: &NestedValues,
235) -> Result<Partial<'facet, BORROW>, UrlEncodedError> {
236    let shape = wip.shape();
237    match shape.ty {
238        Type::User(UserType::Struct(_)) => {
239            trace!("Deserializing struct");
240
241            // Process flat fields
242            for key in values.keys() {
243                if let Some(index) = wip.field_index(key) {
244                    let value = values.get(key).unwrap(); // Safe because we're iterating over keys
245                    wip = wip.begin_nth_field(index)?;
246                    wip = deserialize_scalar_field(key, value, wip)?;
247                    wip = wip.end()?;
248                } else {
249                    trace!("Unknown field: {key}");
250                }
251            }
252
253            // Process nested fields
254            for key in values.nested.keys() {
255                if let Some(index) = wip.field_index(key) {
256                    let nested_values = values.nested.get(key).unwrap(); // Safe because we're iterating over keys
257                    wip = wip.begin_nth_field(index)?;
258                    wip = deserialize_nested_field(key, nested_values, wip)?;
259                    wip = wip.end()?;
260                } else {
261                    trace!("Unknown nested field: {key}");
262                }
263            }
264
265            trace!("Finished deserializing struct");
266            Ok(wip)
267        }
268        _ => {
269            error!("Unsupported root type");
270            Err(UrlEncodedError::UnsupportedShape(
271                "Unsupported root type".to_string(),
272            ))
273        }
274    }
275}
276
277/// Helper function to deserialize a scalar field
278fn deserialize_scalar_field<'facet, const BORROW: bool>(
279    key: &str,
280    value: &str,
281    mut wip: Partial<'facet, BORROW>,
282) -> Result<Partial<'facet, BORROW>, UrlEncodedError> {
283    match wip.shape().def {
284        Def::Scalar => {
285            if wip.shape().is_type::<String>() {
286                let s = value.to_string();
287                wip = wip.set(s)?;
288            } else if wip.shape().is_type::<u64>() {
289                match value.parse::<u64>() {
290                    Ok(num) => wip = wip.set(num)?,
291                    Err(_) => {
292                        return Err(UrlEncodedError::InvalidNumber(
293                            key.to_string(),
294                            value.to_string(),
295                        ));
296                    }
297                };
298            } else {
299                warn!("facet-yaml: unsupported scalar type: {}", wip.shape());
300                return Err(UrlEncodedError::UnsupportedType(format!("{}", wip.shape())));
301            }
302            Ok(wip)
303        }
304        _ => {
305            error!("Expected scalar field");
306            Err(UrlEncodedError::UnsupportedShape(format!(
307                "Expected scalar for field '{key}'"
308            )))
309        }
310    }
311}
312
313/// Helper function to deserialize a nested field
314fn deserialize_nested_field<'facet, const BORROW: bool>(
315    key: &str,
316    nested_values: &NestedValues,
317    mut wip: Partial<'facet, BORROW>,
318) -> Result<Partial<'facet, BORROW>, UrlEncodedError> {
319    let shape = wip.shape();
320    match shape.ty {
321        Type::User(UserType::Struct(_)) => {
322            trace!("Deserializing nested struct field: {key}");
323
324            // Process flat fields in the nested structure
325            for nested_key in nested_values.keys() {
326                if let Some(index) = wip.field_index(nested_key) {
327                    let value = nested_values.get(nested_key).unwrap(); // Safe because we're iterating over keys
328                    wip = wip.begin_nth_field(index)?;
329                    wip = deserialize_scalar_field(nested_key, value, wip)?;
330                    wip = wip.end()?;
331                }
332            }
333
334            // Process deeper nested fields
335            for nested_key in nested_values.nested.keys() {
336                if let Some(index) = wip.field_index(nested_key) {
337                    let deeper_nested = nested_values.nested.get(nested_key).unwrap(); // Safe because we're iterating over keys
338                    wip = wip.begin_nth_field(index)?;
339                    wip = deserialize_nested_field(nested_key, deeper_nested, wip)?;
340                    wip = wip.end()?;
341                }
342            }
343
344            Ok(wip)
345        }
346        _ => {
347            error!("Expected struct field for nested value");
348            Err(UrlEncodedError::UnsupportedShape(format!(
349                "Expected struct for nested field '{key}'"
350            )))
351        }
352    }
353}
354
355/// Errors that can occur during URL encoded form data deserialization.
356#[derive(Debug)]
357pub enum UrlEncodedError {
358    /// The field value couldn't be parsed as a number.
359    InvalidNumber(String, String),
360    /// The shape is not supported for deserialization.
361    UnsupportedShape(String),
362    /// The type is not supported for deserialization.
363    UnsupportedType(String),
364    /// Reflection error
365    ReflectError(ReflectError),
366}
367
368impl From<ReflectError> for UrlEncodedError {
369    fn from(err: ReflectError) -> Self {
370        UrlEncodedError::ReflectError(err)
371    }
372}
373
374impl From<ShapeMismatchError> for UrlEncodedError {
375    fn from(err: ShapeMismatchError) -> Self {
376        UrlEncodedError::UnsupportedShape(format!(
377            "shape mismatch: expected {}, got {}",
378            err.expected, err.actual
379        ))
380    }
381}
382
383impl From<AllocError> for UrlEncodedError {
384    fn from(err: AllocError) -> Self {
385        UrlEncodedError::UnsupportedShape(format!(
386            "allocation failed for {}: {}",
387            err.shape, err.operation
388        ))
389    }
390}
391
392impl core::fmt::Display for UrlEncodedError {
393    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
394        match self {
395            UrlEncodedError::InvalidNumber(field, value) => {
396                write!(f, "Invalid number for field '{field}': '{value}'")
397            }
398            UrlEncodedError::UnsupportedShape(shape) => {
399                write!(f, "Unsupported shape: {shape}")
400            }
401            UrlEncodedError::UnsupportedType(ty) => {
402                write!(f, "Unsupported type: {ty}")
403            }
404            UrlEncodedError::ReflectError(err) => {
405                write!(f, "Reflection error: {err}")
406            }
407        }
408    }
409}
410
411impl std::error::Error for UrlEncodedError {}