Skip to main content

base_web_content/
content.rs

1// Released under the MIT License.
2// Copyright, 2026, by Samuel Williams.
3
4use crate::__private::{Declaration, Item};
5use crate::ContentFormat;
6
7/// Content an application declares that it accepts, with limits on everything in it. Derive it with `#[derive(Content)]` on a struct with named fields; it must not be implemented by hand, since what the derive implements is not public API.
8///
9/// Each field declares one form field or nested group, named by its identifier unless `rename` gives the name a form submits:
10///
11/// | Field type | Submitted as |
12/// | --- | --- |
13/// | `String` | Text, which must be valid UTF-8. |
14/// | `Vec<u8>` | The submitted bytes, unchanged. |
15/// | An integer, `f32`, `f64`, or `bool` | Text in the type's strict syntax, such as `-12`, `1.5e3`, or `true`. |
16/// | A [`FieldValue`](crate::FieldValue), such as an enumeration with `#[derive(FieldValue)]` | Text converted by its code. |
17/// | [`Upload<H>`](crate::Upload) | A file, with the caller's storage handle `H`. |
18/// | A struct deriving `Content` | A nested group, whose fields are submitted as `name[field]`. |
19/// | `Option<T>` | A field the content may omit. |
20/// | `Vec<T>`, other than `Vec<u8>` | An array, submitted as `name[]`, or `name[][field]` for an array of groups. |
21///
22/// Every field without `Option` is required. Names are matched exactly, and a name the declaration does not accept is a fatal error before its content is read.
23///
24/// Each field and the struct take `#[content(...)]` attributes, whose limits are constant `usize` expressions:
25///
26/// | Attribute | On | Default |
27/// | --- | --- | --- |
28/// | `rename = "name"` | A field | The field's identifier |
29/// | `size_limit = N` | A scalar or upload, or each element of an array | 64 KiB, or 128 MiB for an upload |
30/// | `count_limit = N` | An array | 128 |
31/// | `accept("image/png", "image/*")`, matched against the media type the upload's part declares, so that a part without one is refused even by `*/*` | An upload | Any upload |
32/// | `formats("multipart/form-data", "application/x-www-form-urlencoded")` | The struct | Both |
33/// | `depth_limit = N`, the components of a name | The struct | 8 |
34/// | `content_size_limit = N`, the content of every field together | The struct | 256 MiB |
35/// | `part_count_limit = N` | The struct | 128 |
36/// | `header_size_limit = N`, one part's header section | The struct | 8 KiB |
37/// | `header_count_limit = N`, the fields of one part's header section | The struct | 64 |
38/// | `preamble_size_limit = N` | The struct | 64 KiB |
39/// | `epilogue_size_limit = N` | The struct | 1 KiB |
40/// | `pair_count_limit = N`, the pairs of a URL-encoded body | The struct | 1,024 |
41/// | `url_encoded_size_limit = N` | The struct | 2 MiB |
42///
43/// A struct's limits apply when it is the top level of a body, not when it is nested in another. Every upload in a declaration, nested groups included, must have the same handle type `H`, which [`ContentHandles`] names.
44///
45/// ```
46/// use base_web_content::{Content, ContentFormat, FieldValue, Upload};
47///
48/// #[derive(FieldValue)]
49/// enum Plan {
50///     #[content(rename = "free")]
51///     Free,
52///     #[content(rename = "pro")]
53///     Pro,
54/// }
55///
56/// #[derive(Content)]
57/// struct Address {
58///     city: String,
59///     postcode: Option<String>,
60/// }
61///
62/// #[derive(Content)]
63/// #[content(formats("multipart/form-data"))]
64/// struct Signup {
65///     #[content(size_limit = 256)]
66///     email: String,
67///     plan: Plan,
68///     address: Option<Address>,
69///     #[content(count_limit = 8)]
70///     interests: Vec<String>,
71///     #[content(accept("image/png", "image/jpeg"), size_limit = 4 * 1024 * 1024)]
72///     avatar: Option<Upload<std::path::PathBuf>>,
73/// }
74///
75/// assert!(Signup::body_size_limit(ContentFormat::MultipartFormData).is_some());
76/// assert_eq!(Signup::body_size_limit(ContentFormat::UrlEncoded), None);
77/// ```
78pub trait Content: Declaration {
79    /// The largest body in `format` the declaration could need, computed from its fields and limits, or `None` when it does not accept the format.
80    ///
81    /// A server can refuse a request whose `Content-Length` exceeds it with 413 Content Too Large before reading the body. The parser enforces it too, as a running total, whatever the request declared.
82    fn body_size_limit(format: ContentFormat) -> Option<usize> {
83        Self::DECLARATION.body_size_limit(format)
84    }
85}
86
87/// A declaration whose uploads hold handles of type `H`: any declaration without uploads, for every `H`, and one whose uploads are `Upload<H>`. It is implemented for every such declaration, so that generic code can name what [`ContentParser`](crate::ContentParser) parses.
88///
89/// ```
90/// use base_web_content::{ContentError, ContentHandles, ContentParser};
91/// use base_web_media_type::MediaType;
92///
93/// fn parser_for<T: ContentHandles<std::path::PathBuf>>(media_type: &MediaType) -> Result<ContentParser<T, std::path::PathBuf>, ContentError> {
94///     ContentParser::new(Some(media_type))
95/// }
96/// ```
97pub trait ContentHandles<H>: Content + Item<H> {}
98
99impl<T: Content + Item<H>, H> ContentHandles<H> for T {}