Skip to main content

blazingly_core/
lib.rs

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