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.
278///
279/// `Option<T>` fields are transparently descended into via `begin_some`
280/// before scalar parsing — present keys land in `Some(value)`, absent
281/// keys leave the field at its default (`None` unless the struct field
282/// carries `#[facet(default = …)]`). This lets `?provider=github&limit=50`
283/// drive into `struct { provider: Option<String>, limit: Option<i64> }`
284/// without forcing callers to sentinel-encode "missing".
285fn deserialize_scalar_field<'facet, const BORROW: bool>(
286    key: &str,
287    value: &str,
288    mut wip: Partial<'facet, BORROW>,
289) -> Result<Partial<'facet, BORROW>, UrlEncodedError> {
290    // Transparently descend through `Option<T>`. Mirrors how
291    // facet-format's path_navigator handles it.
292    let is_option = matches!(wip.shape().def, Def::Option(_));
293    if is_option {
294        wip = wip.begin_some()?;
295    }
296    match wip.shape().def {
297        Def::Scalar => {
298            if wip.shape().is_type::<String>() {
299                let s = value.to_string();
300                wip = wip.set(s)?;
301            } else if wip.shape().is_type::<u64>() {
302                match value.parse::<u64>() {
303                    Ok(num) => wip = wip.set(num)?,
304                    Err(_) => {
305                        return Err(UrlEncodedError::InvalidNumber(
306                            key.to_string(),
307                            value.to_string(),
308                        ));
309                    }
310                };
311            } else if wip.shape().is_type::<i64>() {
312                match value.parse::<i64>() {
313                    Ok(num) => wip = wip.set(num)?,
314                    Err(_) => {
315                        return Err(UrlEncodedError::InvalidNumber(
316                            key.to_string(),
317                            value.to_string(),
318                        ));
319                    }
320                };
321            } else if wip.shape().is_type::<bool>() {
322                let parsed = match value {
323                    "true" | "1" | "on" | "yes" => true,
324                    "false" | "0" | "off" | "no" | "" => false,
325                    _ => {
326                        return Err(UrlEncodedError::UnsupportedType(format!(
327                            "{}: unparseable bool literal {value:?}",
328                            wip.shape()
329                        )));
330                    }
331                };
332                wip = wip.set(parsed)?;
333            } else {
334                warn!("facet-urlencoded: unsupported scalar type: {}", wip.shape());
335                return Err(UrlEncodedError::UnsupportedType(format!("{}", wip.shape())));
336            }
337            // Pop the `begin_some` frame opened above so the caller's
338            // `.end()` can pop the field cleanly.
339            if is_option {
340                wip = wip.end()?;
341            }
342            Ok(wip)
343        }
344        _ => {
345            error!("Expected scalar field");
346            Err(UrlEncodedError::UnsupportedShape(format!(
347                "Expected scalar for field '{key}'"
348            )))
349        }
350    }
351}
352
353/// Helper function to deserialize a nested field
354fn deserialize_nested_field<'facet, const BORROW: bool>(
355    key: &str,
356    nested_values: &NestedValues,
357    mut wip: Partial<'facet, BORROW>,
358) -> Result<Partial<'facet, BORROW>, UrlEncodedError> {
359    let shape = wip.shape();
360    match shape.ty {
361        Type::User(UserType::Struct(_)) => {
362            trace!("Deserializing nested struct field: {key}");
363
364            // Process flat fields in the nested structure
365            for nested_key in nested_values.keys() {
366                if let Some(index) = wip.field_index(nested_key) {
367                    let value = nested_values.get(nested_key).unwrap(); // Safe because we're iterating over keys
368                    wip = wip.begin_nth_field(index)?;
369                    wip = deserialize_scalar_field(nested_key, value, wip)?;
370                    wip = wip.end()?;
371                }
372            }
373
374            // Process deeper nested fields
375            for nested_key in nested_values.nested.keys() {
376                if let Some(index) = wip.field_index(nested_key) {
377                    let deeper_nested = nested_values.nested.get(nested_key).unwrap(); // Safe because we're iterating over keys
378                    wip = wip.begin_nth_field(index)?;
379                    wip = deserialize_nested_field(nested_key, deeper_nested, wip)?;
380                    wip = wip.end()?;
381                }
382            }
383
384            Ok(wip)
385        }
386        _ => {
387            error!("Expected struct field for nested value");
388            Err(UrlEncodedError::UnsupportedShape(format!(
389                "Expected struct for nested field '{key}'"
390            )))
391        }
392    }
393}
394
395/// Errors that can occur during URL encoded form data deserialization.
396#[derive(Debug)]
397pub enum UrlEncodedError {
398    /// The field value couldn't be parsed as a number.
399    InvalidNumber(String, String),
400    /// The shape is not supported for deserialization.
401    UnsupportedShape(String),
402    /// The type is not supported for deserialization.
403    UnsupportedType(String),
404    /// Reflection error
405    ReflectError(ReflectError),
406}
407
408impl From<ReflectError> for UrlEncodedError {
409    fn from(err: ReflectError) -> Self {
410        UrlEncodedError::ReflectError(err)
411    }
412}
413
414impl From<ShapeMismatchError> for UrlEncodedError {
415    fn from(err: ShapeMismatchError) -> Self {
416        UrlEncodedError::UnsupportedShape(format!(
417            "shape mismatch: expected {}, got {}",
418            err.expected, err.actual
419        ))
420    }
421}
422
423impl From<AllocError> for UrlEncodedError {
424    fn from(err: AllocError) -> Self {
425        UrlEncodedError::UnsupportedShape(format!(
426            "allocation failed for {}: {}",
427            err.shape, err.operation
428        ))
429    }
430}
431
432impl core::fmt::Display for UrlEncodedError {
433    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
434        match self {
435            UrlEncodedError::InvalidNumber(field, value) => {
436                write!(f, "Invalid number for field '{field}': '{value}'")
437            }
438            UrlEncodedError::UnsupportedShape(shape) => {
439                write!(f, "Unsupported shape: {shape}")
440            }
441            UrlEncodedError::UnsupportedType(ty) => {
442                write!(f, "Unsupported type: {ty}")
443            }
444            UrlEncodedError::ReflectError(err) => {
445                write!(f, "Reflection error: {err}")
446            }
447        }
448    }
449}
450
451impl std::error::Error for UrlEncodedError {}