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#[doc(hidden)]
24pub mod schema;
25
26const RESPONSE_HINT_SLOTS: usize = 32;
32const MIN_RESPONSE_HINT: usize = 128;
34const MAX_RESPONSE_HINT: usize = 1 << 20;
37
38thread_local! {
39 static RESPONSE_SIZE_HINTS: std::cell::RefCell<[(usize, usize); RESPONSE_HINT_SLOTS]> =
47 const { std::cell::RefCell::new([(0, 0); RESPONSE_HINT_SLOTS]) };
48}
49
50fn response_shape_key<T: ?Sized>() -> usize {
56 core::any::type_name::<T>().as_ptr() as usize
57}
58
59fn response_hint_slot(key: usize) -> usize {
60 (key >> 4) % RESPONSE_HINT_SLOTS
61}
62
63#[must_use]
65pub fn response_size_hint<T: ?Sized>() -> usize {
66 let key = response_shape_key::<T>();
67 RESPONSE_SIZE_HINTS.with_borrow(|hints| {
68 let (stored_key, hint) = hints[response_hint_slot(key)];
69 if stored_key == key {
70 hint.max(MIN_RESPONSE_HINT)
71 } else {
72 MIN_RESPONSE_HINT
73 }
74 })
75}
76
77pub fn record_response_size<T: ?Sized>(size: usize) {
82 let key = response_shape_key::<T>();
83 let hint = size
84 .saturating_add(size / 8)
85 .saturating_add(32)
86 .clamp(MIN_RESPONSE_HINT, MAX_RESPONSE_HINT);
87 RESPONSE_SIZE_HINTS.with_borrow_mut(|hints| {
88 hints[response_hint_slot(key)] = (key, hint);
89 });
90}
91
92#[derive(Clone, Debug, Eq, PartialEq)]
94pub struct Json<T>(pub T);
95
96#[derive(Clone, Debug, Eq, PartialEq)]
98pub struct Path<T>(pub T);
99
100#[derive(Clone, Debug, Eq, PartialEq)]
102pub struct Query<T>(pub T);
103
104#[derive(Clone, Debug, Eq, PartialEq)]
106pub struct Header<T>(pub T);
107
108#[derive(Clone, Debug, Eq, PartialEq)]
110pub struct Cookie<T>(pub T);
111
112#[derive(Clone, Debug, Eq, PartialEq)]
114pub struct Form<T>(pub T);
115
116#[derive(Clone, Debug, Eq, PartialEq)]
118pub struct Multipart<T>(pub T);
119
120#[derive(Clone, Debug, Eq, PartialEq)]
122pub struct File<T>(pub T);
123
124#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
142pub struct UploadFile {
143 pub field_name: String,
144 pub file_name: Option<String>,
145 pub content_type: Option<String>,
146 pub bytes: Vec<u8>,
147}
148
149impl UploadFile {
150 #[must_use]
151 pub fn new(field_name: impl Into<String>, bytes: Vec<u8>) -> Self {
152 Self {
153 field_name: field_name.into(),
154 file_name: None,
155 content_type: None,
156 bytes,
157 }
158 }
159
160 #[must_use]
161 pub fn with_file_name(mut self, file_name: impl Into<String>) -> Self {
162 self.file_name = Some(file_name.into());
163 self
164 }
165
166 #[must_use]
167 pub fn with_content_type(mut self, content_type: impl Into<String>) -> Self {
168 self.content_type = Some(content_type.into());
169 self
170 }
171}
172
173impl ApiSchema for UploadFile {
174 fn type_descriptor() -> TypeDescriptor {
175 TypeDescriptor::scalar("UploadFile", SchemaKind::Binary)
176 }
177}
178
179const UPLOAD_SLOT_KEY: &str = "$blazingly::upload";
189
190thread_local! {
191 static UPLOAD_SLOTS: std::cell::RefCell<Vec<Option<UploadFile>>> =
206 const { std::cell::RefCell::new(Vec::new()) };
207}
208
209#[doc(hidden)]
219#[derive(Debug)]
220pub struct UploadSlots {
221 base: usize,
222 thread_bound: PhantomData<*const ()>,
225}
226
227impl UploadSlots {
228 #[must_use]
230 pub fn acquire() -> Self {
231 Self {
232 base: UPLOAD_SLOTS.with_borrow(Vec::len),
233 thread_bound: PhantomData,
234 }
235 }
236
237 #[must_use]
243 pub fn park(&self, upload: UploadFile) -> blazingly_json::Value {
244 let index = UPLOAD_SLOTS.with_borrow_mut(|slots| {
245 slots.push(Some(upload));
246 slots.len() - 1
247 });
248 let mut token = blazingly_json::Map::new();
249 token.insert(
250 UPLOAD_SLOT_KEY.to_owned(),
251 blazingly_json::Value::from(index),
252 );
253 blazingly_json::Value::Object(token)
254 }
255}
256
257impl Drop for UploadSlots {
258 fn drop(&mut self) {
259 UPLOAD_SLOTS.with_borrow_mut(|slots| slots.truncate(self.base));
260 }
261}
262
263fn take_parked_upload(index: usize) -> Option<UploadFile> {
269 UPLOAD_SLOTS.with_borrow_mut(|slots| slots.get_mut(index).and_then(Option::take))
270}
271
272enum UploadField {
274 Slot,
275 FieldName,
276 FileName,
277 ContentType,
278 Bytes,
279 Unknown,
280}
281
282impl<'de> Deserialize<'de> for UploadField {
283 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
284 struct FieldVisitor;
285
286 impl serde::de::Visitor<'_> for FieldVisitor {
287 type Value = UploadField;
288
289 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
290 formatter.write_str("an uploaded file field name")
291 }
292
293 fn visit_str<E: serde::de::Error>(self, value: &str) -> Result<UploadField, E> {
294 Ok(match value {
295 UPLOAD_SLOT_KEY => UploadField::Slot,
296 "field_name" => UploadField::FieldName,
297 "file_name" => UploadField::FileName,
298 "content_type" => UploadField::ContentType,
299 "bytes" => UploadField::Bytes,
300 _ => UploadField::Unknown,
301 })
302 }
303 }
304
305 deserializer.deserialize_identifier(FieldVisitor)
306 }
307}
308
309struct UploadFileVisitor;
310
311impl<'de> serde::de::Visitor<'de> for UploadFileVisitor {
312 type Value = UploadFile;
313
314 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
315 formatter.write_str("an uploaded file")
316 }
317
318 fn visit_map<A: serde::de::MapAccess<'de>>(self, mut map: A) -> Result<UploadFile, A::Error> {
319 use serde::de::Error as _;
320
321 let mut field_name: Option<String> = None;
322 let mut file_name: Option<Option<String>> = None;
323 let mut content_type: Option<Option<String>> = None;
324 let mut bytes: Option<Vec<u8>> = None;
325
326 while let Some(key) = map.next_key::<UploadField>()? {
327 match key {
328 UploadField::Slot => {
329 let index = map.next_value::<usize>()?;
330 let upload = take_parked_upload(index).ok_or_else(|| {
331 A::Error::custom("uploaded file bytes are no longer available")
332 })?;
333 while map
335 .next_entry::<serde::de::IgnoredAny, serde::de::IgnoredAny>()?
336 .is_some()
337 {}
338 return Ok(upload);
339 }
340 UploadField::FieldName => {
341 if field_name.is_some() {
342 return Err(A::Error::duplicate_field("field_name"));
343 }
344 field_name = Some(map.next_value()?);
345 }
346 UploadField::FileName => {
347 if file_name.is_some() {
348 return Err(A::Error::duplicate_field("file_name"));
349 }
350 file_name = Some(map.next_value()?);
351 }
352 UploadField::ContentType => {
353 if content_type.is_some() {
354 return Err(A::Error::duplicate_field("content_type"));
355 }
356 content_type = Some(map.next_value()?);
357 }
358 UploadField::Bytes => {
359 if bytes.is_some() {
360 return Err(A::Error::duplicate_field("bytes"));
361 }
362 bytes = Some(map.next_value()?);
363 }
364 UploadField::Unknown => {
365 map.next_value::<serde::de::IgnoredAny>()?;
366 }
367 }
368 }
369
370 Ok(UploadFile {
371 field_name: field_name.ok_or_else(|| A::Error::missing_field("field_name"))?,
372 file_name: file_name.unwrap_or_default(),
373 content_type: content_type.unwrap_or_default(),
374 bytes: bytes.ok_or_else(|| A::Error::missing_field("bytes"))?,
375 })
376 }
377}
378
379impl<'de> Deserialize<'de> for UploadFile {
380 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
381 const FIELDS: &[&str] = &["field_name", "file_name", "content_type", "bytes"];
382 deserializer.deserialize_struct("UploadFile", FIELDS, UploadFileVisitor)
383 }
384}
385
386pub struct PreparedJson<T> {
437 body: Vec<u8>,
438 schema: PhantomData<fn() -> T>,
439}
440
441impl<T> PreparedJson<T> {
442 #[must_use]
446 pub const fn from_bytes(body: Vec<u8>) -> Self {
447 Self {
448 body,
449 schema: PhantomData,
450 }
451 }
452
453 #[must_use]
454 pub fn as_bytes(&self) -> &[u8] {
455 &self.body
456 }
457
458 #[must_use]
459 pub fn into_bytes(self) -> Vec<u8> {
460 self.body
461 }
462
463 #[must_use]
464 pub const fn len(&self) -> usize {
465 self.body.len()
466 }
467
468 #[must_use]
469 pub const fn is_empty(&self) -> bool {
470 self.body.is_empty()
471 }
472
473 pub fn encode<V>(value: &V) -> Result<Self, blazingly_json::Error>
483 where
484 V: Serialize + ?Sized,
485 {
486 let mut body = Vec::with_capacity(response_size_hint::<V>());
487 blazingly_json::to_writer(&mut body, value)?;
488 record_response_size::<V>(body.len());
489 Ok(Self::from_bytes(body))
490 }
491}
492
493impl<T> fmt::Debug for PreparedJson<T> {
494 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
495 formatter
496 .debug_struct("PreparedJson")
497 .field("bytes", &self.body.len())
498 .finish()
499 }
500}
501
502impl<T: ApiSchema> ApiSchema for PreparedJson<T> {
503 fn type_descriptor() -> TypeDescriptor {
504 T::type_descriptor()
505 }
506}
507
508#[derive(Clone, Debug, Eq, PartialEq)]
510pub struct Created<T>(pub T);
511
512#[derive(Clone, Debug, Eq, PartialEq)]
514pub struct Accepted<T>(pub T);
515
516#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
518pub struct NoContent;
519
520#[derive(Clone, Debug, Eq, PartialEq)]
525pub struct BodyStreamError {
526 pub code: String,
527 pub message: String,
528}
529
530impl BodyStreamError {
531 #[must_use]
532 pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
533 Self {
534 code: code.into(),
535 message: message.into(),
536 }
537 }
538}
539
540impl fmt::Display for BodyStreamError {
541 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
542 formatter.write_str(&self.message)
543 }
544}
545
546impl std::error::Error for BodyStreamError {}
547
548pub trait BodyStream: 'static {
554 fn poll_next(
555 self: Pin<&mut Self>,
556 context: &mut Context<'_>,
557 ) -> Poll<Option<Result<Vec<u8>, BodyStreamError>>>;
558
559 fn recycle(self: Pin<&mut Self>, spent: Vec<u8>) {
567 drop(spent);
568 }
569}
570
571pub struct StreamingBody {
576 stream: Pin<Box<dyn BodyStream>>,
577 exact_length: Option<u64>,
578}
579
580impl StreamingBody {
581 #[must_use]
582 pub fn new(stream: impl BodyStream) -> Self {
583 Self {
584 stream: Box::pin(stream),
585 exact_length: None,
586 }
587 }
588
589 #[must_use]
591 pub fn from_chunks<I, Chunk>(chunks: I) -> Self
592 where
593 I: IntoIterator<Item = Chunk>,
594 I::IntoIter: Unpin + 'static,
595 Chunk: Into<Vec<u8>> + 'static,
596 {
597 Self::new(ChunkIterator {
598 chunks: chunks.into_iter(),
599 })
600 }
601
602 #[must_use]
604 pub fn once(bytes: impl Into<Vec<u8>>) -> Self {
605 let bytes = bytes.into();
606 let length = u64::try_from(bytes.len()).unwrap_or(u64::MAX);
607 Self::from_chunks([bytes]).with_exact_length(length)
608 }
609
610 #[must_use]
611 pub const fn with_exact_length(mut self, length: u64) -> Self {
612 self.exact_length = Some(length);
613 self
614 }
615
616 #[must_use]
617 pub const fn exact_length(&self) -> Option<u64> {
618 self.exact_length
619 }
620
621 pub async fn next_chunk(&mut self) -> Option<Result<Vec<u8>, BodyStreamError>> {
626 poll_fn(|context| self.stream.as_mut().poll_next(context)).await
627 }
628
629 pub fn recycle(&mut self, spent: Vec<u8>) {
634 self.stream.as_mut().recycle(spent);
635 }
636}
637
638impl fmt::Debug for StreamingBody {
639 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
640 formatter
641 .debug_struct("StreamingBody")
642 .field("exact_length", &self.exact_length)
643 .finish_non_exhaustive()
644 }
645}
646
647impl ApiSchema for StreamingBody {
648 fn type_descriptor() -> TypeDescriptor {
649 TypeDescriptor::scalar("StreamingBody", SchemaKind::Binary)
650 }
651}
652
653#[derive(Clone, Debug, Eq, PartialEq)]
655pub struct UpgradeIoError {
656 pub code: String,
657 pub message: String,
658}
659
660impl UpgradeIoError {
661 #[must_use]
662 pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
663 Self {
664 code: code.into(),
665 message: message.into(),
666 }
667 }
668}
669
670impl fmt::Display for UpgradeIoError {
671 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
672 formatter.write_str(&self.message)
673 }
674}
675
676impl std::error::Error for UpgradeIoError {}
677
678pub type UpgradeReadFuture<'io> =
683 Pin<Box<dyn Future<Output = Result<Option<Vec<u8>>, UpgradeIoError>> + 'io>>;
684pub type UpgradeWriteFuture<'io> = Pin<Box<dyn Future<Output = Result<(), UpgradeIoError>> + 'io>>;
685
686pub trait UpgradedIo: 'static {
687 fn read(&mut self) -> UpgradeReadFuture<'_>;
688
689 fn write(&mut self, bytes: Vec<u8>) -> UpgradeWriteFuture<'_>;
690
691 fn shutdown(&mut self) -> UpgradeWriteFuture<'_>;
692}
693
694pub type UpgradeFuture = Pin<Box<dyn Future<Output = Result<(), UpgradeIoError>> + 'static>>;
695pub type UpgradeHandler = Box<dyn FnOnce(Box<dyn UpgradedIo>) -> UpgradeFuture + 'static>;
696
697pub struct HttpUpgrade {
699 protocol: &'static str,
700 headers: Vec<ResponseHeader>,
701 handler: Option<UpgradeHandler>,
702}
703
704impl HttpUpgrade {
705 #[must_use]
706 pub fn new(
707 protocol: &'static str,
708 headers: Vec<ResponseHeader>,
709 handler: impl FnOnce(Box<dyn UpgradedIo>) -> UpgradeFuture + 'static,
710 ) -> Self {
711 Self {
712 protocol,
713 headers,
714 handler: Some(Box::new(handler)),
715 }
716 }
717
718 #[must_use]
719 pub const fn protocol(&self) -> &'static str {
720 self.protocol
721 }
722
723 #[must_use]
724 pub fn headers(&self) -> &[ResponseHeader] {
725 &self.headers
726 }
727
728 pub fn extend_headers(&mut self, headers: impl IntoIterator<Item = ResponseHeader>) {
729 self.headers.extend(headers);
730 }
731
732 pub async fn run(mut self, io: Box<dyn UpgradedIo>) -> Result<(), UpgradeIoError> {
738 let handler = self.handler.take().ok_or_else(|| {
739 UpgradeIoError::new(
740 "upgrade_already_consumed",
741 "the protocol upgrade handler has already been consumed",
742 )
743 })?;
744 handler(io).await
745 }
746}
747
748impl fmt::Debug for HttpUpgrade {
749 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
750 formatter
751 .debug_struct("HttpUpgrade")
752 .field("protocol", &self.protocol)
753 .field("headers", &self.headers)
754 .finish_non_exhaustive()
755 }
756}
757
758impl ApiSchema for HttpUpgrade {
759 fn type_descriptor() -> TypeDescriptor {
760 TypeDescriptor::scalar("HttpUpgrade", SchemaKind::Binary)
761 }
762}
763
764#[derive(Clone, Debug, Eq, PartialEq)]
766pub struct BackgroundTaskError {
767 pub code: String,
768 pub message: String,
769}
770
771impl BackgroundTaskError {
772 #[must_use]
773 pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
774 Self {
775 code: code.into(),
776 message: message.into(),
777 }
778 }
779}
780
781impl fmt::Display for BackgroundTaskError {
782 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
783 formatter.write_str(&self.message)
784 }
785}
786
787impl std::error::Error for BackgroundTaskError {}
788
789pub type BackgroundFuture =
790 Pin<Box<dyn Future<Output = Result<(), BackgroundTaskError>> + 'static>>;
791
792pub struct BackgroundTask {
794 task: Option<Box<dyn FnOnce() -> BackgroundFuture + 'static>>,
795}
796
797impl BackgroundTask {
798 #[must_use]
799 pub fn new<Task, TaskFuture>(task: Task) -> Self
800 where
801 Task: FnOnce() -> TaskFuture + 'static,
802 TaskFuture: Future<Output = Result<(), BackgroundTaskError>> + 'static,
803 {
804 Self {
805 task: Some(Box::new(move || Box::pin(task()))),
806 }
807 }
808
809 #[must_use]
810 pub fn infallible<Task, TaskFuture>(task: Task) -> Self
811 where
812 Task: FnOnce() -> TaskFuture + 'static,
813 TaskFuture: Future<Output = ()> + 'static,
814 {
815 Self::new(move || async move {
816 task().await;
817 Ok(())
818 })
819 }
820
821 pub async fn run(mut self) -> Result<(), BackgroundTaskError> {
827 let task = self.task.take().ok_or_else(|| {
828 BackgroundTaskError::new(
829 "background_task_consumed",
830 "background task has already been consumed",
831 )
832 })?;
833 task().await
834 }
835}
836
837impl fmt::Debug for BackgroundTask {
838 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
839 formatter
840 .debug_struct("BackgroundTask")
841 .finish_non_exhaustive()
842 }
843}
844
845#[derive(Debug)]
847pub struct Background<T> {
848 response: T,
849 tasks: Vec<BackgroundTask>,
850}
851
852impl<T> Background<T> {
853 #[must_use]
854 pub fn new(response: T) -> Self {
855 Self {
856 response,
857 tasks: Vec::new(),
858 }
859 }
860
861 #[must_use]
862 pub fn task(mut self, task: BackgroundTask) -> Self {
863 self.tasks.push(task);
864 self
865 }
866
867 #[must_use]
868 pub fn into_parts(self) -> (T, Vec<BackgroundTask>) {
869 (self.response, self.tasks)
870 }
871}
872
873impl<T: ApiSchema> ApiSchema for Background<T> {
874 fn type_descriptor() -> TypeDescriptor {
875 T::type_descriptor()
876 }
877}
878
879pub trait BackgroundExt: Sized {
881 #[must_use]
882 fn background(self, task: BackgroundTask) -> Background<Self> {
883 Background::new(self).task(task)
884 }
885}
886
887impl<T> BackgroundExt for T {}
888
889pub fn merge_validation_errors(
891 target: &mut ValidationErrors,
892 prefix: &str,
893 nested: &ValidationErrors,
894) {
895 for violation in nested.violations() {
896 let field = if violation.field.is_empty() {
897 prefix.to_owned()
898 } else {
899 format!("{prefix}.{}", violation.field)
900 };
901 target.push(field, violation.code.clone(), violation.message.clone());
902 }
903}
904
905pub fn merge_field_validation_errors(
916 target: &mut ValidationErrors,
917 field: &str,
918 nested: &ValidationErrors,
919) {
920 for violation in nested.violations() {
921 let path = if field.is_empty() {
922 violation.field.clone()
923 } else if violation.field.is_empty() || violation.field == field {
924 field.to_owned()
925 } else if is_rooted_at(&violation.field, field) {
926 violation.field.clone()
927 } else {
928 format!("{field}.{}", violation.field)
929 };
930 target.push(path, violation.code.clone(), violation.message.clone());
931 }
932}
933
934fn is_rooted_at(path: &str, field: &str) -> bool {
936 path.strip_prefix(field)
937 .is_some_and(|rest| rest.starts_with('.') || rest.starts_with('['))
938}
939
940pub trait ApiConstrained: ApiSchema {
948 fn constraint_rules() -> Vec<ValidationRule>;
950
951 fn validate_constraints(&self) -> Result<(), ValidationErrors>;
958}
959
960#[derive(Clone, Debug, PartialEq)]
969pub enum FieldMetadata {
970 Default(blazingly_json::Value),
972 Nullable,
974 Enumeration(Vec<String>),
976}
977
978impl FieldMetadata {
979 #[must_use]
981 pub fn parse(encoded: &str) -> Option<Self> {
982 let (keyword, value) = encoded.split_once('=')?;
983 let metadata = match keyword {
984 "default" => Self::Default(blazingly_json::from_str(value).ok()?),
985 "nullable" if value == "true" => Self::Nullable,
986 "enum" if !value.is_empty() => {
987 Self::Enumeration(value.split('|').map(str::to_owned).collect())
988 }
989 _ => return None,
990 };
991 Some(metadata)
992 }
993
994 #[must_use]
996 pub const fn keyword(&self) -> &'static str {
997 match self {
998 Self::Default(_) => "default",
999 Self::Nullable => "nullable",
1000 Self::Enumeration(_) => "enum",
1001 }
1002 }
1003
1004 #[must_use]
1006 pub fn schema_value(&self) -> blazingly_json::Value {
1007 match self {
1008 Self::Default(value) => value.clone(),
1009 Self::Nullable => blazingly_json::Value::Bool(true),
1010 Self::Enumeration(values) => blazingly_json::Value::Array(
1011 values
1012 .iter()
1013 .map(|value| blazingly_json::Value::String(value.clone()))
1014 .collect(),
1015 ),
1016 }
1017 }
1018
1019 pub fn apply_json_schema(&self, schema: &mut blazingly_json::Value) {
1021 if let Some(object) = schema.as_object_mut() {
1022 object.insert(self.keyword().to_owned(), self.schema_value());
1023 }
1024 }
1025}
1026
1027impl fmt::Display for FieldMetadata {
1028 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1029 match self {
1030 Self::Default(value) => write!(formatter, "default={value}"),
1031 Self::Nullable => formatter.write_str("nullable=true"),
1032 Self::Enumeration(values) => {
1033 formatter.write_str("enum=")?;
1034 for (index, value) in values.iter().enumerate() {
1035 if index > 0 {
1036 formatter.write_str("|")?;
1037 }
1038 formatter.write_str(value)?;
1039 }
1040 Ok(())
1041 }
1042 }
1043 }
1044}
1045
1046pub const MAX_MULTIPART_HEADER_BYTES: usize = 16 * 1024;
1055
1056pub const MAX_MULTIPART_PARTS: usize = 256;
1058
1059#[derive(Clone, Debug, Default, Eq, PartialEq)]
1061pub struct MultipartPartHeaders {
1062 pub name: String,
1063 pub file_name: Option<String>,
1064 pub content_type: Option<String>,
1065}
1066
1067#[doc(hidden)]
1073#[must_use]
1074pub fn multipart_boundary(content_type: &str) -> Option<String> {
1075 let mut parameters = header_parameters(content_type);
1076 if !parameters
1077 .next()?
1078 .trim()
1079 .eq_ignore_ascii_case("multipart/form-data")
1080 {
1081 return None;
1082 }
1083 for parameter in parameters {
1084 let (name, value) = parameter.split_once('=')?;
1085 if name.trim().eq_ignore_ascii_case("boundary") {
1086 let boundary = unquote_header_value(value.trim())?;
1087 if boundary.is_empty()
1088 || boundary.len() > 70
1089 || boundary.bytes().any(|byte| byte <= b' ' || byte >= 127)
1090 {
1091 return None;
1092 }
1093 return Some(boundary);
1094 }
1095 }
1096 None
1097}
1098
1099#[doc(hidden)]
1105pub fn multipart_part_headers(headers: &str) -> Result<MultipartPartHeaders, &'static str> {
1106 let mut name = None;
1107 let mut file_name = None;
1108 let mut content_type = None;
1109 for line in headers.split("\r\n") {
1110 let (header_name, value) = line
1111 .split_once(':')
1112 .ok_or("multipart part header is malformed")?;
1113 if header_name.eq_ignore_ascii_case("content-disposition") {
1114 let mut parameters = header_parameters(value);
1115 if !parameters
1116 .next()
1117 .is_some_and(|value| value.trim().eq_ignore_ascii_case("form-data"))
1118 {
1119 return Err("multipart Content-Disposition must be form-data");
1120 }
1121 for parameter in parameters {
1122 let Some((parameter_name, parameter_value)) = parameter.split_once('=') else {
1123 continue;
1124 };
1125 if parameter_name.trim().eq_ignore_ascii_case("name") {
1126 name = unquote_header_value(parameter_value.trim());
1127 } else if parameter_name.trim().eq_ignore_ascii_case("filename") {
1128 file_name = unquote_header_value(parameter_value.trim());
1129 }
1130 }
1131 } else if header_name.eq_ignore_ascii_case("content-type") {
1132 content_type = Some(value.trim().to_owned());
1133 }
1134 }
1135 let name = name
1136 .filter(|name| !name.is_empty())
1137 .ok_or("multipart part has no field name")?;
1138 Ok(MultipartPartHeaders {
1139 name,
1140 file_name,
1141 content_type,
1142 })
1143}
1144
1145#[doc(hidden)]
1147pub fn header_parameters(value: &str) -> impl Iterator<Item = &str> {
1148 let mut start = 0;
1149 let mut quoted = false;
1150 let mut escaped = false;
1151 let mut ranges = Vec::new();
1152 for (index, character) in value.char_indices() {
1153 if escaped {
1154 escaped = false;
1155 } else if character == '\\' && quoted {
1156 escaped = true;
1157 } else if character == '"' {
1158 quoted = !quoted;
1159 } else if character == ';' && !quoted {
1160 ranges.push((start, index));
1161 start = index + character.len_utf8();
1162 }
1163 }
1164 ranges.push((start, value.len()));
1165 ranges
1166 .into_iter()
1167 .map(move |(start, end)| &value[start..end])
1168}
1169
1170#[doc(hidden)]
1172#[must_use]
1173pub fn unquote_header_value(value: &str) -> Option<String> {
1174 if let Some(value) = value.strip_prefix('"') {
1175 let value = value.strip_suffix('"')?;
1176 let mut output = String::with_capacity(value.len());
1177 let mut escaped = false;
1178 for character in value.chars() {
1179 if escaped {
1180 output.push(character);
1181 escaped = false;
1182 } else if character == '\\' {
1183 escaped = true;
1184 } else {
1185 output.push(character);
1186 }
1187 }
1188 if escaped {
1189 return None;
1190 }
1191 Some(output)
1192 } else {
1193 Some(value.to_owned())
1194 }
1195}
1196
1197#[doc(hidden)]
1203#[must_use]
1204pub fn find_bytes(haystack: &[u8], needle: &[u8], from: usize) -> Option<usize> {
1205 let (first, rest) = needle.split_first()?;
1206 let last_start = haystack.len().checked_sub(needle.len())?;
1207 let mut position = from;
1208 while position <= last_start {
1209 let offset = memchr::memchr(*first, haystack.get(position..=last_start)?)?;
1212 let candidate = position + offset;
1213 if haystack.get(candidate + 1..candidate + needle.len()) == Some(rest) {
1214 return Some(candidate);
1215 }
1216 position = candidate + 1;
1217 }
1218 None
1219}
1220
1221#[derive(Clone, Debug, Eq, PartialEq)]
1227pub enum MultipartError {
1228 Malformed(&'static str),
1234 TooLarge { limit: usize },
1237 Transport(BodyStreamError),
1242}
1243
1244const MALFORMED_MULTIPART_MESSAGE: &str = "request body is not valid multipart form data";
1245const UPLOAD_STREAM_MESSAGE: &str = "the request body could not be read to its end";
1246const MULTIPART_TOO_LARGE_MESSAGE: &str = "multipart part exceeds the limit the handler set";
1247
1248impl MultipartError {
1249 #[must_use]
1251 pub const fn status(&self) -> u16 {
1252 match self {
1253 Self::Malformed(_) => 422,
1254 Self::TooLarge { .. } => 413,
1255 Self::Transport(_) => 400,
1256 }
1257 }
1258
1259 #[must_use]
1261 pub const fn code(&self) -> &'static str {
1262 match self {
1263 Self::Malformed(_) => "invalid_multipart",
1264 Self::TooLarge { .. } => "payload_too_large",
1265 Self::Transport(_) => "upload_stream_failed",
1266 }
1267 }
1268
1269 #[must_use]
1271 pub const fn message(&self) -> &'static str {
1272 match self {
1273 Self::Malformed(_) => MALFORMED_MULTIPART_MESSAGE,
1274 Self::TooLarge { .. } => MULTIPART_TOO_LARGE_MESSAGE,
1275 Self::Transport(_) => UPLOAD_STREAM_MESSAGE,
1276 }
1277 }
1278
1279 fn details(&self) -> blazingly_json::Value {
1280 let mut details = blazingly_json::Map::new();
1281 match self {
1282 Self::Malformed(reason) => {
1283 details.insert(
1284 "source".to_owned(),
1285 blazingly_json::Value::String("multipart".to_owned()),
1286 );
1287 details.insert(
1288 "reason".to_owned(),
1289 blazingly_json::Value::String((*reason).to_owned()),
1290 );
1291 }
1292 Self::TooLarge { limit } => {
1293 details.insert(
1294 "source".to_owned(),
1295 blazingly_json::Value::String("multipart".to_owned()),
1296 );
1297 details.insert("limit".to_owned(), blazingly_json::Value::from(*limit));
1298 }
1299 Self::Transport(error) => {
1300 details.insert(
1301 "source".to_owned(),
1302 blazingly_json::Value::String("stream".to_owned()),
1303 );
1304 details.insert(
1305 "reason".to_owned(),
1306 blazingly_json::Value::String(error.code.clone()),
1307 );
1308 }
1309 }
1310 blazingly_json::Value::Object(details)
1311 }
1312}
1313
1314impl fmt::Display for MultipartError {
1315 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1316 match self {
1317 Self::Malformed(reason) => formatter.write_str(reason),
1318 Self::TooLarge { limit } => {
1319 write!(formatter, "multipart part exceeds the {limit}-byte limit")
1320 }
1321 Self::Transport(error) => formatter.write_str(&error.message),
1322 }
1323 }
1324}
1325
1326impl std::error::Error for MultipartError {}
1327
1328impl ApiError for MultipartError {
1329 fn response_descriptors() -> Vec<ResponseDescriptor> {
1330 vec![
1331 ResponseDescriptor::error(400, "upload_stream_failed", UPLOAD_STREAM_MESSAGE, None),
1332 ResponseDescriptor::error(413, "payload_too_large", MULTIPART_TOO_LARGE_MESSAGE, None),
1333 ResponseDescriptor::error(422, "invalid_multipart", MALFORMED_MULTIPART_MESSAGE, None),
1334 ]
1335 }
1336
1337 fn into_failure(self) -> Result<OperationFailure, ResponseBuildError> {
1338 let details = blazingly_json::to_vec(&self.details())
1339 .map_err(|_| ResponseBuildError::serialization_failed())?;
1340 Ok(OperationFailure::new(self.status(), self.code(), self.message()).with_details(details))
1341 }
1342}
1343
1344#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1346enum MultipartState {
1347 Opening,
1349 Headers,
1351 Part,
1353 Done,
1355 Failed,
1357}
1358
1359enum MultipartScan {
1361 Terminator(usize),
1363 Data(usize),
1369}
1370
1371#[derive(Debug)]
1403pub struct MultipartStream {
1404 body: StreamingBody,
1405 delimiter: Vec<u8>,
1407 buffer: Vec<u8>,
1408 cursor: usize,
1409 state: MultipartState,
1410 parts: usize,
1411 bytes_read: u64,
1412}
1413
1414impl MultipartStream {
1415 pub fn new(body: StreamingBody, content_type: &str) -> Result<Self, MultipartError> {
1423 let boundary = multipart_boundary(content_type).ok_or(MultipartError::Malformed(
1424 "multipart boundary is missing or invalid",
1425 ))?;
1426 let mut delimiter = Vec::with_capacity(boundary.len() + 2);
1427 delimiter.extend_from_slice(b"--");
1428 delimiter.extend_from_slice(boundary.as_bytes());
1429 Ok(Self {
1430 body,
1431 delimiter,
1432 buffer: Vec::new(),
1433 cursor: 0,
1434 state: MultipartState::Opening,
1435 parts: 0,
1436 bytes_read: 0,
1437 })
1438 }
1439
1440 #[must_use]
1442 pub const fn bytes_read(&self) -> u64 {
1443 self.bytes_read
1444 }
1445
1446 pub async fn next_field(&mut self) -> Result<Option<MultipartField<'_>>, MultipartError> {
1455 match self.state {
1456 MultipartState::Failed => {
1457 return Err(MultipartError::Malformed(
1458 "multipart body already failed to parse",
1459 ));
1460 }
1461 MultipartState::Done => return Ok(None),
1462 MultipartState::Opening => self.read_opening().await?,
1463 MultipartState::Part => self.skip_part().await?,
1464 MultipartState::Headers => {}
1465 }
1466 if self.state == MultipartState::Done {
1467 return Ok(None);
1468 }
1469 let headers = self.read_headers().await?;
1470 self.parts += 1;
1471 if self.parts > MAX_MULTIPART_PARTS {
1472 return Err(self.fail("multipart body contains too many parts"));
1473 }
1474 self.state = MultipartState::Part;
1475 Ok(Some(MultipartField {
1476 stream: self,
1477 headers,
1478 }))
1479 }
1480
1481 fn fail(&mut self, reason: &'static str) -> MultipartError {
1483 self.state = MultipartState::Failed;
1484 MultipartError::Malformed(reason)
1485 }
1486
1487 const fn available(&self) -> usize {
1489 self.buffer.len() - self.cursor
1490 }
1491
1492 async fn fill(&mut self) -> Result<bool, MultipartError> {
1494 loop {
1495 match self.body.next_chunk().await {
1496 Some(Ok(chunk)) if chunk.is_empty() => {}
1497 Some(Ok(chunk)) => {
1498 if self.cursor > 0 {
1503 self.buffer.drain(..self.cursor);
1504 self.cursor = 0;
1505 }
1506 self.bytes_read = self
1507 .bytes_read
1508 .saturating_add(u64::try_from(chunk.len()).unwrap_or(u64::MAX));
1509 self.buffer.extend_from_slice(&chunk);
1510 self.body.recycle(chunk);
1513 return Ok(true);
1514 }
1515 Some(Err(error)) => {
1516 self.state = MultipartState::Failed;
1517 return Err(MultipartError::Transport(error));
1518 }
1519 None => return Ok(false),
1520 }
1521 }
1522 }
1523
1524 async fn read_opening(&mut self) -> Result<(), MultipartError> {
1526 while self.available() < self.delimiter.len() {
1527 if !self.fill().await? {
1528 return Err(self.fail("multipart body does not start with its declared boundary"));
1529 }
1530 }
1531 if !self.buffer[self.cursor..].starts_with(&self.delimiter) {
1532 return Err(self.fail("multipart body does not start with its declared boundary"));
1533 }
1534 self.cursor += self.delimiter.len();
1535 self.read_boundary_suffix().await
1536 }
1537
1538 async fn read_boundary_suffix(&mut self) -> Result<(), MultipartError> {
1540 while self.available() < 2 {
1541 if !self.fill().await? {
1542 return Err(self.fail("multipart boundary is malformed"));
1543 }
1544 }
1545 let suffix = [self.buffer[self.cursor], self.buffer[self.cursor + 1]];
1546 self.cursor += 2;
1547 match &suffix {
1548 b"--" => {
1549 self.state = MultipartState::Done;
1550 Ok(())
1551 }
1552 b"\r\n" => {
1553 self.state = MultipartState::Headers;
1554 Ok(())
1555 }
1556 _ => Err(self.fail("multipart boundary is malformed")),
1557 }
1558 }
1559
1560 async fn read_headers(&mut self) -> Result<MultipartPartHeaders, MultipartError> {
1562 let mut searched = 0;
1563 let end = loop {
1564 if let Some(found) = find_bytes(&self.buffer[self.cursor..], b"\r\n\r\n", searched) {
1565 break found;
1566 }
1567 let available = self.available();
1568 if available > MAX_MULTIPART_HEADER_BYTES {
1569 return Err(self.fail("multipart part headers exceed the configured limit"));
1570 }
1571 searched = available.saturating_sub(3);
1572 if !self.fill().await? {
1573 return Err(self.fail("multipart part headers are incomplete"));
1574 }
1575 };
1576 if end > MAX_MULTIPART_HEADER_BYTES {
1577 return Err(self.fail("multipart part headers exceed the configured limit"));
1578 }
1579 let parsed = match std::str::from_utf8(&self.buffer[self.cursor..self.cursor + end]) {
1580 Ok(headers) => multipart_part_headers(headers),
1581 Err(_) => Err("multipart part headers are not valid UTF-8"),
1582 };
1583 let parsed = parsed.map_err(|reason| self.fail(reason))?;
1584 self.cursor += end + 4;
1585 Ok(parsed)
1586 }
1587
1588 async fn skip_part(&mut self) -> Result<(), MultipartError> {
1590 while self.advance_part().await?.is_some() {}
1591 Ok(())
1592 }
1593
1594 async fn advance_part(&mut self) -> Result<Option<(usize, usize)>, MultipartError> {
1599 loop {
1600 if self.state != MultipartState::Part {
1601 return Ok(None);
1602 }
1603 match scan_multipart_terminator(&self.buffer[self.cursor..], &self.delimiter) {
1604 MultipartScan::Terminator(offset) => {
1605 if offset > 0 {
1606 let start = self.cursor;
1607 self.cursor += offset;
1608 return Ok(Some((start, self.cursor)));
1609 }
1610 self.cursor += 2 + self.delimiter.len();
1611 self.read_boundary_suffix().await?;
1612 return Ok(None);
1613 }
1614 MultipartScan::Data(safe) => {
1615 if safe > 0 {
1616 let start = self.cursor;
1617 self.cursor += safe;
1618 return Ok(Some((start, self.cursor)));
1619 }
1620 if !self.fill().await? {
1621 return Err(self.fail("multipart part has no closing boundary"));
1622 }
1623 }
1624 }
1625 }
1626 }
1627}
1628
1629#[derive(Debug)]
1634pub struct MultipartField<'stream> {
1635 stream: &'stream mut MultipartStream,
1636 headers: MultipartPartHeaders,
1637}
1638
1639impl MultipartField<'_> {
1640 #[must_use]
1642 pub fn name(&self) -> &str {
1643 &self.headers.name
1644 }
1645
1646 #[must_use]
1648 pub fn file_name(&self) -> Option<&str> {
1649 self.headers.file_name.as_deref()
1650 }
1651
1652 #[must_use]
1654 pub fn content_type(&self) -> Option<&str> {
1655 self.headers.content_type.as_deref()
1656 }
1657
1658 pub async fn next_chunk(&mut self) -> Result<Option<&[u8]>, MultipartError> {
1669 match self.stream.advance_part().await? {
1670 Some((start, end)) => Ok(Some(&self.stream.buffer[start..end])),
1671 None => Ok(None),
1672 }
1673 }
1674
1675 pub async fn collect(mut self, limit: usize) -> Result<Vec<u8>, MultipartError> {
1682 let mut bytes = Vec::new();
1683 while let Some(chunk) = self.next_chunk().await? {
1684 if bytes.len().saturating_add(chunk.len()) > limit {
1685 return Err(MultipartError::TooLarge { limit });
1686 }
1687 bytes.extend_from_slice(chunk);
1688 }
1689 Ok(bytes)
1690 }
1691
1692 pub async fn text(self, limit: usize) -> Result<String, MultipartError> {
1700 let bytes = self.collect(limit).await?;
1701 String::from_utf8(bytes)
1702 .map_err(|_| MultipartError::Malformed("multipart text field is not valid UTF-8"))
1703 }
1704
1705 pub async fn into_upload(self, limit: usize) -> Result<UploadFile, MultipartError> {
1716 let field_name = self.headers.name.clone();
1717 let file_name = self.headers.file_name.clone();
1718 let content_type = self.headers.content_type.clone();
1719 let bytes = self.collect(limit).await?;
1720 Ok(UploadFile {
1721 field_name,
1722 file_name,
1723 content_type,
1724 bytes,
1725 })
1726 }
1727}
1728
1729fn scan_multipart_terminator(data: &[u8], delimiter: &[u8]) -> MultipartScan {
1731 let mut from = 0;
1732 while let Some(found) = find_bytes(data, b"\r\n--", from) {
1733 let start = found + 2;
1734 let end = start + delimiter.len();
1735 if end + 2 > data.len() {
1736 let seen = &data[start..];
1740 let compared = seen.len().min(delimiter.len());
1741 if seen.get(..compared) == delimiter.get(..compared) {
1742 return MultipartScan::Data(found);
1743 }
1744 from = found + 2;
1745 continue;
1746 }
1747 if data.get(start..end) == Some(delimiter)
1748 && matches!(data.get(end..end + 2), Some(b"\r\n" | b"--"))
1749 {
1750 return MultipartScan::Terminator(found);
1751 }
1752 from = found + 2;
1753 }
1754 MultipartScan::Data(data.len().saturating_sub(3))
1756}
1757
1758struct ChunkIterator<I> {
1759 chunks: I,
1760}
1761
1762impl<I, Chunk> BodyStream for ChunkIterator<I>
1763where
1764 I: Iterator<Item = Chunk> + Unpin + 'static,
1765 Chunk: Into<Vec<u8>> + 'static,
1766{
1767 fn poll_next(
1768 self: Pin<&mut Self>,
1769 _context: &mut Context<'_>,
1770 ) -> Poll<Option<Result<Vec<u8>, BodyStreamError>>> {
1771 Poll::Ready(self.get_mut().chunks.next().map(|chunk| Ok(chunk.into())))
1772 }
1773}
1774
1775#[derive(Clone, Debug, Eq, PartialEq)]
1777pub struct Status<const STATUS: u16, T>(pub T);
1778
1779#[derive(Clone, Debug, Eq, PartialEq)]
1781pub struct WithHeaders<T> {
1782 response: T,
1783 headers: Vec<ResponseHeader>,
1784}
1785
1786impl<T> WithHeaders<T> {
1787 #[must_use]
1788 pub fn header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
1789 self.headers.push(ResponseHeader::new(name, value));
1790 self
1791 }
1792
1793 #[must_use]
1794 pub fn into_parts(self) -> (T, Vec<ResponseHeader>) {
1795 (self.response, self.headers)
1796 }
1797}
1798
1799pub trait ResponseExt: Sized {
1801 #[must_use]
1802 fn header(self, name: impl Into<String>, value: impl Into<String>) -> WithHeaders<Self> {
1803 WithHeaders {
1804 response: self,
1805 headers: vec![ResponseHeader::new(name, value)],
1806 }
1807 }
1808}
1809
1810impl<T> ResponseExt for T {}
1811
1812#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
1814#[serde(rename_all = "UPPERCASE")]
1815pub enum HttpMethod {
1816 Get,
1817 Head,
1818 Post,
1819 Put,
1820 Patch,
1821 Delete,
1822 Options,
1823 Trace,
1824 Connect,
1825}
1826
1827impl HttpMethod {
1828 #[must_use]
1829 pub const fn as_str(self) -> &'static str {
1830 match self {
1831 Self::Get => "GET",
1832 Self::Head => "HEAD",
1833 Self::Post => "POST",
1834 Self::Put => "PUT",
1835 Self::Patch => "PATCH",
1836 Self::Delete => "DELETE",
1837 Self::Options => "OPTIONS",
1838 Self::Trace => "TRACE",
1839 Self::Connect => "CONNECT",
1840 }
1841 }
1842
1843 #[must_use]
1844 pub const fn as_openapi_key(self) -> &'static str {
1845 match self {
1846 Self::Get => "get",
1847 Self::Head => "head",
1848 Self::Post => "post",
1849 Self::Put => "put",
1850 Self::Patch => "patch",
1851 Self::Delete => "delete",
1852 Self::Options => "options",
1853 Self::Trace => "trace",
1854 Self::Connect => "x-blazingly-connect",
1856 }
1857 }
1858}
1859
1860#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1862pub struct HttpBinding {
1863 pub method: HttpMethod,
1864 pub path: String,
1865}
1866
1867impl HttpBinding {
1868 #[must_use]
1869 pub fn new(method: HttpMethod, path: impl Into<String>) -> Self {
1870 Self {
1871 method,
1872 path: path.into(),
1873 }
1874 }
1875}
1876
1877#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1879pub struct OperationDescriptor {
1880 pub contract: OperationContract,
1881 pub http: HttpBinding,
1882}
1883
1884impl OperationDescriptor {
1885 pub fn new(
1892 method: HttpMethod,
1893 path: impl Into<String>,
1894 id: impl Into<String>,
1895 summary: impl Into<String>,
1896 input: Option<TypeDescriptor>,
1897 responses: Vec<ResponseDescriptor>,
1898 ) -> Result<Self, InvalidOperationId> {
1899 Ok(Self {
1900 contract: OperationContract::new(id, summary, input, responses)?,
1901 http: HttpBinding::new(method, path),
1902 })
1903 }
1904
1905 #[must_use]
1906 pub fn with_mcp_tool(mut self, tool: McpToolDescriptor, policy: AgentPolicy) -> Self {
1907 self.contract = self.contract.with_agent_policy(policy).with_mcp_tool(tool);
1908 self
1909 }
1910
1911 #[must_use]
1912 pub fn mcp_tool(&self) -> Option<&McpToolDescriptor> {
1913 self.contract.mcp.as_ref()
1914 }
1915
1916 #[must_use]
1917 pub fn with_inputs(mut self, inputs: Vec<InputDescriptor>) -> Self {
1918 self.contract = self.contract.with_inputs(inputs);
1919 self
1920 }
1921
1922 #[must_use]
1923 pub fn with_dependencies(mut self, dependencies: Vec<DependencyDescriptor>) -> Self {
1924 self.contract = self.contract.with_dependencies(dependencies);
1925 self
1926 }
1927
1928 #[must_use]
1929 pub fn with_security(mut self, requirements: Vec<SecurityRequirement>) -> Self {
1930 self.contract = self.contract.with_security(requirements);
1931 self
1932 }
1933}
1934
1935#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1937pub struct AppDefinition {
1938 operations: Vec<OperationDescriptor>,
1939 security_schemes: Vec<SecuritySchemeDescriptor>,
1940}
1941
1942impl AppDefinition {
1943 #[must_use]
1944 pub fn operations(&self) -> &[OperationDescriptor] {
1945 &self.operations
1946 }
1947
1948 #[must_use]
1949 pub fn security_schemes(&self) -> &[SecuritySchemeDescriptor] {
1950 &self.security_schemes
1951 }
1952}
1953
1954#[derive(Clone, Debug, Default)]
1956pub struct App {
1957 operations: Vec<OperationDescriptor>,
1958 security_schemes: Vec<SecuritySchemeDescriptor>,
1959}
1960
1961impl App {
1962 #[must_use]
1963 pub const fn new() -> Self {
1964 Self {
1965 operations: Vec::new(),
1966 security_schemes: Vec::new(),
1967 }
1968 }
1969
1970 #[must_use]
1971 pub fn route(mut self, operation: OperationDescriptor) -> Self {
1972 self.operations.push(operation);
1973 self
1974 }
1975
1976 #[must_use]
1977 pub fn routes(mut self, operations: impl IntoIterator<Item = OperationDescriptor>) -> Self {
1978 self.operations.extend(operations);
1979 self
1980 }
1981
1982 #[must_use]
1984 pub fn security_scheme(mut self, scheme: SecuritySchemeDescriptor) -> Self {
1985 self.security_schemes.push(scheme);
1986 self
1987 }
1988
1989 pub fn build(mut self) -> Result<AppDefinition, BuildError> {
1996 let mut operation_ids = BTreeSet::new();
1997 let mut http_bindings = BTreeSet::new();
1998 let mut route_shapes = BTreeSet::new();
1999 let mut security_names = BTreeSet::new();
2000
2001 for scheme in &self.security_schemes {
2002 if !security_names.insert(scheme.name.clone()) {
2003 return Err(BuildError::DuplicateSecurityScheme(scheme.name.clone()));
2004 }
2005 }
2006
2007 for operation in &self.operations {
2008 validate_operation_inputs(operation)?;
2009 validate_operation_security(operation, &self.security_schemes)?;
2010 if !operation_ids.insert(operation.contract.id.clone()) {
2011 return Err(BuildError::DuplicateOperationId(
2012 operation.contract.id.clone(),
2013 ));
2014 }
2015
2016 let binding = (operation.http.method, operation.http.path.clone());
2017 if !http_bindings.insert(binding) {
2018 return Err(BuildError::DuplicateHttpBinding {
2019 method: operation.http.method,
2020 path: operation.http.path.clone(),
2021 });
2022 }
2023 if !route_shapes.insert((
2024 operation.http.method,
2025 canonical_route_shape(&operation.http.path),
2026 )) {
2027 return Err(BuildError::AmbiguousHttpBinding {
2028 method: operation.http.method,
2029 path: operation.http.path.clone(),
2030 });
2031 }
2032 }
2033
2034 self.operations.sort_by(|left, right| {
2035 left.http
2036 .path
2037 .cmp(&right.http.path)
2038 .then(left.http.method.cmp(&right.http.method))
2039 .then(left.contract.id.cmp(&right.contract.id))
2040 });
2041 self.security_schemes
2042 .sort_by(|left, right| left.name.cmp(&right.name));
2043
2044 Ok(AppDefinition {
2045 operations: self.operations,
2046 security_schemes: self.security_schemes,
2047 })
2048 }
2049}
2050
2051#[derive(Clone, Debug, Eq, PartialEq)]
2053pub enum BuildError {
2054 DuplicateOperationId(OperationId),
2055 DuplicateHttpBinding {
2056 method: HttpMethod,
2057 path: String,
2058 },
2059 AmbiguousHttpBinding {
2060 method: HttpMethod,
2061 path: String,
2062 },
2063 InvalidPathInputs {
2064 operation: OperationId,
2065 },
2066 DuplicateInputName {
2067 operation: OperationId,
2068 name: String,
2069 },
2070 DuplicateSecurityScheme(String),
2071 DuplicateSecurityRequirement {
2072 operation: OperationId,
2073 scheme: String,
2074 },
2075 UnknownSecurityScheme {
2076 operation: OperationId,
2077 scheme: String,
2078 },
2079 UnknownSecurityScope {
2080 operation: OperationId,
2081 scheme: String,
2082 scope: String,
2083 },
2084}
2085
2086impl fmt::Display for BuildError {
2087 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
2088 match self {
2089 Self::DuplicateOperationId(id) => {
2090 write!(
2091 formatter,
2092 "operation id {id:?} is registered more than once"
2093 )
2094 }
2095 Self::DuplicateHttpBinding { method, path } => write!(
2096 formatter,
2097 "{} {path} is registered more than once",
2098 method.as_str()
2099 ),
2100 Self::AmbiguousHttpBinding { method, path } => write!(
2101 formatter,
2102 "{} {path} conflicts with another parameterized route",
2103 method.as_str()
2104 ),
2105 Self::InvalidPathInputs { operation } => write!(
2106 formatter,
2107 "operation {operation} path placeholders do not match its Path<T> inputs"
2108 ),
2109 Self::DuplicateInputName { operation, name } => write!(
2110 formatter,
2111 "operation {operation} exposes input name {name:?} more than once"
2112 ),
2113 Self::DuplicateSecurityScheme(name) => {
2114 write!(
2115 formatter,
2116 "security scheme {name:?} is registered more than once"
2117 )
2118 }
2119 Self::DuplicateSecurityRequirement { operation, scheme } => write!(
2120 formatter,
2121 "operation {operation} requires security scheme {scheme:?} more than once"
2122 ),
2123 Self::UnknownSecurityScheme { operation, scheme } => write!(
2124 formatter,
2125 "operation {operation} references unknown security scheme {scheme:?}"
2126 ),
2127 Self::UnknownSecurityScope {
2128 operation,
2129 scheme,
2130 scope,
2131 } => write!(
2132 formatter,
2133 "operation {operation} requires unknown scope {scope:?} from security scheme {scheme:?}"
2134 ),
2135 }
2136 }
2137}
2138
2139impl std::error::Error for BuildError {}
2140
2141fn canonical_route_shape(path: &str) -> String {
2142 path.split('/')
2143 .map(|segment| {
2144 if segment.starts_with('{') && segment.ends_with('}') {
2145 "{}"
2146 } else {
2147 segment
2148 }
2149 })
2150 .collect::<Vec<_>>()
2151 .join("/")
2152}
2153
2154fn validate_operation_inputs(operation: &OperationDescriptor) -> Result<(), BuildError> {
2155 let placeholders = operation
2156 .http
2157 .path
2158 .split('/')
2159 .filter_map(|segment| {
2160 segment
2161 .strip_prefix('{')
2162 .and_then(|segment| segment.strip_suffix('}'))
2163 .filter(|name| !name.is_empty())
2164 .map(str::to_owned)
2165 })
2166 .collect::<BTreeSet<_>>();
2167 let path_inputs = operation
2168 .contract
2169 .inputs
2170 .iter()
2171 .filter(|input| input.source == InputSource::Path)
2172 .flat_map(input_public_names)
2173 .collect::<BTreeSet<_>>();
2174 if placeholders != path_inputs {
2175 return Err(BuildError::InvalidPathInputs {
2176 operation: operation.contract.id.clone(),
2177 });
2178 }
2179
2180 let mut names = BTreeSet::new();
2181 for name in operation
2182 .contract
2183 .inputs
2184 .iter()
2185 .flat_map(input_public_names)
2186 {
2187 if !names.insert(name.clone()) {
2188 return Err(BuildError::DuplicateInputName {
2189 operation: operation.contract.id.clone(),
2190 name,
2191 });
2192 }
2193 }
2194 Ok(())
2195}
2196
2197fn validate_operation_security(
2198 operation: &OperationDescriptor,
2199 schemes: &[SecuritySchemeDescriptor],
2200) -> Result<(), BuildError> {
2201 let mut required_schemes = BTreeSet::new();
2202 for requirement in &operation.contract.security {
2203 if !required_schemes.insert(requirement.scheme.as_str()) {
2204 return Err(BuildError::DuplicateSecurityRequirement {
2205 operation: operation.contract.id.clone(),
2206 scheme: requirement.scheme.clone(),
2207 });
2208 }
2209 let Some(scheme) = schemes
2210 .iter()
2211 .find(|scheme| scheme.name == requirement.scheme)
2212 else {
2213 return Err(BuildError::UnknownSecurityScheme {
2214 operation: operation.contract.id.clone(),
2215 scheme: requirement.scheme.clone(),
2216 });
2217 };
2218 let declared_scopes = match &scheme.kind {
2219 SecuritySchemeKind::OAuth2 { scopes, .. } => Some(scopes),
2220 SecuritySchemeKind::ApiKey { .. }
2221 | SecuritySchemeKind::Http { .. }
2222 | SecuritySchemeKind::OpenIdConnect { .. }
2223 | SecuritySchemeKind::MutualTls => None,
2224 };
2225 for scope in &requirement.scopes {
2226 if declared_scopes.is_none_or(|scopes| !scopes.contains(scope)) {
2227 return Err(BuildError::UnknownSecurityScope {
2228 operation: operation.contract.id.clone(),
2229 scheme: requirement.scheme.clone(),
2230 scope: scope.clone(),
2231 });
2232 }
2233 }
2234 }
2235 Ok(())
2236}
2237
2238fn input_public_names(input: &InputDescriptor) -> Vec<String> {
2239 input.ty.model.as_ref().map_or_else(
2240 || vec![input.name.clone()],
2241 |model| {
2242 model
2243 .fields
2244 .iter()
2245 .map(|field| field.name.clone())
2246 .collect()
2247 },
2248 )
2249}
2250
2251#[macro_export]
2252macro_rules! descriptors {
2253 ($($operation:ident),* $(,)?) => {
2254 ::std::vec![$($operation::descriptor()),*]
2255 };
2256}
2257
2258#[cfg(test)]
2259mod tests {
2260 use super::{
2261 ApiError, ApiSchema, App, BodyStream, BodyStreamError, BuildError, FieldMetadata,
2262 HttpMethod, InputDescriptor, InputSource, MAX_MULTIPART_PARTS, MAX_RESPONSE_HINT,
2263 MIN_RESPONSE_HINT, MultipartError, MultipartStream, OperationDescriptor, PreparedJson,
2264 ResponseDescriptor, SchemaKind, SecurityRequirement, SecuritySchemeDescriptor,
2265 SecuritySchemeKind, StreamingBody, TypeDescriptor, UploadFile, UploadSlots,
2266 merge_field_validation_errors, record_response_size, response_size_hint,
2267 };
2268 use crate::ValidationErrors;
2269 use futures_lite::future::block_on;
2270 use std::pin::Pin;
2271 use std::task::{Context, Poll};
2272
2273 const BOUNDARY: &str = "apibenchcover9f2c41d7e6b3";
2274
2275 fn content_type() -> String {
2276 format!("multipart/form-data; boundary={BOUNDARY}")
2277 }
2278
2279 type TestPart<'data> = (
2281 &'data str,
2282 Option<&'data str>,
2283 Option<&'data str>,
2284 &'data [u8],
2285 );
2286
2287 fn multipart_document(parts: &[TestPart<'_>]) -> Vec<u8> {
2289 let mut body = Vec::new();
2290 for (name, file_name, media_type, data) in parts {
2291 body.extend_from_slice(format!("--{BOUNDARY}\r\n").as_bytes());
2292 body.extend_from_slice(
2293 format!("Content-Disposition: form-data; name=\"{name}\"").as_bytes(),
2294 );
2295 if let Some(file_name) = file_name {
2296 body.extend_from_slice(format!("; filename=\"{file_name}\"").as_bytes());
2297 }
2298 body.extend_from_slice(b"\r\n");
2299 if let Some(media_type) = media_type {
2300 body.extend_from_slice(format!("Content-Type: {media_type}\r\n").as_bytes());
2301 }
2302 body.extend_from_slice(b"\r\n");
2303 body.extend_from_slice(data);
2304 body.extend_from_slice(b"\r\n");
2305 }
2306 body.extend_from_slice(format!("--{BOUNDARY}--\r\n").as_bytes());
2307 body
2308 }
2309
2310 struct SlicedBody {
2313 bytes: Vec<u8>,
2314 chunk: usize,
2315 position: usize,
2316 }
2317
2318 impl BodyStream for SlicedBody {
2319 fn poll_next(
2320 self: Pin<&mut Self>,
2321 _context: &mut Context<'_>,
2322 ) -> Poll<Option<Result<Vec<u8>, BodyStreamError>>> {
2323 let body = self.get_mut();
2324 if body.position >= body.bytes.len() {
2325 return Poll::Ready(None);
2326 }
2327 let end = body.bytes.len().min(body.position + body.chunk);
2328 let chunk = body.bytes[body.position..end].to_vec();
2329 body.position = end;
2330 Poll::Ready(Some(Ok(chunk)))
2331 }
2332 }
2333
2334 struct FailingBody {
2337 head: Option<Vec<u8>>,
2338 }
2339
2340 impl BodyStream for FailingBody {
2341 fn poll_next(
2342 self: Pin<&mut Self>,
2343 _context: &mut Context<'_>,
2344 ) -> Poll<Option<Result<Vec<u8>, BodyStreamError>>> {
2345 Poll::Ready(Some(match self.get_mut().head.take() {
2346 Some(head) => Ok(head),
2347 None => Err(BodyStreamError::new(
2348 "upload_timeout",
2349 "request body stalled past the configured deadline",
2350 )),
2351 }))
2352 }
2353 }
2354
2355 fn reader(bytes: Vec<u8>, chunk: usize) -> MultipartStream {
2356 MultipartStream::new(
2357 StreamingBody::new(SlicedBody {
2358 bytes,
2359 chunk,
2360 position: 0,
2361 }),
2362 &content_type(),
2363 )
2364 .expect("the declared content type carries a boundary")
2365 }
2366
2367 async fn read_all(
2369 stream: &mut MultipartStream,
2370 ) -> Result<Vec<(String, Vec<u8>)>, MultipartError> {
2371 let mut parts = Vec::new();
2372 while let Some(mut field) = stream.next_field().await? {
2373 let name = field.name().to_owned();
2374 let mut bytes = Vec::new();
2375 while let Some(chunk) = field.next_chunk().await? {
2376 bytes.extend_from_slice(chunk);
2377 }
2378 parts.push((name, bytes));
2379 }
2380 Ok(parts)
2381 }
2382
2383 #[test]
2384 fn a_streamed_document_yields_every_part_whatever_the_chunk_size() {
2385 let payload = vec![7_u8; 40_000];
2386 let document = multipart_document(&[
2387 ("title", None, None, b"A cover"),
2388 (
2389 "file",
2390 Some("cover.jpg"),
2391 Some("image/jpeg"),
2392 payload.as_slice(),
2393 ),
2394 ("note", None, None, b""),
2395 ]);
2396
2397 for chunk in [1, 2, 3, 7, 64, 8192, usize::MAX] {
2400 let mut stream = reader(document.clone(), chunk);
2401 let parts = block_on(read_all(&mut stream)).expect("the document parses");
2402 assert_eq!(parts.len(), 3, "chunk size {chunk}");
2403 assert_eq!(parts[0], ("title".to_owned(), b"A cover".to_vec()));
2404 assert_eq!(parts[1].0, "file");
2405 assert_eq!(parts[1].1, payload);
2406 assert_eq!(parts[2], ("note".to_owned(), Vec::new()));
2407 }
2408 }
2409
2410 #[test]
2411 fn part_metadata_survives_the_streaming_reader() {
2412 let document =
2413 multipart_document(&[("file", Some("cover.jpg"), Some("image/jpeg"), b"body")]);
2414 let mut stream = reader(document, 5);
2415 block_on(async {
2416 let field = stream
2417 .next_field()
2418 .await
2419 .expect("the document parses")
2420 .expect("one part");
2421 assert_eq!(field.name(), "file");
2422 assert_eq!(field.file_name(), Some("cover.jpg"));
2423 assert_eq!(field.content_type(), Some("image/jpeg"));
2424 });
2425 }
2426
2427 #[test]
2428 fn part_data_that_looks_like_a_boundary_is_still_data() {
2429 let mut data = Vec::new();
2430 data.extend_from_slice(b"\r\n--not-the-boundary\r\n");
2431 data.extend_from_slice(format!("\r\n--{BOUNDARY}x\r\n").as_bytes());
2432 data.extend_from_slice(format!("\r\n--{BOUNDARY}").as_bytes());
2433 data.extend_from_slice(b"tail\r\n");
2434 data.extend_from_slice(b"\r\n-\r\n--\r");
2435 let document = multipart_document(&[("file", None, None, &data)]);
2436
2437 for chunk in [1, 4, 17, 8192] {
2438 let mut stream = reader(document.clone(), chunk);
2439 let parts = block_on(read_all(&mut stream)).expect("the document parses");
2440 assert_eq!(parts.len(), 1, "chunk size {chunk}");
2441 assert_eq!(parts[0].1, data, "chunk size {chunk}");
2442 }
2443 }
2444
2445 #[test]
2446 fn a_large_upload_never_becomes_resident() {
2447 let payload = vec![9_u8; 5 * 1024 * 1024];
2450 let document =
2451 multipart_document(&[("file", Some("cover.jpg"), Some("image/jpeg"), &payload)]);
2452 let mut stream = reader(document, 8192);
2453
2454 let (bytes, peak) = block_on(async {
2455 let mut bytes = 0_usize;
2456 let mut peak = 0_usize;
2457 let mut field = stream
2458 .next_field()
2459 .await
2460 .expect("the document parses")
2461 .expect("one part");
2462 while let Some(chunk) = field.next_chunk().await.expect("a chunk") {
2463 bytes += chunk.len();
2464 peak = peak.max(field.stream.buffer.capacity());
2465 }
2466 (bytes, peak)
2467 });
2468
2469 assert_eq!(bytes, payload.len());
2470 assert!(
2472 peak < 64 * 1024,
2473 "a five-megabyte upload held {peak} buffered bytes"
2474 );
2475 }
2476
2477 #[test]
2478 fn a_skipped_part_does_not_disturb_the_next_one() {
2479 let document = multipart_document(&[
2480 ("file", None, None, &[3_u8; 5000]),
2481 ("note", None, None, b"kept"),
2482 ]);
2483 let mut stream = reader(document, 128);
2484 block_on(async {
2485 let first = stream
2486 .next_field()
2487 .await
2488 .expect("the document parses")
2489 .expect("a first part");
2490 assert_eq!(first.name(), "file");
2491 drop(first);
2492 let mut second = stream
2493 .next_field()
2494 .await
2495 .expect("the rest of the document parses")
2496 .expect("a second part");
2497 assert_eq!(second.name(), "note");
2498 let chunk = second
2499 .next_chunk()
2500 .await
2501 .expect("a chunk")
2502 .expect("the part has data");
2503 assert_eq!(chunk, b"kept");
2504 });
2505 }
2506
2507 #[test]
2508 fn a_document_with_no_parts_reads_as_empty() {
2509 let mut stream = reader(format!("--{BOUNDARY}--").into_bytes(), 3);
2510 let parts = block_on(read_all(&mut stream)).expect("an empty document parses");
2511 assert!(parts.is_empty());
2512 }
2513
2514 #[test]
2515 fn a_body_that_ends_before_its_closing_boundary_is_a_failure_not_a_short_read() {
2516 let mut truncated = multipart_document(&[("file", None, None, &[1_u8; 4096])]);
2517 truncated.truncate(2048);
2518 let mut stream = reader(truncated, 512);
2519 let error = block_on(read_all(&mut stream)).expect_err("a truncated body cannot succeed");
2520 assert_eq!(
2521 error,
2522 MultipartError::Malformed("multipart part has no closing boundary")
2523 );
2524 }
2525
2526 #[test]
2527 fn a_producer_failure_mid_body_is_reported_not_swallowed() {
2528 let mut document = multipart_document(&[("file", None, None, &[1_u8; 4096])]);
2529 document.truncate(1024);
2530 let mut stream = MultipartStream::new(
2531 StreamingBody::new(FailingBody {
2532 head: Some(document),
2533 }),
2534 &content_type(),
2535 )
2536 .expect("the declared content type carries a boundary");
2537
2538 let error = block_on(read_all(&mut stream)).expect_err("a failed producer cannot succeed");
2539 let MultipartError::Transport(transport) = &error else {
2540 panic!("expected a transport failure, got {error:?}");
2541 };
2542 assert_eq!(transport.code, "upload_timeout");
2543 assert_eq!(error.status(), 400);
2544 assert_eq!(error.code(), "upload_stream_failed");
2545 }
2546
2547 #[test]
2548 fn a_failed_document_cannot_be_resumed() {
2549 let mut stream = reader(b"not a multipart body at all".to_vec(), 4);
2550 block_on(async {
2551 let first = stream
2552 .next_field()
2553 .await
2554 .expect_err("the body is malformed");
2555 assert_eq!(
2556 first,
2557 MultipartError::Malformed(
2558 "multipart body does not start with its declared boundary"
2559 )
2560 );
2561 let second = stream
2562 .next_field()
2563 .await
2564 .expect_err("the reader stays failed");
2565 assert_eq!(
2566 second,
2567 MultipartError::Malformed("multipart body already failed to parse")
2568 );
2569 });
2570 }
2571
2572 #[test]
2573 fn a_malformed_document_projects_the_buffered_extractors_failure() {
2574 let failure = MultipartError::Malformed("multipart boundary is malformed")
2575 .into_failure()
2576 .expect("the failure projects");
2577 assert_eq!(failure.status, 422);
2578 assert_eq!(failure.code, "invalid_multipart");
2579 assert_eq!(
2580 failure.message,
2581 "request body is not valid multipart form data"
2582 );
2583 let details: blazingly_json::Value =
2584 blazingly_json::from_slice(&failure.details.expect("details")).expect("valid JSON");
2585 assert_eq!(
2586 details,
2587 blazingly_json::json!({
2588 "source": "multipart",
2589 "reason": "multipart boundary is malformed"
2590 })
2591 );
2592 }
2593
2594 #[test]
2595 fn a_content_type_without_a_usable_boundary_is_rejected() {
2596 for content_type in [
2597 "application/json",
2598 "multipart/form-data",
2599 "multipart/form-data; boundary=",
2600 "multipart/form-data; boundary=\"with space\"",
2601 ] {
2602 let error = MultipartStream::new(StreamingBody::once(Vec::new()), content_type)
2603 .err()
2604 .unwrap_or_else(|| panic!("{content_type} should not carry a boundary"));
2605 assert_eq!(
2606 error,
2607 MultipartError::Malformed("multipart boundary is missing or invalid")
2608 );
2609 }
2610 }
2611
2612 #[test]
2613 fn a_document_with_too_many_parts_is_rejected() {
2614 let empty = Vec::new();
2615 let parts = (0..=MAX_MULTIPART_PARTS)
2616 .map(|_| ("field", None, None, empty.as_slice()))
2617 .collect::<Vec<_>>();
2618 let mut stream = reader(multipart_document(&parts), 512);
2619 let error = block_on(read_all(&mut stream)).expect_err("the part count is bounded");
2620 assert_eq!(
2621 error,
2622 MultipartError::Malformed("multipart body contains too many parts")
2623 );
2624 }
2625
2626 #[test]
2627 fn an_oversized_part_header_block_is_rejected() {
2628 let mut document = format!("--{BOUNDARY}\r\n").into_bytes();
2629 document.extend_from_slice(b"Content-Disposition: form-data; name=\"file\"\r\n");
2630 document.extend_from_slice(b"X-Padding: ");
2631 document.extend_from_slice(&vec![b'p'; 32 * 1024]);
2632 document.extend_from_slice(b"\r\n\r\ndata\r\n");
2633 document.extend_from_slice(format!("--{BOUNDARY}--\r\n").as_bytes());
2634
2635 let mut stream = reader(document, 4096);
2636 let error = block_on(read_all(&mut stream)).expect_err("the header block is bounded");
2637 assert_eq!(
2638 error,
2639 MultipartError::Malformed("multipart part headers exceed the configured limit")
2640 );
2641 }
2642
2643 #[test]
2644 fn a_field_can_still_be_buffered_deliberately_with_a_limit() {
2645 let document = multipart_document(&[
2646 ("title", None, None, "A cover".as_bytes()),
2647 ("file", Some("c.png"), Some("image/png"), &[4_u8; 300]),
2648 ]);
2649 let mut stream = reader(document, 37);
2650 block_on(async {
2651 let title = stream
2652 .next_field()
2653 .await
2654 .expect("the document parses")
2655 .expect("a first part");
2656 assert_eq!(title.text(64).await.expect("the text fits"), "A cover");
2657
2658 let file = stream
2659 .next_field()
2660 .await
2661 .expect("the document parses")
2662 .expect("a second part");
2663 let upload = file.into_upload(1024).await.expect("the upload fits");
2664 assert_eq!(
2665 upload,
2666 UploadFile::new("file", vec![4_u8; 300])
2667 .with_file_name("c.png")
2668 .with_content_type("image/png")
2669 );
2670 });
2671 }
2672
2673 #[test]
2674 fn deliberate_buffering_still_honours_the_limit_it_was_given() {
2675 let document = multipart_document(&[("file", None, None, &[4_u8; 300])]);
2676 let mut stream = reader(document, 64);
2677 let error = block_on(async {
2678 let field = stream
2679 .next_field()
2680 .await
2681 .expect("the document parses")
2682 .expect("one part");
2683 field.collect(128).await.expect_err("the part is too large")
2684 });
2685 assert_eq!(error, MultipartError::TooLarge { limit: 128 });
2686 assert_eq!(error.status(), 413);
2687 }
2688
2689 struct DocumentedPage;
2690
2691 impl ApiSchema for DocumentedPage {
2692 fn type_descriptor() -> TypeDescriptor {
2693 TypeDescriptor::scalar("DocumentedPage", SchemaKind::Object)
2694 }
2695 }
2696
2697 struct HintProbe;
2698
2699 #[test]
2700 fn prepared_json_encodes_a_borrowed_view() {
2701 let owned = [String::from("alpha"), String::from("beta")];
2702 let borrowed: Vec<&str> = owned.iter().map(String::as_str).collect();
2703 let body = PreparedJson::<DocumentedPage>::encode(&borrowed).expect("the view encodes");
2704 assert_eq!(body.as_bytes(), br#"["alpha","beta"]"#);
2705 assert_eq!(body.len(), 16);
2706 assert!(!body.is_empty());
2707 }
2708
2709 #[test]
2710 fn prepared_json_reports_the_declared_schema_not_its_bytes() {
2711 assert_eq!(
2712 PreparedJson::<DocumentedPage>::type_descriptor(),
2713 DocumentedPage::type_descriptor()
2714 );
2715 }
2716
2717 #[test]
2718 fn prepared_json_carries_adopted_bytes_verbatim() {
2719 let body =
2720 PreparedJson::<DocumentedPage>::from_bytes(b"{\"already\":\"encoded\"}".to_vec());
2721 assert_eq!(body.into_bytes(), b"{\"already\":\"encoded\"}".to_vec());
2722 }
2723
2724 #[test]
2725 fn an_unseen_response_shape_reserves_the_floor() {
2726 assert_eq!(response_size_hint::<HintProbe>(), MIN_RESPONSE_HINT);
2727 }
2728
2729 #[test]
2730 fn a_recorded_response_shape_reserves_headroom() {
2731 record_response_size::<(u8, HintProbe)>(8192);
2732 let hint = response_size_hint::<(u8, HintProbe)>();
2733 assert!(hint > 8192, "the hint should leave room to grow: {hint}");
2734 assert!(hint <= MAX_RESPONSE_HINT);
2735 }
2736
2737 #[test]
2738 fn an_outsized_response_cannot_pin_the_hint_above_the_ceiling() {
2739 record_response_size::<(u16, HintProbe)>(usize::MAX);
2740 assert_eq!(response_size_hint::<(u16, HintProbe)>(), MAX_RESPONSE_HINT);
2741 }
2742
2743 #[test]
2744 fn a_parked_upload_leaves_only_a_token_in_the_document() {
2745 let slots = UploadSlots::acquire();
2746 let token = slots.park(
2747 UploadFile::new("cover", vec![7; 1 << 20])
2748 .with_file_name("cover.png")
2749 .with_content_type("image/png"),
2750 );
2751
2752 let encoded = blazingly_json::to_string(&token).expect("the token encodes");
2753 assert!(
2754 encoded.len() < 64,
2755 "a megabyte of upload left {} bytes in the document: {encoded}",
2756 encoded.len()
2757 );
2758
2759 let decoded: UploadFile = blazingly_json::from_value(token).expect("the token resolves");
2760 assert_eq!(decoded.field_name, "cover");
2761 assert_eq!(decoded.file_name.as_deref(), Some("cover.png"));
2762 assert_eq!(decoded.content_type.as_deref(), Some("image/png"));
2763 assert_eq!(decoded.bytes.len(), 1 << 20);
2764 assert!(decoded.bytes.iter().all(|byte| *byte == 7));
2765 }
2766
2767 #[test]
2768 fn an_upload_slot_does_not_outlive_its_extraction() {
2769 let token = {
2770 let slots = UploadSlots::acquire();
2771 slots.park(UploadFile::new("gone", vec![1, 2, 3]))
2772 };
2773 let error = blazingly_json::from_value::<UploadFile>(token)
2774 .expect_err("a released slot cannot be resolved");
2775 assert!(error.to_string().contains("no longer available"), "{error}");
2776 }
2777
2778 #[test]
2779 fn an_upload_slot_can_only_be_taken_once() {
2780 let slots = UploadSlots::acquire();
2781 let token = slots.park(UploadFile::new("once", vec![9]));
2782 let first: UploadFile =
2783 blazingly_json::from_value(token.clone()).expect("the first take resolves");
2784 assert_eq!(first.bytes, vec![9]);
2785 assert!(blazingly_json::from_value::<UploadFile>(token).is_err());
2786 }
2787
2788 #[test]
2789 fn two_extractions_on_one_thread_cannot_see_each_others_slots() {
2790 let outer = UploadSlots::acquire();
2791 let outer_token = outer.park(UploadFile::new("outer", vec![1]));
2792 let inner_token = {
2793 let inner = UploadSlots::acquire();
2794 inner.park(UploadFile::new("inner", vec![2]))
2795 };
2796
2797 assert!(blazingly_json::from_value::<UploadFile>(inner_token).is_err());
2798 let resolved: UploadFile =
2799 blazingly_json::from_value(outer_token).expect("the outer slot survives");
2800 assert_eq!(resolved.field_name, "outer");
2801 }
2802
2803 #[test]
2804 fn the_object_form_an_mcp_client_sends_still_decodes() {
2805 let value = blazingly_json::json!({
2806 "field_name": "avatar",
2807 "file_name": "a.png",
2808 "content_type": "image/png",
2809 "bytes": [1, 2, 3]
2810 });
2811 let upload: UploadFile =
2812 blazingly_json::from_value(value).expect("the object form decodes");
2813 assert_eq!(
2814 upload,
2815 UploadFile::new("avatar", vec![1, 2, 3])
2816 .with_file_name("a.png")
2817 .with_content_type("image/png")
2818 );
2819 }
2820
2821 #[test]
2822 fn an_upload_survives_its_own_serialized_form() {
2823 let upload = UploadFile::new("report", vec![4, 5, 6]).with_file_name("r.bin");
2824 let encoded = blazingly_json::to_value(&upload).expect("the upload encodes");
2825 let decoded: UploadFile = blazingly_json::from_value(encoded).expect("the upload decodes");
2826 assert_eq!(decoded, upload);
2827 }
2828
2829 #[test]
2830 fn the_object_form_defaults_metadata_and_still_demands_the_rest() {
2831 let minimal: UploadFile =
2832 blazingly_json::from_value(blazingly_json::json!({"field_name": "a", "bytes": []}))
2833 .expect("optional metadata may be absent");
2834 assert!(minimal.file_name.is_none());
2835 assert!(minimal.content_type.is_none());
2836
2837 let error =
2838 blazingly_json::from_value::<UploadFile>(blazingly_json::json!({"field_name": "a"}))
2839 .expect_err("bytes are required");
2840 assert!(error.to_string().contains("bytes"), "{error}");
2841 }
2842
2843 fn violations(field: &str, nested: &ValidationErrors) -> Vec<String> {
2844 let mut merged = ValidationErrors::new();
2845 merge_field_validation_errors(&mut merged, field, nested);
2846 merged
2847 .violations()
2848 .iter()
2849 .map(|violation| violation.field.clone())
2850 .collect()
2851 }
2852
2853 #[test]
2854 fn a_field_validator_that_names_its_own_field_is_not_doubled() {
2855 let mut reported = ValidationErrors::new();
2856 reported.push("published_at", "too_far_ahead", "too far ahead");
2857 assert_eq!(violations("published_at", &reported), ["published_at"]);
2858 }
2859
2860 #[test]
2861 fn a_field_validator_may_still_report_a_path_inside_the_value() {
2862 let mut reported = ValidationErrors::new();
2863 reported.push("", "invalid", "invalid");
2864 reported.push("window.end", "invalid", "invalid");
2865 reported.push("slots[0]", "invalid", "invalid");
2866 reported.push("end", "invalid", "invalid");
2867 assert_eq!(
2868 violations("window", &reported),
2869 ["window", "window.end", "window.slots[0]", "window.end"]
2870 );
2871 }
2872
2873 #[test]
2874 fn an_unnamed_field_leaves_the_reported_path_alone() {
2875 let mut reported = ValidationErrors::new();
2876 reported.push("street", "min_length", "too short");
2877 assert_eq!(violations("", &reported), ["street"]);
2878 }
2879
2880 #[test]
2881 fn field_metadata_round_trips_through_its_encoding() {
2882 for metadata in [
2883 FieldMetadata::Default(blazingly_json::json!(20)),
2884 FieldMetadata::Default(blazingly_json::json!("draft")),
2885 FieldMetadata::Default(blazingly_json::json!(true)),
2886 FieldMetadata::Nullable,
2887 FieldMetadata::Enumeration(vec!["uk".to_owned(), "ru".to_owned()]),
2888 ] {
2889 let encoded = metadata.to_string();
2890 assert_eq!(
2891 FieldMetadata::parse(&encoded),
2892 Some(metadata.clone()),
2893 "{encoded} did not round trip"
2894 );
2895 }
2896
2897 assert_eq!(FieldMetadata::parse("min_items=2"), None);
2898 assert_eq!(FieldMetadata::parse("validate_code"), None);
2899 assert_eq!(FieldMetadata::parse("nullable=false"), None);
2900 }
2901
2902 #[test]
2903 fn field_metadata_projects_json_schema_keywords() {
2904 let mut schema = blazingly_json::json!({ "type": "integer" });
2905 FieldMetadata::Default(blazingly_json::json!(20)).apply_json_schema(&mut schema);
2906 FieldMetadata::Nullable.apply_json_schema(&mut schema);
2907 FieldMetadata::Enumeration(vec!["uk".to_owned()]).apply_json_schema(&mut schema);
2908
2909 assert_eq!(schema["default"], blazingly_json::json!(20));
2910 assert_eq!(schema["nullable"], blazingly_json::json!(true));
2911 assert_eq!(schema["enum"], blazingly_json::json!(["uk"]));
2912 }
2913
2914 fn operation(id: &str, method: HttpMethod, path: &str) -> OperationDescriptor {
2915 OperationDescriptor::new(
2916 method,
2917 path,
2918 id,
2919 id,
2920 None,
2921 vec![ResponseDescriptor::success(
2922 200,
2923 Some(TypeDescriptor::new("Output")),
2924 )],
2925 )
2926 .expect("test operation id should be valid")
2927 }
2928
2929 #[test]
2930 fn app_rejects_duplicate_operation_ids() {
2931 let result = App::new()
2932 .route(operation("users.read", HttpMethod::Get, "/users/1"))
2933 .route(operation("users.read", HttpMethod::Get, "/users/2"))
2934 .build();
2935
2936 assert!(matches!(result, Err(BuildError::DuplicateOperationId(_))));
2937 }
2938
2939 #[test]
2940 fn app_rejects_duplicate_http_bindings() {
2941 let result = App::new()
2942 .route(operation("users.read", HttpMethod::Get, "/users"))
2943 .route(operation("users.list", HttpMethod::Get, "/users"))
2944 .build();
2945
2946 assert!(matches!(
2947 result,
2948 Err(BuildError::DuplicateHttpBinding { .. })
2949 ));
2950 }
2951
2952 #[test]
2953 fn app_rejects_parameter_routes_with_the_same_shape() {
2954 let result = App::new()
2955 .route(
2956 operation("users.by_id", HttpMethod::Get, "/users/{user_id}").with_inputs(vec![
2957 InputDescriptor::new(
2958 "user_id",
2959 InputSource::Path,
2960 true,
2961 TypeDescriptor::new("u64"),
2962 ),
2963 ]),
2964 )
2965 .route(
2966 operation("users.by_name", HttpMethod::Get, "/users/{user_name}").with_inputs(
2967 vec![InputDescriptor::new(
2968 "user_name",
2969 InputSource::Path,
2970 true,
2971 TypeDescriptor::new("String"),
2972 )],
2973 ),
2974 )
2975 .build();
2976
2977 assert!(matches!(
2978 result,
2979 Err(BuildError::AmbiguousHttpBinding { .. })
2980 ));
2981 }
2982
2983 #[test]
2984 fn app_order_is_deterministic() {
2985 let app = App::new()
2986 .route(operation("users.create", HttpMethod::Post, "/users"))
2987 .route(operation("health.read", HttpMethod::Get, "/health"))
2988 .build()
2989 .expect("application should be valid");
2990
2991 let ids: Vec<_> = app
2992 .operations()
2993 .iter()
2994 .map(|operation| operation.contract.id.as_str())
2995 .collect();
2996 assert_eq!(ids, ["health.read", "users.create"]);
2997 }
2998
2999 #[test]
3000 fn app_validates_and_orders_operation_security() {
3001 let secured = operation("users.write", HttpMethod::Put, "/users").with_security(vec![
3002 SecurityRequirement::new("oauth").with_scopes(vec!["users:write".to_owned()]),
3003 ]);
3004 let app = App::new()
3005 .route(secured)
3006 .security_scheme(SecuritySchemeDescriptor::new(
3007 "oauth",
3008 SecuritySchemeKind::OAuth2 {
3009 authorization_url: Some("https://auth.example/authorize".to_owned()),
3010 token_url: Some("https://auth.example/token".to_owned()),
3011 scopes: vec!["users:read".to_owned(), "users:write".to_owned()],
3012 },
3013 ))
3014 .build()
3015 .expect("registered security requirements should compile");
3016
3017 assert_eq!(app.security_schemes()[0].name, "oauth");
3018 assert_eq!(
3019 app.operations()[0].contract.security[0].scopes,
3020 ["users:write"]
3021 );
3022 }
3023
3024 #[test]
3025 fn app_rejects_unknown_security_schemes_and_scopes() {
3026 let unknown_scheme = App::new()
3027 .route(
3028 operation("users.read", HttpMethod::Get, "/users")
3029 .with_security(vec![SecurityRequirement::new("missing")]),
3030 )
3031 .build();
3032 assert!(matches!(
3033 unknown_scheme,
3034 Err(BuildError::UnknownSecurityScheme { .. })
3035 ));
3036
3037 let unknown_scope = App::new()
3038 .route(
3039 operation("users.read", HttpMethod::Get, "/users").with_security(vec![
3040 SecurityRequirement::new("oauth").with_scopes(vec!["users:write".to_owned()]),
3041 ]),
3042 )
3043 .security_scheme(SecuritySchemeDescriptor::new(
3044 "oauth",
3045 SecuritySchemeKind::OAuth2 {
3046 authorization_url: None,
3047 token_url: Some("https://auth.example/token".to_owned()),
3048 scopes: vec!["users:read".to_owned()],
3049 },
3050 ))
3051 .build();
3052 assert!(matches!(
3053 unknown_scope,
3054 Err(BuildError::UnknownSecurityScope { .. })
3055 ));
3056 }
3057}