|
29 | 29 | //! - **Layer 3 (drasi-lib):** Returns `DrasiError` which is converted to |
30 | 30 | //! `ErrorResponse` via `From<DrasiError>` with proper status code mapping. |
31 | 31 |
|
32 | | -use axum::async_trait; |
33 | | -use axum::body::Bytes; |
34 | | -use axum::extract::FromRequest; |
35 | | -use axum::http::header::CONTENT_TYPE; |
36 | 32 | use axum::http::StatusCode; |
37 | 33 | use axum::response::IntoResponse; |
38 | 34 | use drasi_lib::DrasiError; |
39 | | -use serde::{de::DeserializeOwned, Serialize}; |
| 35 | +use serde::Serialize; |
40 | 36 | use utoipa::ToSchema; |
41 | 37 |
|
42 | | -/// A request-body extractor that accepts both JSON and YAML payloads. |
43 | | -/// |
44 | | -/// The body format is selected from the request's `Content-Type` header: |
45 | | -/// YAML media types (`application/yaml`, `application/x-yaml`, `text/yaml`, |
46 | | -/// `text/x-yaml`, `text/vnd.yaml`) are parsed with `serde_yaml`; everything |
47 | | -/// else (including a missing `Content-Type`) defaults to JSON. This lets every |
48 | | -/// HTTP route on the API accept JSON and YAML interchangeably. |
49 | | -/// |
50 | | -/// On failure it returns a structured `ErrorResponse` with the serde error |
51 | | -/// details included (HTTP 400 via the `INVALID_REQUEST` code). |
52 | | -#[derive(Debug, Clone, Copy, Default)] |
53 | | -pub struct ConfigBody<T>(pub T); |
54 | | - |
55 | | -/// Returns `true` when the supplied `Content-Type` value denotes a YAML media type. |
56 | | -fn is_yaml_content_type(content_type: &str) -> bool { |
57 | | - // Ignore any parameters (e.g. "; charset=utf-8") and surrounding whitespace. |
58 | | - let essence = content_type |
59 | | - .split(';') |
60 | | - .next() |
61 | | - .unwrap_or("") |
62 | | - .trim() |
63 | | - .to_ascii_lowercase(); |
64 | | - matches!( |
65 | | - essence.as_str(), |
66 | | - "application/yaml" |
67 | | - | "application/x-yaml" |
68 | | - | "text/yaml" |
69 | | - | "text/x-yaml" |
70 | | - | "text/vnd.yaml" |
71 | | - ) |
72 | | -} |
73 | | - |
74 | | -#[async_trait] |
75 | | -impl<T, S> FromRequest<S> for ConfigBody<T> |
76 | | -where |
77 | | - T: DeserializeOwned, |
78 | | - S: Send + Sync, |
79 | | -{ |
80 | | - type Rejection = ErrorResponse; |
81 | | - |
82 | | - async fn from_request( |
83 | | - req: axum::http::Request<axum::body::Body>, |
84 | | - state: &S, |
85 | | - ) -> Result<Self, Self::Rejection> { |
86 | | - let is_yaml = req |
87 | | - .headers() |
88 | | - .get(CONTENT_TYPE) |
89 | | - .and_then(|value| value.to_str().ok()) |
90 | | - .map(is_yaml_content_type) |
91 | | - .unwrap_or(false); |
92 | | - |
93 | | - let bytes = Bytes::from_request(req, state).await.map_err(|rejection| { |
94 | | - log::debug!("Failed to read request body: {}", rejection.body_text()); |
95 | | - ErrorResponse::new( |
96 | | - error_codes::INVALID_REQUEST, |
97 | | - "Failed to read request body".to_string(), |
98 | | - ) |
99 | | - })?; |
100 | | - |
101 | | - if is_yaml { |
102 | | - serde_yaml::from_slice(&bytes).map(ConfigBody).map_err(|e| { |
103 | | - log::debug!("YAML extraction failed: {e}"); |
104 | | - ErrorResponse::new( |
105 | | - error_codes::INVALID_REQUEST, |
106 | | - format!("Failed to parse YAML request body: {e}"), |
107 | | - ) |
108 | | - }) |
109 | | - } else { |
110 | | - serde_json::from_slice(&bytes).map(ConfigBody).map_err(|e| { |
111 | | - log::debug!("JSON extraction failed: {e}"); |
112 | | - ErrorResponse::new( |
113 | | - error_codes::INVALID_REQUEST, |
114 | | - format!("Failed to parse JSON request body: {e}"), |
115 | | - ) |
116 | | - }) |
117 | | - } |
118 | | - } |
119 | | -} |
120 | | - |
121 | 38 | /// Error codes for API responses |
122 | 39 | pub mod error_codes { |
123 | 40 | pub const SOURCE_CREATE_FAILED: &str = "SOURCE_CREATE_FAILED"; |
|
0 commit comments