Skip to main content

blazingly_core/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use blazingly_contract::{InvalidOperationId, OperationContract};
5use core::fmt;
6use serde::{Deserialize, Serialize};
7use std::collections::BTreeSet;
8use std::future::{Future, poll_fn};
9use std::marker::PhantomData;
10use std::pin::Pin;
11use std::task::{Context, Poll};
12
13pub use blazingly_contract::{
14    AgentPolicy, ApiError, ApiModel, ApiSchema, CURRENT_CONTRACT_FORMAT_VERSION, Compatibility,
15    CompatibilityChange, CompatibilityImpact, CompatibilityReport, Confirmation,
16    ContractFingerprint, ContractFormatVersion, DependencyDescriptor, FieldDescriptor,
17    FieldViolation, InputDescriptor, InputSource, McpToolDescriptor, ModelDescriptor,
18    OperationFailure, OperationId, OperationRisk, OutputExposure, ResponseBuildError,
19    ResponseDescriptor, ResponseHeader, SchemaKind, SecurityLocation, SecurityRequirement,
20    SecuritySchemeDescriptor, SecuritySchemeKind, TypeDescriptor, ValidationErrors, ValidationRule,
21};
22
23/// Shared plumbing for the first-party schema projections, not stable API.
24#[doc(hidden)]
25pub mod schema;
26
27// ---------------------------------------------------------------------------
28// Response body sizing
29// ---------------------------------------------------------------------------
30
31/// Distinct response shapes tracked per thread before slots start colliding.
32const RESPONSE_HINT_SLOTS: usize = 32;
33/// Floor for a learned hint, matching what `blazingly_json::to_vec` starts with.
34const MIN_RESPONSE_HINT: usize = 128;
35/// Ceiling for a learned hint, so one outsized response cannot make every
36/// later response of that shape reserve megabytes.
37const MAX_RESPONSE_HINT: usize = 1 << 20;
38
39thread_local! {
40    /// Last observed encoded size per response shape, direct mapped.
41    ///
42    /// A JSON body is grown from nothing on every request, which for an 18 KB
43    /// listing means about nine reallocations and 32 KB of copying that the
44    /// previous request already knew the answer to. The table is advisory: a
45    /// stale or colliding entry only changes the initial capacity, never the
46    /// bytes produced.
47    static RESPONSE_SIZE_HINTS: std::cell::RefCell<[(usize, usize); RESPONSE_HINT_SLOTS]> =
48        const { std::cell::RefCell::new([(0, 0); RESPONSE_HINT_SLOTS]) };
49}
50
51/// A per-monomorphization key for `T`.
52///
53/// `type_name` returns a `&'static str` whose address is stable within one
54/// monomorphization. Two types that share an address share a size hint, which
55/// costs at most one reallocation.
56fn response_shape_key<T: ?Sized>() -> usize {
57    core::any::type_name::<T>().as_ptr() as usize
58}
59
60fn response_hint_slot(key: usize) -> usize {
61    (key >> 4) % RESPONSE_HINT_SLOTS
62}
63
64/// Returns the capacity to reserve for the next body of shape `T`.
65#[must_use]
66pub fn response_size_hint<T: ?Sized>() -> usize {
67    let key = response_shape_key::<T>();
68    RESPONSE_SIZE_HINTS.with_borrow(|hints| {
69        let (stored_key, hint) = hints[response_hint_slot(key)];
70        if stored_key == key {
71            hint.max(MIN_RESPONSE_HINT)
72        } else {
73            MIN_RESPONSE_HINT
74        }
75    })
76}
77
78/// Records the encoded size of a body of shape `T`.
79///
80/// The recorded value carries an eighth of headroom so a body that grows
81/// slightly from one request to the next still fits the reserved capacity.
82pub fn record_response_size<T: ?Sized>(size: usize) {
83    let key = response_shape_key::<T>();
84    let hint = size
85        .saturating_add(size / 8)
86        .saturating_add(32)
87        .clamp(MIN_RESPONSE_HINT, MAX_RESPONSE_HINT);
88    RESPONSE_SIZE_HINTS.with_borrow_mut(|hints| {
89        hints[response_hint_slot(key)] = (key, hint);
90    });
91}
92
93/// A typed JSON request body.
94#[derive(Clone, Debug, Eq, PartialEq)]
95pub struct Json<T>(pub T);
96
97/// A typed path argument.
98#[derive(Clone, Debug, Eq, PartialEq)]
99pub struct Path<T>(pub T);
100
101/// Typed URL query arguments.
102#[derive(Clone, Debug, Eq, PartialEq)]
103pub struct Query<T>(pub T);
104
105/// A typed HTTP header argument.
106#[derive(Clone, Debug, Eq, PartialEq)]
107pub struct Header<T>(pub T);
108
109/// A typed HTTP cookie argument.
110#[derive(Clone, Debug, Eq, PartialEq)]
111pub struct Cookie<T>(pub T);
112
113/// A typed `application/x-www-form-urlencoded` request body.
114#[derive(Clone, Debug, Eq, PartialEq)]
115pub struct Form<T>(pub T);
116
117/// A typed `multipart/form-data` request body.
118#[derive(Clone, Debug, Eq, PartialEq)]
119pub struct Multipart<T>(pub T);
120
121/// A typed uploaded file argument.
122#[derive(Clone, Debug, Eq, PartialEq)]
123pub struct File<T>(pub T);
124
125/// Runtime-neutral buffered upload metadata.
126///
127/// Native and Cloudflare adapters may obtain these bytes differently; neither
128/// storage nor socket APIs leak into the operation contract.
129///
130/// # Wire forms
131///
132/// Deserialization accepts two shapes, because the same type serves two very
133/// different transports:
134///
135/// * the plain object — `field_name`, `file_name`, `content_type`, `bytes` —
136///   which is what [`Serialize`] produces and what an MCP client sends;
137/// * a one-entry *slot token* the multipart extractor writes in place of the
138///   bytes, resolved through [`UploadSlots`] instead of through the document.
139///
140/// The second form exists so that `Multipart<T>` never has to render megabytes
141/// of upload payload as a JSON array of numbers; see [`UploadSlots`].
142#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
143pub struct UploadFile {
144    pub field_name: String,
145    pub file_name: Option<String>,
146    pub content_type: Option<String>,
147    pub bytes: Vec<u8>,
148}
149
150impl UploadFile {
151    #[must_use]
152    pub fn new(field_name: impl Into<String>, bytes: Vec<u8>) -> Self {
153        Self {
154            field_name: field_name.into(),
155            file_name: None,
156            content_type: None,
157            bytes,
158        }
159    }
160
161    #[must_use]
162    pub fn with_file_name(mut self, file_name: impl Into<String>) -> Self {
163        self.file_name = Some(file_name.into());
164        self
165    }
166
167    #[must_use]
168    pub fn with_content_type(mut self, content_type: impl Into<String>) -> Self {
169        self.content_type = Some(content_type.into());
170        self
171    }
172}
173
174impl ApiSchema for UploadFile {
175    fn type_descriptor() -> TypeDescriptor {
176        TypeDescriptor::scalar("UploadFile", SchemaKind::Binary)
177    }
178}
179
180// ---------------------------------------------------------------------------
181// Out-of-band upload transfer
182// ---------------------------------------------------------------------------
183
184/// The reserved object key that stands for an upload parked in a slot.
185///
186/// The `$` prefix and the path separator keep it clear of any identifier a
187/// `#[api_model]` field could be named, and of any key an MCP client is likely
188/// to send by accident.
189const UPLOAD_SLOT_KEY: &str = "$blazingly::upload";
190
191thread_local! {
192    /// Uploads parked for the extraction currently running on this thread.
193    ///
194    /// A typed multipart body is decoded by handing `T` a JSON document built
195    /// from the parts. Text parts belong in that document; upload bytes do not.
196    /// A five-megabyte image rendered as a JSON array costs one `Value` per
197    /// byte — hundreds of megabytes of resident memory and a full
198    /// serialize/deserialize round trip — to arrive at the `Vec<u8>` the
199    /// extractor already held.
200    ///
201    /// So the bytes travel beside the document instead of inside it: the
202    /// extractor parks each upload here, writes a one-entry token in its place,
203    /// and [`UploadFile`]'s deserializer takes the upload back out. The
204    /// document stays small enough to be irrelevant, and the bytes are moved,
205    /// never re-encoded.
206    static UPLOAD_SLOTS: std::cell::RefCell<Vec<Option<UploadFile>>> =
207        const { std::cell::RefCell::new(Vec::new()) };
208}
209
210/// The extraction scope that owns the uploads parked inside it.
211///
212/// Slots opened through this guard are released when it is dropped, whether
213/// the decode succeeded, failed, or never reached them. The guard restores the
214/// table to the length it had on acquisition, so two extractions on one thread
215/// — sequential or nested — cannot see each other's slots.
216///
217/// This is framework plumbing between the executor and [`UploadFile`]'s
218/// deserializer; applications never name it.
219#[doc(hidden)]
220#[derive(Debug)]
221pub struct UploadSlots {
222    base: usize,
223    /// A slot index only means anything on the thread that parked it, so the
224    /// guard must not travel to another one.
225    thread_bound: PhantomData<*const ()>,
226}
227
228impl UploadSlots {
229    /// Opens a scope for the uploads of one extraction.
230    #[must_use]
231    pub fn acquire() -> Self {
232        Self {
233            base: UPLOAD_SLOTS.with_borrow(Vec::len),
234            thread_bound: PhantomData,
235        }
236    }
237
238    /// Parks `upload` and returns the token that stands for it in the document.
239    ///
240    /// The token is a one-entry object; the bytes stay in the slot table until
241    /// [`UploadFile`]'s deserializer moves them into the decoded value, or
242    /// until this guard is dropped.
243    #[must_use]
244    pub fn park(&self, upload: UploadFile) -> blazingly_json::Value {
245        let index = UPLOAD_SLOTS.with_borrow_mut(|slots| {
246            slots.push(Some(upload));
247            slots.len() - 1
248        });
249        let mut token = blazingly_json::Map::new();
250        token.insert(
251            UPLOAD_SLOT_KEY.to_owned(),
252            blazingly_json::Value::from(index),
253        );
254        blazingly_json::Value::Object(token)
255    }
256}
257
258impl Drop for UploadSlots {
259    fn drop(&mut self) {
260        UPLOAD_SLOTS.with_borrow_mut(|slots| slots.truncate(self.base));
261    }
262}
263
264/// Moves the upload parked at `index` out of the slot table.
265///
266/// Returns `None` for an index that was never parked, was already taken, or
267/// belongs to an extraction that has already ended — which is also what a
268/// forged token in a request body produces.
269fn take_parked_upload(index: usize) -> Option<UploadFile> {
270    UPLOAD_SLOTS.with_borrow_mut(|slots| slots.get_mut(index).and_then(Option::take))
271}
272
273/// Fields understood by [`UploadFile`]'s deserializer.
274enum UploadField {
275    Slot,
276    FieldName,
277    FileName,
278    ContentType,
279    Bytes,
280    Unknown,
281}
282
283impl<'de> Deserialize<'de> for UploadField {
284    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
285        struct FieldVisitor;
286
287        impl serde::de::Visitor<'_> for FieldVisitor {
288            type Value = UploadField;
289
290            fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
291                formatter.write_str("an uploaded file field name")
292            }
293
294            fn visit_str<E: serde::de::Error>(self, value: &str) -> Result<UploadField, E> {
295                Ok(match value {
296                    UPLOAD_SLOT_KEY => UploadField::Slot,
297                    "field_name" => UploadField::FieldName,
298                    "file_name" => UploadField::FileName,
299                    "content_type" => UploadField::ContentType,
300                    "bytes" => UploadField::Bytes,
301                    _ => UploadField::Unknown,
302                })
303            }
304        }
305
306        deserializer.deserialize_identifier(FieldVisitor)
307    }
308}
309
310struct UploadFileVisitor;
311
312impl<'de> serde::de::Visitor<'de> for UploadFileVisitor {
313    type Value = UploadFile;
314
315    fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
316        formatter.write_str("an uploaded file")
317    }
318
319    fn visit_map<A: serde::de::MapAccess<'de>>(self, mut map: A) -> Result<UploadFile, A::Error> {
320        use serde::de::Error as _;
321
322        let mut field_name: Option<String> = None;
323        let mut file_name: Option<Option<String>> = None;
324        let mut content_type: Option<Option<String>> = None;
325        let mut bytes: Option<Vec<u8>> = None;
326
327        while let Some(key) = map.next_key::<UploadField>()? {
328            match key {
329                UploadField::Slot => {
330                    let index = map.next_value::<usize>()?;
331                    let upload = take_parked_upload(index).ok_or_else(|| {
332                        A::Error::custom("uploaded file bytes are no longer available")
333                    })?;
334                    // Drain the rest so the caller's map access finishes cleanly.
335                    while map
336                        .next_entry::<serde::de::IgnoredAny, serde::de::IgnoredAny>()?
337                        .is_some()
338                    {}
339                    return Ok(upload);
340                }
341                UploadField::FieldName => {
342                    if field_name.is_some() {
343                        return Err(A::Error::duplicate_field("field_name"));
344                    }
345                    field_name = Some(map.next_value()?);
346                }
347                UploadField::FileName => {
348                    if file_name.is_some() {
349                        return Err(A::Error::duplicate_field("file_name"));
350                    }
351                    file_name = Some(map.next_value()?);
352                }
353                UploadField::ContentType => {
354                    if content_type.is_some() {
355                        return Err(A::Error::duplicate_field("content_type"));
356                    }
357                    content_type = Some(map.next_value()?);
358                }
359                UploadField::Bytes => {
360                    if bytes.is_some() {
361                        return Err(A::Error::duplicate_field("bytes"));
362                    }
363                    bytes = Some(map.next_value()?);
364                }
365                UploadField::Unknown => {
366                    map.next_value::<serde::de::IgnoredAny>()?;
367                }
368            }
369        }
370
371        Ok(UploadFile {
372            field_name: field_name.ok_or_else(|| A::Error::missing_field("field_name"))?,
373            file_name: file_name.unwrap_or_default(),
374            content_type: content_type.unwrap_or_default(),
375            bytes: bytes.ok_or_else(|| A::Error::missing_field("bytes"))?,
376        })
377    }
378}
379
380impl<'de> Deserialize<'de> for UploadFile {
381    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
382        const FIELDS: &[&str] = &["field_name", "file_name", "content_type", "bytes"];
383        deserializer.deserialize_struct("UploadFile", FIELDS, UploadFileVisitor)
384    }
385}
386
387/// A JSON response body the operation encoded itself.
388///
389/// [`Json<T>`] is encoded *after* the operation has returned, so the value it
390/// wraps has to own everything it prints. An operation that reads from a lock
391/// guard, an arena, or a shared corpus therefore has to clone every string it
392/// wants to include, and those clones are freed again a few microseconds later
393/// once the body has been written.
394///
395/// `PreparedJson<T>` moves the encode step inside the operation, where those
396/// borrows are still alive, and carries the finished bytes to the transport
397/// untouched. A listing operation can build a borrowed view over the store,
398/// encode it while it still holds the read guard, and never allocate an owned
399/// mirror of the data at all.
400///
401/// The type parameter carries the *documented* schema and nothing else: the
402/// operation still advertises `T` in `OpenAPI`, MCP, and compatibility
403/// analysis, exactly as `Json<T>` would.
404///
405/// # Contract
406///
407/// The framework does not re-parse the bytes, so it cannot check them against
408/// `T`. The operation asserts that the body it encoded is a valid instance of
409/// the schema it declares. Use [`PreparedJson::encode`] with a value whose
410/// serialized shape matches `T`; [`PreparedJson::from_bytes`] hands the same
411/// obligation to the caller for bytes produced some other way.
412///
413/// # Examples
414///
415/// ```
416/// use blazingly_core::{ApiSchema, PreparedJson, SchemaKind, TypeDescriptor};
417/// use serde::Serialize;
418///
419/// # struct Page;
420/// # impl ApiSchema for Page {
421/// #     fn type_descriptor() -> TypeDescriptor {
422/// #         TypeDescriptor::scalar("Page", SchemaKind::Object)
423/// #     }
424/// # }
425/// #[derive(Serialize)]
426/// struct BorrowedPage<'store> {
427///     items: Vec<&'store str>,
428/// }
429///
430/// let store = vec![String::from("first"), String::from("second")];
431/// let view = BorrowedPage {
432///     items: store.iter().map(String::as_str).collect(),
433/// };
434/// let body = PreparedJson::<Page>::encode(&view).expect("the view encodes");
435/// assert_eq!(body.as_bytes(), br#"{"items":["first","second"]}"#);
436/// ```
437pub struct PreparedJson<T> {
438    body: Vec<u8>,
439    schema: PhantomData<fn() -> T>,
440}
441
442impl<T> PreparedJson<T> {
443    /// Adopts bytes the caller has already encoded.
444    ///
445    /// The bytes are sent verbatim; see the type-level contract note.
446    #[must_use]
447    pub const fn from_bytes(body: Vec<u8>) -> Self {
448        Self {
449            body,
450            schema: PhantomData,
451        }
452    }
453
454    #[must_use]
455    pub fn as_bytes(&self) -> &[u8] {
456        &self.body
457    }
458
459    #[must_use]
460    pub fn into_bytes(self) -> Vec<u8> {
461        self.body
462    }
463
464    #[must_use]
465    pub const fn len(&self) -> usize {
466        self.body.len()
467    }
468
469    #[must_use]
470    pub const fn is_empty(&self) -> bool {
471        self.body.is_empty()
472    }
473
474    /// Encodes `value` into the response body now.
475    ///
476    /// `value` may borrow from anything alive at the call site, which is the
477    /// whole point: it is encoded before the borrow ends.
478    ///
479    /// # Errors
480    ///
481    /// Returns the `blazingly_json` failure when `value` cannot be encoded, for
482    /// instance because a map key is not a string.
483    pub fn encode<V>(value: &V) -> Result<Self, blazingly_json::Error>
484    where
485        V: Serialize + ?Sized,
486    {
487        let mut body = Vec::with_capacity(response_size_hint::<V>());
488        blazingly_json::to_writer(&mut body, value)?;
489        record_response_size::<V>(body.len());
490        Ok(Self::from_bytes(body))
491    }
492}
493
494impl<T> fmt::Debug for PreparedJson<T> {
495    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
496        formatter
497            .debug_struct("PreparedJson")
498            .field("bytes", &self.body.len())
499            .finish()
500    }
501}
502
503impl<T: ApiSchema> ApiSchema for PreparedJson<T> {
504    fn type_descriptor() -> TypeDescriptor {
505        T::type_descriptor()
506    }
507}
508
509/// A successful HTTP 201 response.
510#[derive(Clone, Debug, Eq, PartialEq)]
511pub struct Created<T>(pub T);
512
513/// A successful HTTP 202 response.
514#[derive(Clone, Debug, Eq, PartialEq)]
515pub struct Accepted<T>(pub T);
516
517/// A successful HTTP 204 response without a body.
518#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
519pub struct NoContent;
520
521/// One failure produced while an HTTP response body is being streamed.
522///
523/// The error is transport-neutral. Native servers can terminate the wire
524/// stream, while in-memory adapters return it directly to tests.
525#[derive(Clone, Debug, Eq, PartialEq)]
526pub struct BodyStreamError {
527    pub code: String,
528    pub message: String,
529}
530
531impl BodyStreamError {
532    #[must_use]
533    pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
534        Self {
535            code: code.into(),
536            message: message.into(),
537        }
538    }
539}
540
541impl fmt::Display for BodyStreamError {
542    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
543        formatter.write_str(&self.message)
544    }
545}
546
547impl std::error::Error for BodyStreamError {}
548
549/// Runtime-neutral, pull-based response byte stream.
550///
551/// A transport polls the next chunk only after it has capacity to write it.
552/// That pull boundary is the framework's backpressure contract; producers do
553/// not depend on Tokio, Compio, or a Cloudflare runtime.
554pub trait BodyStream: 'static {
555    fn poll_next(
556        self: Pin<&mut Self>,
557        context: &mut Context<'_>,
558    ) -> Poll<Option<Result<Vec<u8>, BodyStreamError>>>;
559
560    /// Offers a spent chunk's buffer back to the producer.
561    ///
562    /// A consumer that copies chunks out of the stream can return the vector
563    /// here once it is done with it; a producer that refills recycled buffers
564    /// then moves a long body with a handful of allocations instead of one
565    /// per chunk. Purely an optimization hint: the buffer's contents are dead
566    /// either way, and the default implementation simply drops it.
567    fn recycle(self: Pin<&mut Self>, spent: Vec<u8>) {
568        drop(spent);
569    }
570}
571
572/// Typed streaming HTTP response body.
573///
574/// `exact_length` is optional. HTTP/1 uses chunked transfer coding when it is
575/// absent, while HTTP/2 emits DATA frames without a content-length field.
576pub struct StreamingBody {
577    stream: Pin<Box<dyn BodyStream>>,
578    exact_length: Option<u64>,
579}
580
581impl StreamingBody {
582    #[must_use]
583    pub fn new(stream: impl BodyStream) -> Self {
584        Self {
585            stream: Box::pin(stream),
586            exact_length: None,
587        }
588    }
589
590    /// Builds a pull stream from already available chunks.
591    #[must_use]
592    pub fn from_chunks<I, Chunk>(chunks: I) -> Self
593    where
594        I: IntoIterator<Item = Chunk>,
595        I::IntoIter: Unpin + 'static,
596        Chunk: Into<Vec<u8>> + 'static,
597    {
598        Self::new(ChunkIterator {
599            chunks: chunks.into_iter(),
600        })
601    }
602
603    /// Builds a one-chunk stream with a known exact length.
604    #[must_use]
605    pub fn once(bytes: impl Into<Vec<u8>>) -> Self {
606        let bytes = bytes.into();
607        let length = u64::try_from(bytes.len()).unwrap_or(u64::MAX);
608        Self::from_chunks([bytes]).with_exact_length(length)
609    }
610
611    #[must_use]
612    pub const fn with_exact_length(mut self, length: u64) -> Self {
613        self.exact_length = Some(length);
614        self
615    }
616
617    #[must_use]
618    pub const fn exact_length(&self) -> Option<u64> {
619        self.exact_length
620    }
621
622    /// Waits until the producer yields one chunk.
623    ///
624    /// Calling this method is the consumer demand signal. Transports should
625    /// not request another chunk until the previous one has been written.
626    pub async fn next_chunk(&mut self) -> Option<Result<Vec<u8>, BodyStreamError>> {
627        poll_fn(|context| self.stream.as_mut().poll_next(context)).await
628    }
629
630    /// Hands a spent chunk's buffer back to the producer for reuse.
631    ///
632    /// See [`BodyStream::recycle`]; producers that do not reuse buffers
633    /// simply drop it.
634    pub fn recycle(&mut self, spent: Vec<u8>) {
635        self.stream.as_mut().recycle(spent);
636    }
637}
638
639impl fmt::Debug for StreamingBody {
640    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
641        formatter
642            .debug_struct("StreamingBody")
643            .field("exact_length", &self.exact_length)
644            .finish_non_exhaustive()
645    }
646}
647
648impl ApiSchema for StreamingBody {
649    fn type_descriptor() -> TypeDescriptor {
650        TypeDescriptor::scalar("StreamingBody", SchemaKind::Binary)
651    }
652}
653
654/// A transport error after an HTTP connection has switched protocols.
655#[derive(Clone, Debug, Eq, PartialEq)]
656pub struct UpgradeIoError {
657    pub code: String,
658    pub message: String,
659}
660
661impl UpgradeIoError {
662    #[must_use]
663    pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
664        Self {
665            code: code.into(),
666            message: message.into(),
667        }
668    }
669}
670
671impl fmt::Display for UpgradeIoError {
672    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
673        formatter.write_str(&self.message)
674    }
675}
676
677impl std::error::Error for UpgradeIoError {}
678
679/// Runtime-neutral byte I/O owned after an HTTP protocol upgrade.
680///
681/// Native and edge adapters implement this trait without exposing their socket
682/// or runtime types to handlers.
683pub type UpgradeReadFuture<'io> =
684    Pin<Box<dyn Future<Output = Result<Option<Vec<u8>>, UpgradeIoError>> + 'io>>;
685pub type UpgradeWriteFuture<'io> = Pin<Box<dyn Future<Output = Result<(), UpgradeIoError>> + 'io>>;
686
687pub trait UpgradedIo: 'static {
688    fn read(&mut self) -> UpgradeReadFuture<'_>;
689
690    fn write(&mut self, bytes: Vec<u8>) -> UpgradeWriteFuture<'_>;
691
692    fn shutdown(&mut self) -> UpgradeWriteFuture<'_>;
693}
694
695pub type UpgradeFuture = Pin<Box<dyn Future<Output = Result<(), UpgradeIoError>> + 'static>>;
696pub type UpgradeHandler = Box<dyn FnOnce(Box<dyn UpgradedIo>) -> UpgradeFuture + 'static>;
697
698/// A validated HTTP protocol switch plus its post-handshake session handler.
699pub struct HttpUpgrade {
700    protocol: &'static str,
701    headers: Vec<ResponseHeader>,
702    handler: Option<UpgradeHandler>,
703}
704
705impl HttpUpgrade {
706    #[must_use]
707    pub fn new(
708        protocol: &'static str,
709        headers: Vec<ResponseHeader>,
710        handler: impl FnOnce(Box<dyn UpgradedIo>) -> UpgradeFuture + 'static,
711    ) -> Self {
712        Self {
713            protocol,
714            headers,
715            handler: Some(Box::new(handler)),
716        }
717    }
718
719    #[must_use]
720    pub const fn protocol(&self) -> &'static str {
721        self.protocol
722    }
723
724    #[must_use]
725    pub fn headers(&self) -> &[ResponseHeader] {
726        &self.headers
727    }
728
729    pub fn extend_headers(&mut self, headers: impl IntoIterator<Item = ResponseHeader>) {
730        self.headers.extend(headers);
731    }
732
733    /// Runs the one-shot upgraded protocol session.
734    ///
735    /// # Errors
736    ///
737    /// Returns the adapter or upgraded protocol error produced by the session.
738    pub async fn run(mut self, io: Box<dyn UpgradedIo>) -> Result<(), UpgradeIoError> {
739        let handler = self.handler.take().ok_or_else(|| {
740            UpgradeIoError::new(
741                "upgrade_already_consumed",
742                "the protocol upgrade handler has already been consumed",
743            )
744        })?;
745        handler(io).await
746    }
747}
748
749impl fmt::Debug for HttpUpgrade {
750    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
751        formatter
752            .debug_struct("HttpUpgrade")
753            .field("protocol", &self.protocol)
754            .field("headers", &self.headers)
755            .finish_non_exhaustive()
756    }
757}
758
759impl ApiSchema for HttpUpgrade {
760    fn type_descriptor() -> TypeDescriptor {
761        TypeDescriptor::scalar("HttpUpgrade", SchemaKind::Binary)
762    }
763}
764
765/// A failure produced by work scheduled after an HTTP response is sent.
766#[derive(Clone, Debug, Eq, PartialEq)]
767pub struct BackgroundTaskError {
768    pub code: String,
769    pub message: String,
770}
771
772impl BackgroundTaskError {
773    #[must_use]
774    pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
775        Self {
776            code: code.into(),
777            message: message.into(),
778        }
779    }
780}
781
782impl fmt::Display for BackgroundTaskError {
783    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
784        formatter.write_str(&self.message)
785    }
786}
787
788impl std::error::Error for BackgroundTaskError {}
789
790pub type BackgroundFuture =
791    Pin<Box<dyn Future<Output = Result<(), BackgroundTaskError>> + 'static>>;
792
793/// One runtime-neutral task that begins after the response body is written.
794pub struct BackgroundTask {
795    task: Option<Box<dyn FnOnce() -> BackgroundFuture + 'static>>,
796}
797
798impl BackgroundTask {
799    #[must_use]
800    pub fn new<Task, TaskFuture>(task: Task) -> Self
801    where
802        Task: FnOnce() -> TaskFuture + 'static,
803        TaskFuture: Future<Output = Result<(), BackgroundTaskError>> + 'static,
804    {
805        Self {
806            task: Some(Box::new(move || Box::pin(task()))),
807        }
808    }
809
810    #[must_use]
811    pub fn infallible<Task, TaskFuture>(task: Task) -> Self
812    where
813        Task: FnOnce() -> TaskFuture + 'static,
814        TaskFuture: Future<Output = ()> + 'static,
815    {
816        Self::new(move || async move {
817            task().await;
818            Ok(())
819        })
820    }
821
822    /// Runs this task exactly once.
823    ///
824    /// # Errors
825    ///
826    /// Returns the task failure or an already-consumed error.
827    pub async fn run(mut self) -> Result<(), BackgroundTaskError> {
828        let task = self.task.take().ok_or_else(|| {
829            BackgroundTaskError::new(
830                "background_task_consumed",
831                "background task has already been consumed",
832            )
833        })?;
834        task().await
835    }
836}
837
838impl fmt::Debug for BackgroundTask {
839    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
840        formatter
841            .debug_struct("BackgroundTask")
842            .finish_non_exhaustive()
843    }
844}
845
846/// A typed response carrying work that starts after its wire body is sent.
847#[derive(Debug)]
848pub struct Background<T> {
849    response: T,
850    tasks: Vec<BackgroundTask>,
851}
852
853impl<T> Background<T> {
854    #[must_use]
855    pub fn new(response: T) -> Self {
856        Self {
857            response,
858            tasks: Vec::new(),
859        }
860    }
861
862    #[must_use]
863    pub fn task(mut self, task: BackgroundTask) -> Self {
864        self.tasks.push(task);
865        self
866    }
867
868    #[must_use]
869    pub fn into_parts(self) -> (T, Vec<BackgroundTask>) {
870        (self.response, self.tasks)
871    }
872}
873
874impl<T: ApiSchema> ApiSchema for Background<T> {
875    fn type_descriptor() -> TypeDescriptor {
876        T::type_descriptor()
877    }
878}
879
880/// Ergonomic after-response task decoration.
881pub trait BackgroundExt: Sized {
882    #[must_use]
883    fn background(self, task: BackgroundTask) -> Background<Self> {
884        Background::new(self).task(task)
885    }
886}
887
888impl<T> BackgroundExt for T {}
889
890/// Prefixes nested model violations while preserving stable codes/messages.
891pub fn merge_validation_errors(
892    target: &mut ValidationErrors,
893    prefix: &str,
894    nested: &ValidationErrors,
895) {
896    for violation in nested.violations() {
897        let field = if violation.field.is_empty() {
898            prefix.to_owned()
899        } else {
900            format!("{prefix}.{}", violation.field)
901        };
902        target.push(field, violation.code.clone(), violation.message.clone());
903    }
904}
905
906/// Records a field validator's violations under the field it was declared on.
907///
908/// A field validator is handed the value alone and cannot see the public name
909/// of the field it is checking, yet the obvious way to write one is to name
910/// that field anyway. Prefixing unconditionally, the way
911/// [`merge_validation_errors`] does for a nested model, turns that into
912/// `published_at.published_at`.
913///
914/// A violation is therefore taken to be about the field itself when its path
915/// is empty or is the field, and to be a path inside the value otherwise.
916pub fn merge_field_validation_errors(
917    target: &mut ValidationErrors,
918    field: &str,
919    nested: &ValidationErrors,
920) {
921    for violation in nested.violations() {
922        let path = if field.is_empty() {
923            violation.field.clone()
924        } else if violation.field.is_empty() || violation.field == field {
925            field.to_owned()
926        } else if is_rooted_at(&violation.field, field) {
927            violation.field.clone()
928        } else {
929            format!("{field}.{}", violation.field)
930        };
931        target.push(path, violation.code.clone(), violation.message.clone());
932    }
933}
934
935/// Reports whether `path` already descends from `field`.
936fn is_rooted_at(path: &str, field: &str) -> bool {
937    path.strip_prefix(field)
938        .is_some_and(|rest| rest.starts_with('.') || rest.starts_with('['))
939}
940
941/// A value type whose field rules are declared once and reused by name.
942///
943/// `#[api_model]` implements this for a one-field tuple struct and for a
944/// unit-variant enum. A model that carries such a field inherits
945/// [`ApiConstrained::constraint_rules`] into its own field descriptor and runs
946/// [`ApiConstrained::validate_constraints`] while validating, so a bundle of
947/// rules written once applies to every field declared with the type.
948pub trait ApiConstrained: ApiSchema {
949    /// Rules a model records for every field declared with this type.
950    fn constraint_rules() -> Vec<ValidationRule>;
951
952    /// Runs the declared rules against one value.
953    ///
954    /// # Errors
955    ///
956    /// Returns the violations found. Their field paths are relative to the
957    /// value, so the caller names the field the value was found in.
958    fn validate_constraints(&self) -> Result<(), ValidationErrors>;
959}
960
961/// Field metadata carried inside [`ValidationRule::Custom`].
962///
963/// The contract's rule list has no variant for a default, for nullability, or
964/// for a string enumeration, and its encoding is frozen. `#[api_model]`
965/// therefore writes them as `keyword=value` strings — the channel already used
966/// for `pattern` and the numeric bounds — and a schema projection recovers them
967/// with [`FieldMetadata::parse`] instead of treating them as opaque validator
968/// names.
969#[derive(Clone, Debug, PartialEq)]
970pub enum FieldMetadata {
971    /// Value substituted when the field is absent from the request.
972    Default(blazingly_json::Value),
973    /// The field accepts `null` in addition to its declared type.
974    Nullable,
975    /// The complete set of accepted string values, in declaration order.
976    Enumeration(Vec<String>),
977}
978
979impl FieldMetadata {
980    /// Parses the canonical `keyword=value` encoding emitted by `#[api_model]`.
981    #[must_use]
982    pub fn parse(encoded: &str) -> Option<Self> {
983        let (keyword, value) = encoded.split_once('=')?;
984        let metadata = match keyword {
985            "default" => Self::Default(blazingly_json::from_str(value).ok()?),
986            "nullable" if value == "true" => Self::Nullable,
987            "enum" if !value.is_empty() => {
988                Self::Enumeration(value.split('|').map(str::to_owned).collect())
989            }
990            _ => return None,
991        };
992        Some(metadata)
993    }
994
995    /// JSON Schema keyword this metadata projects to.
996    #[must_use]
997    pub const fn keyword(&self) -> &'static str {
998        match self {
999            Self::Default(_) => "default",
1000            Self::Nullable => "nullable",
1001            Self::Enumeration(_) => "enum",
1002        }
1003    }
1004
1005    /// JSON Schema value this metadata projects to.
1006    #[must_use]
1007    pub fn schema_value(&self) -> blazingly_json::Value {
1008        match self {
1009            Self::Default(value) => value.clone(),
1010            Self::Nullable => blazingly_json::Value::Bool(true),
1011            Self::Enumeration(values) => blazingly_json::Value::Array(
1012                values
1013                    .iter()
1014                    .map(|value| blazingly_json::Value::String(value.clone()))
1015                    .collect(),
1016            ),
1017        }
1018    }
1019
1020    /// Writes the metadata into a JSON Schema object in place.
1021    pub fn apply_json_schema(&self, schema: &mut blazingly_json::Value) {
1022        if let Some(object) = schema.as_object_mut() {
1023            object.insert(self.keyword().to_owned(), self.schema_value());
1024        }
1025    }
1026}
1027
1028impl fmt::Display for FieldMetadata {
1029    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1030        match self {
1031            Self::Default(value) => write!(formatter, "default={value}"),
1032            Self::Nullable => formatter.write_str("nullable=true"),
1033            Self::Enumeration(values) => {
1034                formatter.write_str("enum=")?;
1035                for (index, value) in values.iter().enumerate() {
1036                    if index > 0 {
1037                        formatter.write_str("|")?;
1038                    }
1039                    formatter.write_str(value)?;
1040                }
1041                Ok(())
1042            }
1043        }
1044    }
1045}
1046
1047// ---------------------------------------------------------------------------
1048// multipart/form-data syntax
1049// ---------------------------------------------------------------------------
1050
1051/// Largest header block accepted for one `multipart/form-data` part.
1052///
1053/// The buffered and the streaming readers share this bound, so a body that one
1054/// accepts is not rejected by the other.
1055pub const MAX_MULTIPART_HEADER_BYTES: usize = 16 * 1024;
1056
1057/// Largest number of parts accepted in one `multipart/form-data` body.
1058pub const MAX_MULTIPART_PARTS: usize = 256;
1059
1060/// The `Content-Disposition` metadata of one multipart part.
1061#[derive(Clone, Debug, Default, Eq, PartialEq)]
1062pub struct MultipartPartHeaders {
1063    pub name: String,
1064    pub file_name: Option<String>,
1065    pub content_type: Option<String>,
1066}
1067
1068/// Extracts the boundary from a `multipart/form-data` `Content-Type`.
1069///
1070/// Returns `None` when the media type is something else, when the boundary
1071/// parameter is absent, or when the boundary is not the 1..=70 printable
1072/// non-space characters RFC 2046 allows.
1073#[doc(hidden)]
1074#[must_use]
1075pub fn multipart_boundary(content_type: &str) -> Option<String> {
1076    let mut parameters = header_parameters(content_type);
1077    if !parameters
1078        .next()?
1079        .trim()
1080        .eq_ignore_ascii_case("multipart/form-data")
1081    {
1082        return None;
1083    }
1084    for parameter in parameters {
1085        let (name, value) = parameter.split_once('=')?;
1086        if name.trim().eq_ignore_ascii_case("boundary") {
1087            let boundary = unquote_header_value(value.trim())?;
1088            if boundary.is_empty()
1089                || boundary.len() > 70
1090                || boundary.bytes().any(|byte| byte <= b' ' || byte >= 127)
1091            {
1092                return None;
1093            }
1094            return Some(boundary);
1095        }
1096    }
1097    None
1098}
1099
1100/// Parses one multipart part's header block.
1101///
1102/// # Errors
1103///
1104/// Returns the stable reason string both multipart readers report.
1105#[doc(hidden)]
1106pub fn multipart_part_headers(headers: &str) -> Result<MultipartPartHeaders, &'static str> {
1107    let mut name = None;
1108    let mut file_name = None;
1109    let mut content_type = None;
1110    for line in headers.split("\r\n") {
1111        let (header_name, value) = line
1112            .split_once(':')
1113            .ok_or("multipart part header is malformed")?;
1114        if header_name.eq_ignore_ascii_case("content-disposition") {
1115            let mut parameters = header_parameters(value);
1116            if !parameters
1117                .next()
1118                .is_some_and(|value| value.trim().eq_ignore_ascii_case("form-data"))
1119            {
1120                return Err("multipart Content-Disposition must be form-data");
1121            }
1122            for parameter in parameters {
1123                let Some((parameter_name, parameter_value)) = parameter.split_once('=') else {
1124                    continue;
1125                };
1126                if parameter_name.trim().eq_ignore_ascii_case("name") {
1127                    name = unquote_header_value(parameter_value.trim());
1128                } else if parameter_name.trim().eq_ignore_ascii_case("filename") {
1129                    file_name = unquote_header_value(parameter_value.trim());
1130                }
1131            }
1132        } else if header_name.eq_ignore_ascii_case("content-type") {
1133            content_type = Some(value.trim().to_owned());
1134        }
1135    }
1136    let name = name
1137        .filter(|name| !name.is_empty())
1138        .ok_or("multipart part has no field name")?;
1139    Ok(MultipartPartHeaders {
1140        name,
1141        file_name,
1142        content_type,
1143    })
1144}
1145
1146/// Splits a header value on unquoted semicolons.
1147#[doc(hidden)]
1148pub fn header_parameters(value: &str) -> impl Iterator<Item = &str> {
1149    let mut start = 0;
1150    let mut quoted = false;
1151    let mut escaped = false;
1152    let mut ranges = Vec::new();
1153    for (index, character) in value.char_indices() {
1154        if escaped {
1155            escaped = false;
1156        } else if character == '\\' && quoted {
1157            escaped = true;
1158        } else if character == '"' {
1159            quoted = !quoted;
1160        } else if character == ';' && !quoted {
1161            ranges.push((start, index));
1162            start = index + character.len_utf8();
1163        }
1164    }
1165    ranges.push((start, value.len()));
1166    ranges
1167        .into_iter()
1168        .map(move |(start, end)| &value[start..end])
1169}
1170
1171/// Removes the quotes and backslash escapes from one header parameter value.
1172#[doc(hidden)]
1173#[must_use]
1174pub fn unquote_header_value(value: &str) -> Option<String> {
1175    if let Some(value) = value.strip_prefix('"') {
1176        let value = value.strip_suffix('"')?;
1177        let mut output = String::with_capacity(value.len());
1178        let mut escaped = false;
1179        for character in value.chars() {
1180            if escaped {
1181                output.push(character);
1182                escaped = false;
1183            } else if character == '\\' {
1184                escaped = true;
1185            } else {
1186                output.push(character);
1187            }
1188        }
1189        if escaped {
1190            return None;
1191        }
1192        Some(output)
1193    } else {
1194        Some(value.to_owned())
1195    }
1196}
1197
1198/// Finds `needle` in `haystack` at or after `from`.
1199///
1200/// The scan looks for the first byte and only then compares the rest, so a
1201/// megabyte of upload costs one pass over the data rather than one comparison
1202/// per needle byte per position.
1203#[doc(hidden)]
1204#[must_use]
1205pub fn find_bytes(haystack: &[u8], needle: &[u8], from: usize) -> Option<usize> {
1206    let (first, rest) = needle.split_first()?;
1207    let last_start = haystack.len().checked_sub(needle.len())?;
1208    let mut position = from;
1209    while position <= last_start {
1210        // The first-byte scan runs over every byte of a streamed upload, so
1211        // it uses `memchr`'s vectorized search rather than a scalar loop.
1212        let offset = memchr::memchr(*first, haystack.get(position..=last_start)?)?;
1213        let candidate = position + offset;
1214        if haystack.get(candidate + 1..candidate + needle.len()) == Some(rest) {
1215            return Some(candidate);
1216        }
1217        position = candidate + 1;
1218    }
1219    None
1220}
1221
1222// ---------------------------------------------------------------------------
1223// Streaming multipart request bodies
1224// ---------------------------------------------------------------------------
1225
1226/// A failure produced while reading a `multipart/form-data` request body.
1227#[derive(Clone, Debug, Eq, PartialEq)]
1228pub enum MultipartError {
1229    /// The bytes are not a well-formed `multipart/form-data` document.
1230    ///
1231    /// The reason strings are the ones the buffered `Multipart<T>` extractor
1232    /// already reports, and the projected failure carries the same status and
1233    /// code, so a malformed body looks the same whichever reader saw it.
1234    Malformed(&'static str),
1235    /// A handler asked to buffer a part that turned out to be larger than the
1236    /// limit it named.
1237    TooLarge { limit: usize },
1238    /// The transport could not deliver the rest of the body.
1239    ///
1240    /// Surfacing this instead of treating it as the end of the body is what
1241    /// stops a truncated upload from being reported as a complete one.
1242    Transport(BodyStreamError),
1243}
1244
1245const MALFORMED_MULTIPART_MESSAGE: &str = "request body is not valid multipart form data";
1246const UPLOAD_STREAM_MESSAGE: &str = "the request body could not be read to its end";
1247const MULTIPART_TOO_LARGE_MESSAGE: &str = "multipart part exceeds the limit the handler set";
1248
1249impl MultipartError {
1250    /// The HTTP status this failure projects to.
1251    #[must_use]
1252    pub const fn status(&self) -> u16 {
1253        match self {
1254            Self::Malformed(_) => 422,
1255            Self::TooLarge { .. } => 413,
1256            Self::Transport(_) => 400,
1257        }
1258    }
1259
1260    /// The stable client-visible code this failure projects to.
1261    #[must_use]
1262    pub const fn code(&self) -> &'static str {
1263        match self {
1264            Self::Malformed(_) => "invalid_multipart",
1265            Self::TooLarge { .. } => "payload_too_large",
1266            Self::Transport(_) => "upload_stream_failed",
1267        }
1268    }
1269
1270    /// The stable client-visible message this failure projects to.
1271    #[must_use]
1272    pub const fn message(&self) -> &'static str {
1273        match self {
1274            Self::Malformed(_) => MALFORMED_MULTIPART_MESSAGE,
1275            Self::TooLarge { .. } => MULTIPART_TOO_LARGE_MESSAGE,
1276            Self::Transport(_) => UPLOAD_STREAM_MESSAGE,
1277        }
1278    }
1279
1280    fn details(&self) -> blazingly_json::Value {
1281        let mut details = blazingly_json::Map::new();
1282        match self {
1283            Self::Malformed(reason) => {
1284                details.insert(
1285                    "source".to_owned(),
1286                    blazingly_json::Value::String("multipart".to_owned()),
1287                );
1288                details.insert(
1289                    "reason".to_owned(),
1290                    blazingly_json::Value::String((*reason).to_owned()),
1291                );
1292            }
1293            Self::TooLarge { limit } => {
1294                details.insert(
1295                    "source".to_owned(),
1296                    blazingly_json::Value::String("multipart".to_owned()),
1297                );
1298                details.insert("limit".to_owned(), blazingly_json::Value::from(*limit));
1299            }
1300            Self::Transport(error) => {
1301                details.insert(
1302                    "source".to_owned(),
1303                    blazingly_json::Value::String("stream".to_owned()),
1304                );
1305                details.insert(
1306                    "reason".to_owned(),
1307                    blazingly_json::Value::String(error.code.clone()),
1308                );
1309            }
1310        }
1311        blazingly_json::Value::Object(details)
1312    }
1313}
1314
1315impl fmt::Display for MultipartError {
1316    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1317        match self {
1318            Self::Malformed(reason) => formatter.write_str(reason),
1319            Self::TooLarge { limit } => {
1320                write!(formatter, "multipart part exceeds the {limit}-byte limit")
1321            }
1322            Self::Transport(error) => formatter.write_str(&error.message),
1323        }
1324    }
1325}
1326
1327impl std::error::Error for MultipartError {}
1328
1329impl ApiError for MultipartError {
1330    fn response_descriptors() -> Vec<ResponseDescriptor> {
1331        vec![
1332            ResponseDescriptor::error(400, "upload_stream_failed", UPLOAD_STREAM_MESSAGE, None),
1333            ResponseDescriptor::error(413, "payload_too_large", MULTIPART_TOO_LARGE_MESSAGE, None),
1334            ResponseDescriptor::error(422, "invalid_multipart", MALFORMED_MULTIPART_MESSAGE, None),
1335        ]
1336    }
1337
1338    fn into_failure(self) -> Result<OperationFailure, ResponseBuildError> {
1339        let details = blazingly_json::to_vec(&self.details())
1340            .map_err(|_| ResponseBuildError::serialization_failed())?;
1341        Ok(OperationFailure::new(self.status(), self.code(), self.message()).with_details(details))
1342    }
1343}
1344
1345/// Where a [`MultipartStream`] currently is in the document.
1346#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1347enum MultipartState {
1348    /// Before the opening delimiter.
1349    Opening,
1350    /// Between two parts, with a header block still to read.
1351    Headers,
1352    /// Inside one part's data.
1353    Part,
1354    /// The closing delimiter has been consumed.
1355    Done,
1356    /// A failure was already reported; the document cannot be resumed.
1357    Failed,
1358}
1359
1360/// What a scan of the available part data found.
1361enum MultipartScan {
1362    /// A closing delimiter begins at this offset.
1363    Terminator(usize),
1364    /// This many leading bytes are certainly part data.
1365    ///
1366    /// The rest is withheld because it could still turn out to be the start of
1367    /// a delimiter once more bytes arrive; it is never more than the delimiter
1368    /// plus four bytes.
1369    Data(usize),
1370}
1371
1372/// A `multipart/form-data` request body read part by part, chunk by chunk.
1373///
1374/// The buffered [`Multipart<T>`] extractor materializes every part before the
1375/// handler starts, which costs resident memory proportional to the requests in
1376/// flight. This reader instead drives the pull-based request body: it holds one
1377/// transport chunk plus, at most, a delimiter's worth of look-ahead, whatever
1378/// the upload's size. A handler that counts bytes and drops them holds nothing.
1379///
1380/// Transport limits are not re-implemented here. The stream this reads from is
1381/// the adapter's, so the decoded-size limit, the chunk-count limit, and the
1382/// body read deadline apply exactly as they do to [`StreamingBody`] itself, and
1383/// they apply while the body is arriving rather than after it has all been
1384/// accepted.
1385///
1386/// # Examples
1387///
1388/// ```no_run
1389/// # use blazingly_core::{MultipartError, MultipartStream};
1390/// # async fn count(mut multipart: MultipartStream) -> Result<usize, MultipartError> {
1391/// let mut bytes = 0;
1392/// while let Some(mut field) = multipart.next_field().await? {
1393///     if field.name() != "file" {
1394///         continue;
1395///     }
1396///     while let Some(chunk) = field.next_chunk().await? {
1397///         bytes += chunk.len();
1398///     }
1399/// }
1400/// # Ok(bytes)
1401/// # }
1402/// ```
1403#[derive(Debug)]
1404pub struct MultipartStream {
1405    body: StreamingBody,
1406    /// `--` followed by the declared boundary.
1407    delimiter: Vec<u8>,
1408    buffer: Vec<u8>,
1409    cursor: usize,
1410    state: MultipartState,
1411    parts: usize,
1412    bytes_read: u64,
1413}
1414
1415impl MultipartStream {
1416    /// Reads `body` as the `multipart/form-data` document `content_type`
1417    /// describes.
1418    ///
1419    /// # Errors
1420    ///
1421    /// Returns [`MultipartError::Malformed`] when `content_type` is not
1422    /// `multipart/form-data` with a usable boundary.
1423    pub fn new(body: StreamingBody, content_type: &str) -> Result<Self, MultipartError> {
1424        let boundary = multipart_boundary(content_type).ok_or(MultipartError::Malformed(
1425            "multipart boundary is missing or invalid",
1426        ))?;
1427        let mut delimiter = Vec::with_capacity(boundary.len() + 2);
1428        delimiter.extend_from_slice(b"--");
1429        delimiter.extend_from_slice(boundary.as_bytes());
1430        Ok(Self {
1431            body,
1432            delimiter,
1433            buffer: Vec::new(),
1434            cursor: 0,
1435            state: MultipartState::Opening,
1436            parts: 0,
1437            bytes_read: 0,
1438        })
1439    }
1440
1441    /// Bytes pulled from the transport so far, framing included.
1442    #[must_use]
1443    pub const fn bytes_read(&self) -> u64 {
1444        self.bytes_read
1445    }
1446
1447    /// Advances to the next part, skipping whatever is left of the current one.
1448    ///
1449    /// Returns `None` once the closing delimiter has been read.
1450    ///
1451    /// # Errors
1452    ///
1453    /// Returns [`MultipartError`] when the document is malformed, when it ends
1454    /// before its closing delimiter, or when the transport fails.
1455    pub async fn next_field(&mut self) -> Result<Option<MultipartField<'_>>, MultipartError> {
1456        match self.state {
1457            MultipartState::Failed => {
1458                return Err(MultipartError::Malformed(
1459                    "multipart body already failed to parse",
1460                ));
1461            }
1462            MultipartState::Done => return Ok(None),
1463            MultipartState::Opening => self.read_opening().await?,
1464            MultipartState::Part => self.skip_part().await?,
1465            MultipartState::Headers => {}
1466        }
1467        if self.state == MultipartState::Done {
1468            return Ok(None);
1469        }
1470        let headers = self.read_headers().await?;
1471        self.parts += 1;
1472        if self.parts > MAX_MULTIPART_PARTS {
1473            return Err(self.fail("multipart body contains too many parts"));
1474        }
1475        self.state = MultipartState::Part;
1476        Ok(Some(MultipartField {
1477            stream: self,
1478            headers,
1479        }))
1480    }
1481
1482    /// Marks the document unusable and produces the failure to report.
1483    fn fail(&mut self, reason: &'static str) -> MultipartError {
1484        self.state = MultipartState::Failed;
1485        MultipartError::Malformed(reason)
1486    }
1487
1488    /// Bytes buffered but not yet consumed.
1489    const fn available(&self) -> usize {
1490        self.buffer.len() - self.cursor
1491    }
1492
1493    /// Pulls one transport chunk, reporting whether the body had more.
1494    async fn fill(&mut self) -> Result<bool, MultipartError> {
1495        loop {
1496            match self.body.next_chunk().await {
1497                Some(Ok(chunk)) if chunk.is_empty() => {}
1498                Some(Ok(chunk)) => {
1499                    // Dropping the consumed prefix before growing the buffer is
1500                    // what keeps a five-megabyte upload resident in kilobytes.
1501                    // The leftover is at most a delimiter's worth of withheld
1502                    // look-ahead, so the move is a few dozen bytes per refill.
1503                    if self.cursor > 0 {
1504                        self.buffer.drain(..self.cursor);
1505                        self.cursor = 0;
1506                    }
1507                    self.bytes_read = self
1508                        .bytes_read
1509                        .saturating_add(u64::try_from(chunk.len()).unwrap_or(u64::MAX));
1510                    self.buffer.extend_from_slice(&chunk);
1511                    // The chunk's bytes now live in the window, so its buffer
1512                    // goes back to the producer to be refilled.
1513                    self.body.recycle(chunk);
1514                    return Ok(true);
1515                }
1516                Some(Err(error)) => {
1517                    self.state = MultipartState::Failed;
1518                    return Err(MultipartError::Transport(error));
1519                }
1520                None => return Ok(false),
1521            }
1522        }
1523    }
1524
1525    /// Consumes the opening delimiter.
1526    async fn read_opening(&mut self) -> Result<(), MultipartError> {
1527        while self.available() < self.delimiter.len() {
1528            if !self.fill().await? {
1529                return Err(self.fail("multipart body does not start with its declared boundary"));
1530            }
1531        }
1532        if !self.buffer[self.cursor..].starts_with(&self.delimiter) {
1533            return Err(self.fail("multipart body does not start with its declared boundary"));
1534        }
1535        self.cursor += self.delimiter.len();
1536        self.read_boundary_suffix().await
1537    }
1538
1539    /// Consumes the two bytes that follow a delimiter and picks the next state.
1540    async fn read_boundary_suffix(&mut self) -> Result<(), MultipartError> {
1541        while self.available() < 2 {
1542            if !self.fill().await? {
1543                return Err(self.fail("multipart boundary is malformed"));
1544            }
1545        }
1546        let suffix = [self.buffer[self.cursor], self.buffer[self.cursor + 1]];
1547        self.cursor += 2;
1548        match &suffix {
1549            b"--" => {
1550                self.state = MultipartState::Done;
1551                Ok(())
1552            }
1553            b"\r\n" => {
1554                self.state = MultipartState::Headers;
1555                Ok(())
1556            }
1557            _ => Err(self.fail("multipart boundary is malformed")),
1558        }
1559    }
1560
1561    /// Reads one part's header block.
1562    async fn read_headers(&mut self) -> Result<MultipartPartHeaders, MultipartError> {
1563        let mut searched = 0;
1564        let end = loop {
1565            if let Some(found) = find_bytes(&self.buffer[self.cursor..], b"\r\n\r\n", searched) {
1566                break found;
1567            }
1568            let available = self.available();
1569            if available > MAX_MULTIPART_HEADER_BYTES {
1570                return Err(self.fail("multipart part headers exceed the configured limit"));
1571            }
1572            searched = available.saturating_sub(3);
1573            if !self.fill().await? {
1574                return Err(self.fail("multipart part headers are incomplete"));
1575            }
1576        };
1577        if end > MAX_MULTIPART_HEADER_BYTES {
1578            return Err(self.fail("multipart part headers exceed the configured limit"));
1579        }
1580        let parsed = match std::str::from_utf8(&self.buffer[self.cursor..self.cursor + end]) {
1581            Ok(headers) => multipart_part_headers(headers),
1582            Err(_) => Err("multipart part headers are not valid UTF-8"),
1583        };
1584        let parsed = parsed.map_err(|reason| self.fail(reason))?;
1585        self.cursor += end + 4;
1586        Ok(parsed)
1587    }
1588
1589    /// Consumes whatever is left of the current part.
1590    async fn skip_part(&mut self) -> Result<(), MultipartError> {
1591        while self.advance_part().await?.is_some() {}
1592        Ok(())
1593    }
1594
1595    /// Advances past the next run of part data.
1596    ///
1597    /// The returned pair is a range in `self.buffer` that stays valid until the
1598    /// next call, which is what lets a chunk be handed out without copying it.
1599    async fn advance_part(&mut self) -> Result<Option<(usize, usize)>, MultipartError> {
1600        loop {
1601            if self.state != MultipartState::Part {
1602                return Ok(None);
1603            }
1604            match scan_multipart_terminator(&self.buffer[self.cursor..], &self.delimiter) {
1605                MultipartScan::Terminator(offset) => {
1606                    if offset > 0 {
1607                        let start = self.cursor;
1608                        self.cursor += offset;
1609                        return Ok(Some((start, self.cursor)));
1610                    }
1611                    self.cursor += 2 + self.delimiter.len();
1612                    self.read_boundary_suffix().await?;
1613                    return Ok(None);
1614                }
1615                MultipartScan::Data(safe) => {
1616                    if safe > 0 {
1617                        let start = self.cursor;
1618                        self.cursor += safe;
1619                        return Ok(Some((start, self.cursor)));
1620                    }
1621                    if !self.fill().await? {
1622                        return Err(self.fail("multipart part has no closing boundary"));
1623                    }
1624                }
1625            }
1626        }
1627    }
1628}
1629
1630/// One part of a streamed `multipart/form-data` body.
1631///
1632/// Dropping a field without reading it to the end is allowed; the next
1633/// [`MultipartStream::next_field`] call skips whatever is left.
1634#[derive(Debug)]
1635pub struct MultipartField<'stream> {
1636    stream: &'stream mut MultipartStream,
1637    headers: MultipartPartHeaders,
1638}
1639
1640impl MultipartField<'_> {
1641    /// The part's form field name.
1642    #[must_use]
1643    pub fn name(&self) -> &str {
1644        &self.headers.name
1645    }
1646
1647    /// The part's declared file name, when it has one.
1648    #[must_use]
1649    pub fn file_name(&self) -> Option<&str> {
1650        self.headers.file_name.as_deref()
1651    }
1652
1653    /// The part's declared media type, when it has one.
1654    #[must_use]
1655    pub fn content_type(&self) -> Option<&str> {
1656        self.headers.content_type.as_deref()
1657    }
1658
1659    /// Pulls the next run of this part's data.
1660    ///
1661    /// The slice borrows the reader's buffer and is valid until the next call,
1662    /// so nothing is copied on the way to the handler. Returns `None` once the
1663    /// part's closing delimiter has been reached.
1664    ///
1665    /// # Errors
1666    ///
1667    /// Returns [`MultipartError`] when the document ends before its closing
1668    /// delimiter or when the transport fails.
1669    pub async fn next_chunk(&mut self) -> Result<Option<&[u8]>, MultipartError> {
1670        match self.stream.advance_part().await? {
1671            Some((start, end)) => Ok(Some(&self.stream.buffer[start..end])),
1672            None => Ok(None),
1673        }
1674    }
1675
1676    /// Deliberately buffers the rest of this part, with an explicit limit.
1677    ///
1678    /// # Errors
1679    ///
1680    /// Returns [`MultipartError::TooLarge`] when the part is longer than
1681    /// `limit`, or the reader failure that stopped it.
1682    pub async fn collect(mut self, limit: usize) -> Result<Vec<u8>, MultipartError> {
1683        let mut bytes = Vec::new();
1684        while let Some(chunk) = self.next_chunk().await? {
1685            if bytes.len().saturating_add(chunk.len()) > limit {
1686                return Err(MultipartError::TooLarge { limit });
1687            }
1688            bytes.extend_from_slice(chunk);
1689        }
1690        Ok(bytes)
1691    }
1692
1693    /// Deliberately buffers the rest of this part as text.
1694    ///
1695    /// # Errors
1696    ///
1697    /// Returns [`MultipartError::TooLarge`] when the part is longer than
1698    /// `limit`, [`MultipartError::Malformed`] when it is not UTF-8, or the
1699    /// reader failure that stopped it.
1700    pub async fn text(self, limit: usize) -> Result<String, MultipartError> {
1701        let bytes = self.collect(limit).await?;
1702        String::from_utf8(bytes)
1703            .map_err(|_| MultipartError::Malformed("multipart text field is not valid UTF-8"))
1704    }
1705
1706    /// Deliberately buffers the rest of this part as an [`UploadFile`].
1707    ///
1708    /// This is the bridge to the buffered API: an operation that wants the
1709    /// bytes after all gets the same value the `File<UploadFile>` extractor
1710    /// would have produced, having chosen the limit itself.
1711    ///
1712    /// # Errors
1713    ///
1714    /// Returns [`MultipartError::TooLarge`] when the part is longer than
1715    /// `limit`, or the reader failure that stopped it.
1716    pub async fn into_upload(self, limit: usize) -> Result<UploadFile, MultipartError> {
1717        let field_name = self.headers.name.clone();
1718        let file_name = self.headers.file_name.clone();
1719        let content_type = self.headers.content_type.clone();
1720        let bytes = self.collect(limit).await?;
1721        Ok(UploadFile {
1722            field_name,
1723            file_name,
1724            content_type,
1725            bytes,
1726        })
1727    }
1728}
1729
1730/// Looks for the closing delimiter in the part data available so far.
1731fn scan_multipart_terminator(data: &[u8], delimiter: &[u8]) -> MultipartScan {
1732    let mut from = 0;
1733    while let Some(found) = find_bytes(data, b"\r\n--", from) {
1734        let start = found + 2;
1735        let end = start + delimiter.len();
1736        if end + 2 > data.len() {
1737            // Undecidable yet. Withhold the candidate only while what did
1738            // arrive still agrees with the delimiter; otherwise it is data and
1739            // the scan continues past it.
1740            let seen = &data[start..];
1741            let compared = seen.len().min(delimiter.len());
1742            if seen.get(..compared) == delimiter.get(..compared) {
1743                return MultipartScan::Data(found);
1744            }
1745            from = found + 2;
1746            continue;
1747        }
1748        if data.get(start..end) == Some(delimiter)
1749            && matches!(data.get(end..end + 2), Some(b"\r\n" | b"--"))
1750        {
1751            return MultipartScan::Terminator(found);
1752        }
1753        from = found + 2;
1754    }
1755    // A trailing `\r`, `\r\n`, or `\r\n-` could still become a delimiter.
1756    MultipartScan::Data(data.len().saturating_sub(3))
1757}
1758
1759struct ChunkIterator<I> {
1760    chunks: I,
1761}
1762
1763impl<I, Chunk> BodyStream for ChunkIterator<I>
1764where
1765    I: Iterator<Item = Chunk> + Unpin + 'static,
1766    Chunk: Into<Vec<u8>> + 'static,
1767{
1768    fn poll_next(
1769        self: Pin<&mut Self>,
1770        _context: &mut Context<'_>,
1771    ) -> Poll<Option<Result<Vec<u8>, BodyStreamError>>> {
1772        Poll::Ready(self.get_mut().chunks.next().map(|chunk| Ok(chunk.into())))
1773    }
1774}
1775
1776/// Overrides the successful status of another typed response.
1777#[derive(Clone, Debug, Eq, PartialEq)]
1778pub struct Status<const STATUS: u16, T>(pub T);
1779
1780/// Adds response headers without changing the typed response body.
1781#[derive(Clone, Debug, Eq, PartialEq)]
1782pub struct WithHeaders<T> {
1783    response: T,
1784    headers: Vec<ResponseHeader>,
1785}
1786
1787impl<T> WithHeaders<T> {
1788    #[must_use]
1789    pub fn header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
1790        self.headers.push(ResponseHeader::new(name, value));
1791        self
1792    }
1793
1794    #[must_use]
1795    pub fn into_parts(self) -> (T, Vec<ResponseHeader>) {
1796        (self.response, self.headers)
1797    }
1798}
1799
1800/// Ergonomic response decoration shared by typed success responses.
1801pub trait ResponseExt: Sized {
1802    #[must_use]
1803    fn header(self, name: impl Into<String>, value: impl Into<String>) -> WithHeaders<Self> {
1804        WithHeaders {
1805            response: self,
1806            headers: vec![ResponseHeader::new(name, value)],
1807        }
1808    }
1809}
1810
1811impl<T> ResponseExt for T {}
1812
1813/// HTTP methods supported by the operation frontend.
1814#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
1815#[serde(rename_all = "UPPERCASE")]
1816pub enum HttpMethod {
1817    Get,
1818    Head,
1819    Post,
1820    Put,
1821    Patch,
1822    Delete,
1823    Options,
1824    Trace,
1825    Connect,
1826}
1827
1828impl HttpMethod {
1829    #[must_use]
1830    pub const fn as_str(self) -> &'static str {
1831        match self {
1832            Self::Get => "GET",
1833            Self::Head => "HEAD",
1834            Self::Post => "POST",
1835            Self::Put => "PUT",
1836            Self::Patch => "PATCH",
1837            Self::Delete => "DELETE",
1838            Self::Options => "OPTIONS",
1839            Self::Trace => "TRACE",
1840            Self::Connect => "CONNECT",
1841        }
1842    }
1843
1844    #[must_use]
1845    pub const fn as_openapi_key(self) -> &'static str {
1846        match self {
1847            Self::Get => "get",
1848            Self::Head => "head",
1849            Self::Post => "post",
1850            Self::Put => "put",
1851            Self::Patch => "patch",
1852            Self::Delete => "delete",
1853            Self::Options => "options",
1854            Self::Trace => "trace",
1855            // CONNECT is not a standard OpenAPI Path Item field.
1856            Self::Connect => "x-blazingly-connect",
1857        }
1858    }
1859}
1860
1861/// The HTTP projection of a protocol-neutral operation contract.
1862#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1863pub struct HttpBinding {
1864    pub method: HttpMethod,
1865    pub path: String,
1866}
1867
1868impl HttpBinding {
1869    #[must_use]
1870    pub fn new(method: HttpMethod, path: impl Into<String>) -> Self {
1871        Self {
1872            method,
1873            path: path.into(),
1874        }
1875    }
1876}
1877
1878/// A protocol-neutral operation paired with its HTTP projection.
1879#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1880pub struct OperationDescriptor {
1881    pub contract: OperationContract,
1882    pub http: HttpBinding,
1883}
1884
1885impl OperationDescriptor {
1886    /// Creates an HTTP projection of a protocol-neutral operation.
1887    ///
1888    /// # Errors
1889    ///
1890    /// Returns [`InvalidOperationId`] when `id` is not a valid stable
1891    /// operation identity.
1892    pub fn new(
1893        method: HttpMethod,
1894        path: impl Into<String>,
1895        id: impl Into<String>,
1896        summary: impl Into<String>,
1897        input: Option<TypeDescriptor>,
1898        responses: Vec<ResponseDescriptor>,
1899    ) -> Result<Self, InvalidOperationId> {
1900        Ok(Self {
1901            contract: OperationContract::new(id, summary, input, responses)?,
1902            http: HttpBinding::new(method, path),
1903        })
1904    }
1905
1906    #[must_use]
1907    pub fn with_mcp_tool(mut self, tool: McpToolDescriptor, policy: AgentPolicy) -> Self {
1908        self.contract = self.contract.with_agent_policy(policy).with_mcp_tool(tool);
1909        self
1910    }
1911
1912    #[must_use]
1913    pub fn mcp_tool(&self) -> Option<&McpToolDescriptor> {
1914        self.contract.mcp.as_ref()
1915    }
1916
1917    #[must_use]
1918    pub fn with_inputs(mut self, inputs: Vec<InputDescriptor>) -> Self {
1919        self.contract = self.contract.with_inputs(inputs);
1920        self
1921    }
1922
1923    #[must_use]
1924    pub fn with_dependencies(mut self, dependencies: Vec<DependencyDescriptor>) -> Self {
1925        self.contract = self.contract.with_dependencies(dependencies);
1926        self
1927    }
1928
1929    #[must_use]
1930    pub fn with_security(mut self, requirements: Vec<SecurityRequirement>) -> Self {
1931        self.contract = self.contract.with_security(requirements);
1932        self
1933    }
1934}
1935
1936/// A validated, deterministic application description.
1937#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1938pub struct AppDefinition {
1939    operations: Vec<OperationDescriptor>,
1940    security_schemes: Vec<SecuritySchemeDescriptor>,
1941}
1942
1943impl AppDefinition {
1944    #[must_use]
1945    pub fn operations(&self) -> &[OperationDescriptor] {
1946        &self.operations
1947    }
1948
1949    #[must_use]
1950    pub fn security_schemes(&self) -> &[SecuritySchemeDescriptor] {
1951        &self.security_schemes
1952    }
1953}
1954
1955/// Builder for an application description.
1956#[derive(Clone, Debug, Default)]
1957pub struct App {
1958    operations: Vec<OperationDescriptor>,
1959    security_schemes: Vec<SecuritySchemeDescriptor>,
1960}
1961
1962impl App {
1963    #[must_use]
1964    pub const fn new() -> Self {
1965        Self {
1966            operations: Vec::new(),
1967            security_schemes: Vec::new(),
1968        }
1969    }
1970
1971    #[must_use]
1972    pub fn route(mut self, operation: OperationDescriptor) -> Self {
1973        self.operations.push(operation);
1974        self
1975    }
1976
1977    #[must_use]
1978    pub fn routes(mut self, operations: impl IntoIterator<Item = OperationDescriptor>) -> Self {
1979        self.operations.extend(operations);
1980        self
1981    }
1982
1983    /// Registers a named security scheme referenced by operation contracts.
1984    #[must_use]
1985    pub fn security_scheme(mut self, scheme: SecuritySchemeDescriptor) -> Self {
1986        self.security_schemes.push(scheme);
1987        self
1988    }
1989
1990    /// Validates and deterministically orders the application graph.
1991    ///
1992    /// # Errors
1993    ///
1994    /// Returns [`BuildError`] when an operation identity or HTTP binding is
1995    /// registered more than once.
1996    pub fn build(mut self) -> Result<AppDefinition, BuildError> {
1997        let mut operation_ids = BTreeSet::new();
1998        let mut http_bindings = BTreeSet::new();
1999        let mut route_shapes = BTreeSet::new();
2000        let mut security_names = BTreeSet::new();
2001
2002        for scheme in &self.security_schemes {
2003            if !security_names.insert(scheme.name.clone()) {
2004                return Err(BuildError::DuplicateSecurityScheme(scheme.name.clone()));
2005            }
2006        }
2007
2008        for operation in &self.operations {
2009            validate_operation_inputs(operation)?;
2010            validate_operation_security(operation, &self.security_schemes)?;
2011            if !operation_ids.insert(operation.contract.id.clone()) {
2012                return Err(BuildError::DuplicateOperationId(
2013                    operation.contract.id.clone(),
2014                ));
2015            }
2016
2017            let binding = (operation.http.method, operation.http.path.clone());
2018            if !http_bindings.insert(binding) {
2019                return Err(BuildError::DuplicateHttpBinding {
2020                    method: operation.http.method,
2021                    path: operation.http.path.clone(),
2022                });
2023            }
2024            if !route_shapes.insert((
2025                operation.http.method,
2026                canonical_route_shape(&operation.http.path),
2027            )) {
2028                return Err(BuildError::AmbiguousHttpBinding {
2029                    method: operation.http.method,
2030                    path: operation.http.path.clone(),
2031                });
2032            }
2033        }
2034
2035        self.operations.sort_by(|left, right| {
2036            left.http
2037                .path
2038                .cmp(&right.http.path)
2039                .then(left.http.method.cmp(&right.http.method))
2040                .then(left.contract.id.cmp(&right.contract.id))
2041        });
2042        self.security_schemes
2043            .sort_by(|left, right| left.name.cmp(&right.name));
2044
2045        Ok(AppDefinition {
2046            operations: self.operations,
2047            security_schemes: self.security_schemes,
2048        })
2049    }
2050}
2051
2052/// An invalid application graph.
2053#[derive(Clone, Debug, Eq, PartialEq)]
2054pub enum BuildError {
2055    DuplicateOperationId(OperationId),
2056    DuplicateHttpBinding {
2057        method: HttpMethod,
2058        path: String,
2059    },
2060    AmbiguousHttpBinding {
2061        method: HttpMethod,
2062        path: String,
2063    },
2064    InvalidPathInputs {
2065        operation: OperationId,
2066    },
2067    DuplicateInputName {
2068        operation: OperationId,
2069        name: String,
2070    },
2071    DuplicateSecurityScheme(String),
2072    DuplicateSecurityRequirement {
2073        operation: OperationId,
2074        scheme: String,
2075    },
2076    UnknownSecurityScheme {
2077        operation: OperationId,
2078        scheme: String,
2079    },
2080    UnknownSecurityScope {
2081        operation: OperationId,
2082        scheme: String,
2083        scope: String,
2084    },
2085}
2086
2087impl fmt::Display for BuildError {
2088    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
2089        match self {
2090            Self::DuplicateOperationId(id) => {
2091                write!(
2092                    formatter,
2093                    "operation id {id:?} is registered more than once"
2094                )
2095            }
2096            Self::DuplicateHttpBinding { method, path } => write!(
2097                formatter,
2098                "{} {path} is registered more than once",
2099                method.as_str()
2100            ),
2101            Self::AmbiguousHttpBinding { method, path } => write!(
2102                formatter,
2103                "{} {path} conflicts with another parameterized route",
2104                method.as_str()
2105            ),
2106            Self::InvalidPathInputs { operation } => write!(
2107                formatter,
2108                "operation {operation} path placeholders do not match its Path<T> inputs"
2109            ),
2110            Self::DuplicateInputName { operation, name } => write!(
2111                formatter,
2112                "operation {operation} exposes input name {name:?} more than once"
2113            ),
2114            Self::DuplicateSecurityScheme(name) => {
2115                write!(
2116                    formatter,
2117                    "security scheme {name:?} is registered more than once"
2118                )
2119            }
2120            Self::DuplicateSecurityRequirement { operation, scheme } => write!(
2121                formatter,
2122                "operation {operation} requires security scheme {scheme:?} more than once"
2123            ),
2124            Self::UnknownSecurityScheme { operation, scheme } => write!(
2125                formatter,
2126                "operation {operation} references unknown security scheme {scheme:?}"
2127            ),
2128            Self::UnknownSecurityScope {
2129                operation,
2130                scheme,
2131                scope,
2132            } => write!(
2133                formatter,
2134                "operation {operation} requires unknown scope {scope:?} from security scheme {scheme:?}"
2135            ),
2136        }
2137    }
2138}
2139
2140impl std::error::Error for BuildError {}
2141
2142fn canonical_route_shape(path: &str) -> String {
2143    path.split('/')
2144        .map(|segment| {
2145            if segment.starts_with('{') && segment.ends_with('}') {
2146                "{}"
2147            } else {
2148                segment
2149            }
2150        })
2151        .collect::<Vec<_>>()
2152        .join("/")
2153}
2154
2155fn validate_operation_inputs(operation: &OperationDescriptor) -> Result<(), BuildError> {
2156    let placeholders = operation
2157        .http
2158        .path
2159        .split('/')
2160        .filter_map(|segment| {
2161            segment
2162                .strip_prefix('{')
2163                .and_then(|segment| segment.strip_suffix('}'))
2164                .filter(|name| !name.is_empty())
2165                .map(str::to_owned)
2166        })
2167        .collect::<BTreeSet<_>>();
2168    let path_inputs = operation
2169        .contract
2170        .inputs
2171        .iter()
2172        .filter(|input| input.source == InputSource::Path)
2173        .flat_map(input_public_names)
2174        .collect::<BTreeSet<_>>();
2175    if placeholders != path_inputs {
2176        return Err(BuildError::InvalidPathInputs {
2177            operation: operation.contract.id.clone(),
2178        });
2179    }
2180
2181    let mut names = BTreeSet::new();
2182    for name in operation
2183        .contract
2184        .inputs
2185        .iter()
2186        .flat_map(input_public_names)
2187    {
2188        if !names.insert(name.clone()) {
2189            return Err(BuildError::DuplicateInputName {
2190                operation: operation.contract.id.clone(),
2191                name,
2192            });
2193        }
2194    }
2195    Ok(())
2196}
2197
2198fn validate_operation_security(
2199    operation: &OperationDescriptor,
2200    schemes: &[SecuritySchemeDescriptor],
2201) -> Result<(), BuildError> {
2202    let mut required_schemes = BTreeSet::new();
2203    for requirement in &operation.contract.security {
2204        if !required_schemes.insert(requirement.scheme.as_str()) {
2205            return Err(BuildError::DuplicateSecurityRequirement {
2206                operation: operation.contract.id.clone(),
2207                scheme: requirement.scheme.clone(),
2208            });
2209        }
2210        let Some(scheme) = schemes
2211            .iter()
2212            .find(|scheme| scheme.name == requirement.scheme)
2213        else {
2214            return Err(BuildError::UnknownSecurityScheme {
2215                operation: operation.contract.id.clone(),
2216                scheme: requirement.scheme.clone(),
2217            });
2218        };
2219        let declared_scopes = match &scheme.kind {
2220            SecuritySchemeKind::OAuth2 { scopes, .. } => Some(scopes),
2221            SecuritySchemeKind::ApiKey { .. }
2222            | SecuritySchemeKind::Http { .. }
2223            | SecuritySchemeKind::OpenIdConnect { .. }
2224            | SecuritySchemeKind::MutualTls => None,
2225        };
2226        for scope in &requirement.scopes {
2227            if declared_scopes.is_none_or(|scopes| !scopes.contains(scope)) {
2228                return Err(BuildError::UnknownSecurityScope {
2229                    operation: operation.contract.id.clone(),
2230                    scheme: requirement.scheme.clone(),
2231                    scope: scope.clone(),
2232                });
2233            }
2234        }
2235    }
2236    Ok(())
2237}
2238
2239fn input_public_names(input: &InputDescriptor) -> Vec<String> {
2240    input.ty.model.as_ref().map_or_else(
2241        || vec![input.name.clone()],
2242        |model| {
2243            model
2244                .fields
2245                .iter()
2246                .map(|field| field.name.clone())
2247                .collect()
2248        },
2249    )
2250}
2251
2252#[macro_export]
2253macro_rules! descriptors {
2254    ($($operation:ident),* $(,)?) => {
2255        ::std::vec![$($operation::descriptor()),*]
2256    };
2257}
2258
2259#[cfg(test)]
2260mod tests {
2261    use super::{
2262        ApiError, ApiSchema, App, BodyStream, BodyStreamError, BuildError, FieldMetadata,
2263        HttpMethod, InputDescriptor, InputSource, MAX_MULTIPART_PARTS, MAX_RESPONSE_HINT,
2264        MIN_RESPONSE_HINT, MultipartError, MultipartStream, OperationDescriptor, PreparedJson,
2265        ResponseDescriptor, SchemaKind, SecurityRequirement, SecuritySchemeDescriptor,
2266        SecuritySchemeKind, StreamingBody, TypeDescriptor, UploadFile, UploadSlots,
2267        merge_field_validation_errors, record_response_size, response_size_hint,
2268    };
2269    use crate::ValidationErrors;
2270    use futures_lite::future::block_on;
2271    use std::pin::Pin;
2272    use std::task::{Context, Poll};
2273
2274    const BOUNDARY: &str = "apibenchcover9f2c41d7e6b3";
2275
2276    fn content_type() -> String {
2277        format!("multipart/form-data; boundary={BOUNDARY}")
2278    }
2279
2280    /// One part to assemble: field name, file name, media type, and data.
2281    type TestPart<'data> = (
2282        &'data str,
2283        Option<&'data str>,
2284        Option<&'data str>,
2285        &'data [u8],
2286    );
2287
2288    /// Assembles one `multipart/form-data` document.
2289    fn multipart_document(parts: &[TestPart<'_>]) -> Vec<u8> {
2290        let mut body = Vec::new();
2291        for (name, file_name, media_type, data) in parts {
2292            body.extend_from_slice(format!("--{BOUNDARY}\r\n").as_bytes());
2293            body.extend_from_slice(
2294                format!("Content-Disposition: form-data; name=\"{name}\"").as_bytes(),
2295            );
2296            if let Some(file_name) = file_name {
2297                body.extend_from_slice(format!("; filename=\"{file_name}\"").as_bytes());
2298            }
2299            body.extend_from_slice(b"\r\n");
2300            if let Some(media_type) = media_type {
2301                body.extend_from_slice(format!("Content-Type: {media_type}\r\n").as_bytes());
2302            }
2303            body.extend_from_slice(b"\r\n");
2304            body.extend_from_slice(data);
2305            body.extend_from_slice(b"\r\n");
2306        }
2307        body.extend_from_slice(format!("--{BOUNDARY}--\r\n").as_bytes());
2308        body
2309    }
2310
2311    /// A body delivered in fixed-size chunks, so a delimiter can be split
2312    /// across as many transport chunks as the test wants.
2313    struct SlicedBody {
2314        bytes: Vec<u8>,
2315        chunk: usize,
2316        position: usize,
2317    }
2318
2319    impl BodyStream for SlicedBody {
2320        fn poll_next(
2321            self: Pin<&mut Self>,
2322            _context: &mut Context<'_>,
2323        ) -> Poll<Option<Result<Vec<u8>, BodyStreamError>>> {
2324            let body = self.get_mut();
2325            if body.position >= body.bytes.len() {
2326                return Poll::Ready(None);
2327            }
2328            let end = body.bytes.len().min(body.position + body.chunk);
2329            let chunk = body.bytes[body.position..end].to_vec();
2330            body.position = end;
2331            Poll::Ready(Some(Ok(chunk)))
2332        }
2333    }
2334
2335    /// A body that delivers a prefix and then fails, the way a stalled or
2336    /// aborted upload does.
2337    struct FailingBody {
2338        head: Option<Vec<u8>>,
2339    }
2340
2341    impl BodyStream for FailingBody {
2342        fn poll_next(
2343            self: Pin<&mut Self>,
2344            _context: &mut Context<'_>,
2345        ) -> Poll<Option<Result<Vec<u8>, BodyStreamError>>> {
2346            Poll::Ready(Some(match self.get_mut().head.take() {
2347                Some(head) => Ok(head),
2348                None => Err(BodyStreamError::new(
2349                    "upload_timeout",
2350                    "request body stalled past the configured deadline",
2351                )),
2352            }))
2353        }
2354    }
2355
2356    fn reader(bytes: Vec<u8>, chunk: usize) -> MultipartStream {
2357        MultipartStream::new(
2358            StreamingBody::new(SlicedBody {
2359                bytes,
2360                chunk,
2361                position: 0,
2362            }),
2363            &content_type(),
2364        )
2365        .expect("the declared content type carries a boundary")
2366    }
2367
2368    /// Reads every part, returning its name and the bytes it carried.
2369    async fn read_all(
2370        stream: &mut MultipartStream,
2371    ) -> Result<Vec<(String, Vec<u8>)>, MultipartError> {
2372        let mut parts = Vec::new();
2373        while let Some(mut field) = stream.next_field().await? {
2374            let name = field.name().to_owned();
2375            let mut bytes = Vec::new();
2376            while let Some(chunk) = field.next_chunk().await? {
2377                bytes.extend_from_slice(chunk);
2378            }
2379            parts.push((name, bytes));
2380        }
2381        Ok(parts)
2382    }
2383
2384    #[test]
2385    fn a_streamed_document_yields_every_part_whatever_the_chunk_size() {
2386        let payload = vec![7_u8; 40_000];
2387        let document = multipart_document(&[
2388            ("title", None, None, b"A cover"),
2389            (
2390                "file",
2391                Some("cover.jpg"),
2392                Some("image/jpeg"),
2393                payload.as_slice(),
2394            ),
2395            ("note", None, None, b""),
2396        ]);
2397
2398        // One byte at a time splits the delimiter, the header block, and the
2399        // CRLF that precedes a boundary across as many chunks as possible.
2400        for chunk in [1, 2, 3, 7, 64, 8192, usize::MAX] {
2401            let mut stream = reader(document.clone(), chunk);
2402            let parts = block_on(read_all(&mut stream)).expect("the document parses");
2403            assert_eq!(parts.len(), 3, "chunk size {chunk}");
2404            assert_eq!(parts[0], ("title".to_owned(), b"A cover".to_vec()));
2405            assert_eq!(parts[1].0, "file");
2406            assert_eq!(parts[1].1, payload);
2407            assert_eq!(parts[2], ("note".to_owned(), Vec::new()));
2408        }
2409    }
2410
2411    #[test]
2412    fn part_metadata_survives_the_streaming_reader() {
2413        let document =
2414            multipart_document(&[("file", Some("cover.jpg"), Some("image/jpeg"), b"body")]);
2415        let mut stream = reader(document, 5);
2416        block_on(async {
2417            let field = stream
2418                .next_field()
2419                .await
2420                .expect("the document parses")
2421                .expect("one part");
2422            assert_eq!(field.name(), "file");
2423            assert_eq!(field.file_name(), Some("cover.jpg"));
2424            assert_eq!(field.content_type(), Some("image/jpeg"));
2425        });
2426    }
2427
2428    #[test]
2429    fn part_data_that_looks_like_a_boundary_is_still_data() {
2430        let mut data = Vec::new();
2431        data.extend_from_slice(b"\r\n--not-the-boundary\r\n");
2432        data.extend_from_slice(format!("\r\n--{BOUNDARY}x\r\n").as_bytes());
2433        data.extend_from_slice(format!("\r\n--{BOUNDARY}").as_bytes());
2434        data.extend_from_slice(b"tail\r\n");
2435        data.extend_from_slice(b"\r\n-\r\n--\r");
2436        let document = multipart_document(&[("file", None, None, &data)]);
2437
2438        for chunk in [1, 4, 17, 8192] {
2439            let mut stream = reader(document.clone(), chunk);
2440            let parts = block_on(read_all(&mut stream)).expect("the document parses");
2441            assert_eq!(parts.len(), 1, "chunk size {chunk}");
2442            assert_eq!(parts[0].1, data, "chunk size {chunk}");
2443        }
2444    }
2445
2446    #[test]
2447    fn a_large_upload_never_becomes_resident() {
2448        // Five mebibytes, the size the upload benchmark sends, delivered in the
2449        // eight-kibibyte chunks the native adapter uses.
2450        let payload = vec![9_u8; 5 * 1024 * 1024];
2451        let document =
2452            multipart_document(&[("file", Some("cover.jpg"), Some("image/jpeg"), &payload)]);
2453        let mut stream = reader(document, 8192);
2454
2455        let (bytes, peak) = block_on(async {
2456            let mut bytes = 0_usize;
2457            let mut peak = 0_usize;
2458            let mut field = stream
2459                .next_field()
2460                .await
2461                .expect("the document parses")
2462                .expect("one part");
2463            while let Some(chunk) = field.next_chunk().await.expect("a chunk") {
2464                bytes += chunk.len();
2465                peak = peak.max(field.stream.buffer.capacity());
2466            }
2467            (bytes, peak)
2468        });
2469
2470        assert_eq!(bytes, payload.len());
2471        // One transport chunk plus a delimiter of look-ahead, not the upload.
2472        assert!(
2473            peak < 64 * 1024,
2474            "a five-megabyte upload held {peak} buffered bytes"
2475        );
2476    }
2477
2478    #[test]
2479    fn a_skipped_part_does_not_disturb_the_next_one() {
2480        let document = multipart_document(&[
2481            ("file", None, None, &[3_u8; 5000]),
2482            ("note", None, None, b"kept"),
2483        ]);
2484        let mut stream = reader(document, 128);
2485        block_on(async {
2486            let first = stream
2487                .next_field()
2488                .await
2489                .expect("the document parses")
2490                .expect("a first part");
2491            assert_eq!(first.name(), "file");
2492            drop(first);
2493            let mut second = stream
2494                .next_field()
2495                .await
2496                .expect("the rest of the document parses")
2497                .expect("a second part");
2498            assert_eq!(second.name(), "note");
2499            let chunk = second
2500                .next_chunk()
2501                .await
2502                .expect("a chunk")
2503                .expect("the part has data");
2504            assert_eq!(chunk, b"kept");
2505        });
2506    }
2507
2508    #[test]
2509    fn a_document_with_no_parts_reads_as_empty() {
2510        let mut stream = reader(format!("--{BOUNDARY}--").into_bytes(), 3);
2511        let parts = block_on(read_all(&mut stream)).expect("an empty document parses");
2512        assert!(parts.is_empty());
2513    }
2514
2515    #[test]
2516    fn a_body_that_ends_before_its_closing_boundary_is_a_failure_not_a_short_read() {
2517        let mut truncated = multipart_document(&[("file", None, None, &[1_u8; 4096])]);
2518        truncated.truncate(2048);
2519        let mut stream = reader(truncated, 512);
2520        let error = block_on(read_all(&mut stream)).expect_err("a truncated body cannot succeed");
2521        assert_eq!(
2522            error,
2523            MultipartError::Malformed("multipart part has no closing boundary")
2524        );
2525    }
2526
2527    #[test]
2528    fn a_producer_failure_mid_body_is_reported_not_swallowed() {
2529        let mut document = multipart_document(&[("file", None, None, &[1_u8; 4096])]);
2530        document.truncate(1024);
2531        let mut stream = MultipartStream::new(
2532            StreamingBody::new(FailingBody {
2533                head: Some(document),
2534            }),
2535            &content_type(),
2536        )
2537        .expect("the declared content type carries a boundary");
2538
2539        let error = block_on(read_all(&mut stream)).expect_err("a failed producer cannot succeed");
2540        let MultipartError::Transport(transport) = &error else {
2541            panic!("expected a transport failure, got {error:?}");
2542        };
2543        assert_eq!(transport.code, "upload_timeout");
2544        assert_eq!(error.status(), 400);
2545        assert_eq!(error.code(), "upload_stream_failed");
2546    }
2547
2548    #[test]
2549    fn a_failed_document_cannot_be_resumed() {
2550        let mut stream = reader(b"not a multipart body at all".to_vec(), 4);
2551        block_on(async {
2552            let first = stream
2553                .next_field()
2554                .await
2555                .expect_err("the body is malformed");
2556            assert_eq!(
2557                first,
2558                MultipartError::Malformed(
2559                    "multipart body does not start with its declared boundary"
2560                )
2561            );
2562            let second = stream
2563                .next_field()
2564                .await
2565                .expect_err("the reader stays failed");
2566            assert_eq!(
2567                second,
2568                MultipartError::Malformed("multipart body already failed to parse")
2569            );
2570        });
2571    }
2572
2573    #[test]
2574    fn a_malformed_document_projects_the_buffered_extractors_failure() {
2575        let failure = MultipartError::Malformed("multipart boundary is malformed")
2576            .into_failure()
2577            .expect("the failure projects");
2578        assert_eq!(failure.status, 422);
2579        assert_eq!(failure.code, "invalid_multipart");
2580        assert_eq!(
2581            failure.message,
2582            "request body is not valid multipart form data"
2583        );
2584        let details: blazingly_json::Value =
2585            blazingly_json::from_slice(&failure.details.expect("details")).expect("valid JSON");
2586        assert_eq!(
2587            details,
2588            blazingly_json::json!({
2589                "source": "multipart",
2590                "reason": "multipart boundary is malformed"
2591            })
2592        );
2593    }
2594
2595    #[test]
2596    fn a_content_type_without_a_usable_boundary_is_rejected() {
2597        for content_type in [
2598            "application/json",
2599            "multipart/form-data",
2600            "multipart/form-data; boundary=",
2601            "multipart/form-data; boundary=\"with space\"",
2602        ] {
2603            let error = MultipartStream::new(StreamingBody::once(Vec::new()), content_type)
2604                .err()
2605                .unwrap_or_else(|| panic!("{content_type} should not carry a boundary"));
2606            assert_eq!(
2607                error,
2608                MultipartError::Malformed("multipart boundary is missing or invalid")
2609            );
2610        }
2611    }
2612
2613    #[test]
2614    fn a_document_with_too_many_parts_is_rejected() {
2615        let empty = Vec::new();
2616        let parts = (0..=MAX_MULTIPART_PARTS)
2617            .map(|_| ("field", None, None, empty.as_slice()))
2618            .collect::<Vec<_>>();
2619        let mut stream = reader(multipart_document(&parts), 512);
2620        let error = block_on(read_all(&mut stream)).expect_err("the part count is bounded");
2621        assert_eq!(
2622            error,
2623            MultipartError::Malformed("multipart body contains too many parts")
2624        );
2625    }
2626
2627    #[test]
2628    fn an_oversized_part_header_block_is_rejected() {
2629        let mut document = format!("--{BOUNDARY}\r\n").into_bytes();
2630        document.extend_from_slice(b"Content-Disposition: form-data; name=\"file\"\r\n");
2631        document.extend_from_slice(b"X-Padding: ");
2632        document.extend_from_slice(&vec![b'p'; 32 * 1024]);
2633        document.extend_from_slice(b"\r\n\r\ndata\r\n");
2634        document.extend_from_slice(format!("--{BOUNDARY}--\r\n").as_bytes());
2635
2636        let mut stream = reader(document, 4096);
2637        let error = block_on(read_all(&mut stream)).expect_err("the header block is bounded");
2638        assert_eq!(
2639            error,
2640            MultipartError::Malformed("multipart part headers exceed the configured limit")
2641        );
2642    }
2643
2644    #[test]
2645    fn a_field_can_still_be_buffered_deliberately_with_a_limit() {
2646        let document = multipart_document(&[
2647            ("title", None, None, "A cover".as_bytes()),
2648            ("file", Some("c.png"), Some("image/png"), &[4_u8; 300]),
2649        ]);
2650        let mut stream = reader(document, 37);
2651        block_on(async {
2652            let title = stream
2653                .next_field()
2654                .await
2655                .expect("the document parses")
2656                .expect("a first part");
2657            assert_eq!(title.text(64).await.expect("the text fits"), "A cover");
2658
2659            let file = stream
2660                .next_field()
2661                .await
2662                .expect("the document parses")
2663                .expect("a second part");
2664            let upload = file.into_upload(1024).await.expect("the upload fits");
2665            assert_eq!(
2666                upload,
2667                UploadFile::new("file", vec![4_u8; 300])
2668                    .with_file_name("c.png")
2669                    .with_content_type("image/png")
2670            );
2671        });
2672    }
2673
2674    #[test]
2675    fn deliberate_buffering_still_honours_the_limit_it_was_given() {
2676        let document = multipart_document(&[("file", None, None, &[4_u8; 300])]);
2677        let mut stream = reader(document, 64);
2678        let error = block_on(async {
2679            let field = stream
2680                .next_field()
2681                .await
2682                .expect("the document parses")
2683                .expect("one part");
2684            field.collect(128).await.expect_err("the part is too large")
2685        });
2686        assert_eq!(error, MultipartError::TooLarge { limit: 128 });
2687        assert_eq!(error.status(), 413);
2688    }
2689
2690    struct DocumentedPage;
2691
2692    impl ApiSchema for DocumentedPage {
2693        fn type_descriptor() -> TypeDescriptor {
2694            TypeDescriptor::scalar("DocumentedPage", SchemaKind::Object)
2695        }
2696    }
2697
2698    struct HintProbe;
2699
2700    #[test]
2701    fn prepared_json_encodes_a_borrowed_view() {
2702        let owned = [String::from("alpha"), String::from("beta")];
2703        let borrowed: Vec<&str> = owned.iter().map(String::as_str).collect();
2704        let body = PreparedJson::<DocumentedPage>::encode(&borrowed).expect("the view encodes");
2705        assert_eq!(body.as_bytes(), br#"["alpha","beta"]"#);
2706        assert_eq!(body.len(), 16);
2707        assert!(!body.is_empty());
2708    }
2709
2710    #[test]
2711    fn prepared_json_reports_the_declared_schema_not_its_bytes() {
2712        assert_eq!(
2713            PreparedJson::<DocumentedPage>::type_descriptor(),
2714            DocumentedPage::type_descriptor()
2715        );
2716    }
2717
2718    #[test]
2719    fn prepared_json_carries_adopted_bytes_verbatim() {
2720        let body =
2721            PreparedJson::<DocumentedPage>::from_bytes(b"{\"already\":\"encoded\"}".to_vec());
2722        assert_eq!(body.into_bytes(), b"{\"already\":\"encoded\"}".to_vec());
2723    }
2724
2725    #[test]
2726    fn an_unseen_response_shape_reserves_the_floor() {
2727        assert_eq!(response_size_hint::<HintProbe>(), MIN_RESPONSE_HINT);
2728    }
2729
2730    #[test]
2731    fn a_recorded_response_shape_reserves_headroom() {
2732        record_response_size::<(u8, HintProbe)>(8192);
2733        let hint = response_size_hint::<(u8, HintProbe)>();
2734        assert!(hint > 8192, "the hint should leave room to grow: {hint}");
2735        assert!(hint <= MAX_RESPONSE_HINT);
2736    }
2737
2738    #[test]
2739    fn an_outsized_response_cannot_pin_the_hint_above_the_ceiling() {
2740        record_response_size::<(u16, HintProbe)>(usize::MAX);
2741        assert_eq!(response_size_hint::<(u16, HintProbe)>(), MAX_RESPONSE_HINT);
2742    }
2743
2744    #[test]
2745    fn a_parked_upload_leaves_only_a_token_in_the_document() {
2746        let slots = UploadSlots::acquire();
2747        let token = slots.park(
2748            UploadFile::new("cover", vec![7; 1 << 20])
2749                .with_file_name("cover.png")
2750                .with_content_type("image/png"),
2751        );
2752
2753        let encoded = blazingly_json::to_string(&token).expect("the token encodes");
2754        assert!(
2755            encoded.len() < 64,
2756            "a megabyte of upload left {} bytes in the document: {encoded}",
2757            encoded.len()
2758        );
2759
2760        let decoded: UploadFile = blazingly_json::from_value(token).expect("the token resolves");
2761        assert_eq!(decoded.field_name, "cover");
2762        assert_eq!(decoded.file_name.as_deref(), Some("cover.png"));
2763        assert_eq!(decoded.content_type.as_deref(), Some("image/png"));
2764        assert_eq!(decoded.bytes.len(), 1 << 20);
2765        assert!(decoded.bytes.iter().all(|byte| *byte == 7));
2766    }
2767
2768    #[test]
2769    fn an_upload_slot_does_not_outlive_its_extraction() {
2770        let token = {
2771            let slots = UploadSlots::acquire();
2772            slots.park(UploadFile::new("gone", vec![1, 2, 3]))
2773        };
2774        let error = blazingly_json::from_value::<UploadFile>(token)
2775            .expect_err("a released slot cannot be resolved");
2776        assert!(error.to_string().contains("no longer available"), "{error}");
2777    }
2778
2779    #[test]
2780    fn an_upload_slot_can_only_be_taken_once() {
2781        let slots = UploadSlots::acquire();
2782        let token = slots.park(UploadFile::new("once", vec![9]));
2783        let first: UploadFile =
2784            blazingly_json::from_value(token.clone()).expect("the first take resolves");
2785        assert_eq!(first.bytes, vec![9]);
2786        assert!(blazingly_json::from_value::<UploadFile>(token).is_err());
2787    }
2788
2789    #[test]
2790    fn two_extractions_on_one_thread_cannot_see_each_others_slots() {
2791        let outer = UploadSlots::acquire();
2792        let outer_token = outer.park(UploadFile::new("outer", vec![1]));
2793        let inner_token = {
2794            let inner = UploadSlots::acquire();
2795            inner.park(UploadFile::new("inner", vec![2]))
2796        };
2797
2798        assert!(blazingly_json::from_value::<UploadFile>(inner_token).is_err());
2799        let resolved: UploadFile =
2800            blazingly_json::from_value(outer_token).expect("the outer slot survives");
2801        assert_eq!(resolved.field_name, "outer");
2802    }
2803
2804    #[test]
2805    fn the_object_form_an_mcp_client_sends_still_decodes() {
2806        let value = blazingly_json::json!({
2807            "field_name": "avatar",
2808            "file_name": "a.png",
2809            "content_type": "image/png",
2810            "bytes": [1, 2, 3]
2811        });
2812        let upload: UploadFile =
2813            blazingly_json::from_value(value).expect("the object form decodes");
2814        assert_eq!(
2815            upload,
2816            UploadFile::new("avatar", vec![1, 2, 3])
2817                .with_file_name("a.png")
2818                .with_content_type("image/png")
2819        );
2820    }
2821
2822    #[test]
2823    fn an_upload_survives_its_own_serialized_form() {
2824        let upload = UploadFile::new("report", vec![4, 5, 6]).with_file_name("r.bin");
2825        let encoded = blazingly_json::to_value(&upload).expect("the upload encodes");
2826        let decoded: UploadFile = blazingly_json::from_value(encoded).expect("the upload decodes");
2827        assert_eq!(decoded, upload);
2828    }
2829
2830    #[test]
2831    fn the_object_form_defaults_metadata_and_still_demands_the_rest() {
2832        let minimal: UploadFile =
2833            blazingly_json::from_value(blazingly_json::json!({"field_name": "a", "bytes": []}))
2834                .expect("optional metadata may be absent");
2835        assert!(minimal.file_name.is_none());
2836        assert!(minimal.content_type.is_none());
2837
2838        let error =
2839            blazingly_json::from_value::<UploadFile>(blazingly_json::json!({"field_name": "a"}))
2840                .expect_err("bytes are required");
2841        assert!(error.to_string().contains("bytes"), "{error}");
2842    }
2843
2844    fn violations(field: &str, nested: &ValidationErrors) -> Vec<String> {
2845        let mut merged = ValidationErrors::new();
2846        merge_field_validation_errors(&mut merged, field, nested);
2847        merged
2848            .violations()
2849            .iter()
2850            .map(|violation| violation.field.clone())
2851            .collect()
2852    }
2853
2854    #[test]
2855    fn a_field_validator_that_names_its_own_field_is_not_doubled() {
2856        let mut reported = ValidationErrors::new();
2857        reported.push("published_at", "too_far_ahead", "too far ahead");
2858        assert_eq!(violations("published_at", &reported), ["published_at"]);
2859    }
2860
2861    #[test]
2862    fn a_field_validator_may_still_report_a_path_inside_the_value() {
2863        let mut reported = ValidationErrors::new();
2864        reported.push("", "invalid", "invalid");
2865        reported.push("window.end", "invalid", "invalid");
2866        reported.push("slots[0]", "invalid", "invalid");
2867        reported.push("end", "invalid", "invalid");
2868        assert_eq!(
2869            violations("window", &reported),
2870            ["window", "window.end", "window.slots[0]", "window.end"]
2871        );
2872    }
2873
2874    #[test]
2875    fn an_unnamed_field_leaves_the_reported_path_alone() {
2876        let mut reported = ValidationErrors::new();
2877        reported.push("street", "min_length", "too short");
2878        assert_eq!(violations("", &reported), ["street"]);
2879    }
2880
2881    #[test]
2882    fn field_metadata_round_trips_through_its_encoding() {
2883        for metadata in [
2884            FieldMetadata::Default(blazingly_json::json!(20)),
2885            FieldMetadata::Default(blazingly_json::json!("draft")),
2886            FieldMetadata::Default(blazingly_json::json!(true)),
2887            FieldMetadata::Nullable,
2888            FieldMetadata::Enumeration(vec!["uk".to_owned(), "ru".to_owned()]),
2889        ] {
2890            let encoded = metadata.to_string();
2891            assert_eq!(
2892                FieldMetadata::parse(&encoded),
2893                Some(metadata.clone()),
2894                "{encoded} did not round trip"
2895            );
2896        }
2897
2898        assert_eq!(FieldMetadata::parse("min_items=2"), None);
2899        assert_eq!(FieldMetadata::parse("validate_code"), None);
2900        assert_eq!(FieldMetadata::parse("nullable=false"), None);
2901    }
2902
2903    #[test]
2904    fn field_metadata_projects_json_schema_keywords() {
2905        let mut schema = blazingly_json::json!({ "type": "integer" });
2906        FieldMetadata::Default(blazingly_json::json!(20)).apply_json_schema(&mut schema);
2907        FieldMetadata::Nullable.apply_json_schema(&mut schema);
2908        FieldMetadata::Enumeration(vec!["uk".to_owned()]).apply_json_schema(&mut schema);
2909
2910        assert_eq!(schema["default"], blazingly_json::json!(20));
2911        assert_eq!(schema["nullable"], blazingly_json::json!(true));
2912        assert_eq!(schema["enum"], blazingly_json::json!(["uk"]));
2913    }
2914
2915    fn operation(id: &str, method: HttpMethod, path: &str) -> OperationDescriptor {
2916        OperationDescriptor::new(
2917            method,
2918            path,
2919            id,
2920            id,
2921            None,
2922            vec![ResponseDescriptor::success(
2923                200,
2924                Some(TypeDescriptor::new("Output")),
2925            )],
2926        )
2927        .expect("test operation id should be valid")
2928    }
2929
2930    #[test]
2931    fn app_rejects_duplicate_operation_ids() {
2932        let result = App::new()
2933            .route(operation("users.read", HttpMethod::Get, "/users/1"))
2934            .route(operation("users.read", HttpMethod::Get, "/users/2"))
2935            .build();
2936
2937        assert!(matches!(result, Err(BuildError::DuplicateOperationId(_))));
2938    }
2939
2940    #[test]
2941    fn app_rejects_duplicate_http_bindings() {
2942        let result = App::new()
2943            .route(operation("users.read", HttpMethod::Get, "/users"))
2944            .route(operation("users.list", HttpMethod::Get, "/users"))
2945            .build();
2946
2947        assert!(matches!(
2948            result,
2949            Err(BuildError::DuplicateHttpBinding { .. })
2950        ));
2951    }
2952
2953    #[test]
2954    fn app_rejects_parameter_routes_with_the_same_shape() {
2955        let result = App::new()
2956            .route(
2957                operation("users.by_id", HttpMethod::Get, "/users/{user_id}").with_inputs(vec![
2958                    InputDescriptor::new(
2959                        "user_id",
2960                        InputSource::Path,
2961                        true,
2962                        TypeDescriptor::new("u64"),
2963                    ),
2964                ]),
2965            )
2966            .route(
2967                operation("users.by_name", HttpMethod::Get, "/users/{user_name}").with_inputs(
2968                    vec![InputDescriptor::new(
2969                        "user_name",
2970                        InputSource::Path,
2971                        true,
2972                        TypeDescriptor::new("String"),
2973                    )],
2974                ),
2975            )
2976            .build();
2977
2978        assert!(matches!(
2979            result,
2980            Err(BuildError::AmbiguousHttpBinding { .. })
2981        ));
2982    }
2983
2984    #[test]
2985    fn app_order_is_deterministic() {
2986        let app = App::new()
2987            .route(operation("users.create", HttpMethod::Post, "/users"))
2988            .route(operation("health.read", HttpMethod::Get, "/health"))
2989            .build()
2990            .expect("application should be valid");
2991
2992        let ids: Vec<_> = app
2993            .operations()
2994            .iter()
2995            .map(|operation| operation.contract.id.as_str())
2996            .collect();
2997        assert_eq!(ids, ["health.read", "users.create"]);
2998    }
2999
3000    #[test]
3001    fn app_validates_and_orders_operation_security() {
3002        let secured = operation("users.write", HttpMethod::Put, "/users").with_security(vec![
3003            SecurityRequirement::new("oauth").with_scopes(vec!["users:write".to_owned()]),
3004        ]);
3005        let app = App::new()
3006            .route(secured)
3007            .security_scheme(SecuritySchemeDescriptor::new(
3008                "oauth",
3009                SecuritySchemeKind::OAuth2 {
3010                    authorization_url: Some("https://auth.example/authorize".to_owned()),
3011                    token_url: Some("https://auth.example/token".to_owned()),
3012                    scopes: vec!["users:read".to_owned(), "users:write".to_owned()],
3013                },
3014            ))
3015            .build()
3016            .expect("registered security requirements should compile");
3017
3018        assert_eq!(app.security_schemes()[0].name, "oauth");
3019        assert_eq!(
3020            app.operations()[0].contract.security[0].scopes,
3021            ["users:write"]
3022        );
3023    }
3024
3025    #[test]
3026    fn app_rejects_unknown_security_schemes_and_scopes() {
3027        let unknown_scheme = App::new()
3028            .route(
3029                operation("users.read", HttpMethod::Get, "/users")
3030                    .with_security(vec![SecurityRequirement::new("missing")]),
3031            )
3032            .build();
3033        assert!(matches!(
3034            unknown_scheme,
3035            Err(BuildError::UnknownSecurityScheme { .. })
3036        ));
3037
3038        let unknown_scope = App::new()
3039            .route(
3040                operation("users.read", HttpMethod::Get, "/users").with_security(vec![
3041                    SecurityRequirement::new("oauth").with_scopes(vec!["users:write".to_owned()]),
3042                ]),
3043            )
3044            .security_scheme(SecuritySchemeDescriptor::new(
3045                "oauth",
3046                SecuritySchemeKind::OAuth2 {
3047                    authorization_url: None,
3048                    token_url: Some("https://auth.example/token".to_owned()),
3049                    scopes: vec!["users:read".to_owned()],
3050                },
3051            ))
3052            .build();
3053        assert!(matches!(
3054            unknown_scope,
3055            Err(BuildError::UnknownSecurityScope { .. })
3056        ));
3057    }
3058}