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
22const RESPONSE_HINT_SLOTS: usize = 32;
28const MIN_RESPONSE_HINT: usize = 128;
30const MAX_RESPONSE_HINT: usize = 1 << 20;
33
34thread_local! {
35 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
46fn 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#[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
73pub 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#[derive(Clone, Debug, Eq, PartialEq)]
90pub struct Json<T>(pub T);
91
92#[derive(Clone, Debug, Eq, PartialEq)]
94pub struct Path<T>(pub T);
95
96#[derive(Clone, Debug, Eq, PartialEq)]
98pub struct Query<T>(pub T);
99
100#[derive(Clone, Debug, Eq, PartialEq)]
102pub struct Header<T>(pub T);
103
104#[derive(Clone, Debug, Eq, PartialEq)]
106pub struct Cookie<T>(pub T);
107
108#[derive(Clone, Debug, Eq, PartialEq)]
110pub struct Form<T>(pub T);
111
112#[derive(Clone, Debug, Eq, PartialEq)]
114pub struct Multipart<T>(pub T);
115
116#[derive(Clone, Debug, Eq, PartialEq)]
118pub struct File<T>(pub T);
119
120#[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
175const UPLOAD_SLOT_KEY: &str = "$blazingly::upload";
185
186thread_local! {
187 static UPLOAD_SLOTS: std::cell::RefCell<Vec<Option<UploadFile>>> =
202 const { std::cell::RefCell::new(Vec::new()) };
203}
204
205#[doc(hidden)]
215#[derive(Debug)]
216pub struct UploadSlots {
217 base: usize,
218 thread_bound: PhantomData<*const ()>,
221}
222
223impl UploadSlots {
224 #[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 #[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
259fn take_parked_upload(index: usize) -> Option<UploadFile> {
265 UPLOAD_SLOTS.with_borrow_mut(|slots| slots.get_mut(index).and_then(Option::take))
266}
267
268enum 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 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
382pub struct PreparedJson<T> {
433 body: Vec<u8>,
434 schema: PhantomData<fn() -> T>,
435}
436
437impl<T> PreparedJson<T> {
438 #[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 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#[derive(Clone, Debug, Eq, PartialEq)]
506pub struct Created<T>(pub T);
507
508#[derive(Clone, Debug, Eq, PartialEq)]
510pub struct Accepted<T>(pub T);
511
512#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
514pub struct NoContent;
515
516#[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
544pub 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 fn recycle(self: Pin<&mut Self>, spent: Vec<u8>) {
563 drop(spent);
564 }
565}
566
567pub 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 #[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 #[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 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 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#[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
674pub 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
693pub 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 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#[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
788pub 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 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#[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
875pub 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
885pub 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
901pub 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
930fn 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
936pub trait ApiConstrained: ApiSchema {
944 fn constraint_rules() -> Vec<ValidationRule>;
946
947 fn validate_constraints(&self) -> Result<(), ValidationErrors>;
954}
955
956#[derive(Clone, Debug, PartialEq)]
965pub enum FieldMetadata {
966 Default(blazingly_json::Value),
968 Nullable,
970 Enumeration(Vec<String>),
972}
973
974impl FieldMetadata {
975 #[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 #[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 #[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 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
1042pub const MAX_MULTIPART_HEADER_BYTES: usize = 16 * 1024;
1051
1052pub const MAX_MULTIPART_PARTS: usize = 256;
1054
1055#[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#[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#[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#[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#[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#[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 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#[derive(Clone, Debug, Eq, PartialEq)]
1223pub enum MultipartError {
1224 Malformed(&'static str),
1230 TooLarge { limit: usize },
1233 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 #[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 #[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 #[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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1342enum MultipartState {
1343 Opening,
1345 Headers,
1347 Part,
1349 Done,
1351 Failed,
1353}
1354
1355enum MultipartScan {
1357 Terminator(usize),
1359 Data(usize),
1365}
1366
1367#[derive(Debug)]
1399pub struct MultipartStream {
1400 body: StreamingBody,
1401 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 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 #[must_use]
1438 pub const fn bytes_read(&self) -> u64 {
1439 self.bytes_read
1440 }
1441
1442 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 fn fail(&mut self, reason: &'static str) -> MultipartError {
1479 self.state = MultipartState::Failed;
1480 MultipartError::Malformed(reason)
1481 }
1482
1483 const fn available(&self) -> usize {
1485 self.buffer.len() - self.cursor
1486 }
1487
1488 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 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 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 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 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 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 async fn skip_part(&mut self) -> Result<(), MultipartError> {
1586 while self.advance_part().await?.is_some() {}
1587 Ok(())
1588 }
1589
1590 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#[derive(Debug)]
1630pub struct MultipartField<'stream> {
1631 stream: &'stream mut MultipartStream,
1632 headers: MultipartPartHeaders,
1633}
1634
1635impl MultipartField<'_> {
1636 #[must_use]
1638 pub fn name(&self) -> &str {
1639 &self.headers.name
1640 }
1641
1642 #[must_use]
1644 pub fn file_name(&self) -> Option<&str> {
1645 self.headers.file_name.as_deref()
1646 }
1647
1648 #[must_use]
1650 pub fn content_type(&self) -> Option<&str> {
1651 self.headers.content_type.as_deref()
1652 }
1653
1654 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 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 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 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
1725fn 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 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 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#[derive(Clone, Debug, Eq, PartialEq)]
1773pub struct Status<const STATUS: u16, T>(pub T);
1774
1775#[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
1795pub 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#[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 Self::Connect => "x-blazingly-connect",
1852 }
1853 }
1854}
1855
1856#[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#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1875pub struct OperationDescriptor {
1876 pub contract: OperationContract,
1877 pub http: HttpBinding,
1878}
1879
1880impl OperationDescriptor {
1881 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#[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#[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 #[must_use]
1980 pub fn security_scheme(mut self, scheme: SecuritySchemeDescriptor) -> Self {
1981 self.security_schemes.push(scheme);
1982 self
1983 }
1984
1985 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#[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 type TestPart<'data> = (
2277 &'data str,
2278 Option<&'data str>,
2279 Option<&'data str>,
2280 &'data [u8],
2281 );
2282
2283 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 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 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 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 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 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 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}