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