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