rustapi_toon/
extractor.rs

1//! TOON extractor and response types
2
3use crate::error::ToonError;
4use crate::{TOON_CONTENT_TYPE, TOON_CONTENT_TYPE_TEXT};
5use bytes::Bytes;
6use http::{header, StatusCode};
7use http_body_util::Full;
8use rustapi_core::{ApiError, FromRequest, IntoResponse, Request, Response, Result};
9use rustapi_openapi::{
10    MediaType, Operation, OperationModifier, ResponseModifier, ResponseSpec, SchemaRef,
11};
12use serde::de::DeserializeOwned;
13use serde::Serialize;
14use std::collections::HashMap;
15use std::ops::{Deref, DerefMut};
16
17/// TOON body extractor and response type
18///
19/// This extractor parses TOON-formatted request bodies and deserializes
20/// them into the specified type. It can also be used as a response type
21/// to serialize data into TOON format.
22///
23/// # Request Extraction
24///
25/// Accepts request bodies with content types:
26/// - `application/toon`
27/// - `text/toon`
28///
29/// # Example - Extractor
30///
31/// ```rust,ignore
32/// use rustapi_rs::prelude::*;
33/// use rustapi_rs::toon::Toon;
34///
35/// #[derive(Deserialize)]
36/// struct CreateUser {
37///     name: String,
38///     email: String,
39/// }
40///
41/// async fn create_user(Toon(user): Toon<CreateUser>) -> impl IntoResponse {
42///     // user is parsed from TOON format
43///     Json(user)
44/// }
45/// ```
46///
47/// # Example - Response
48///
49/// ```rust,ignore
50/// use rustapi_rs::prelude::*;
51/// use rustapi_rs::toon::Toon;
52///
53/// #[derive(Serialize)]
54/// struct User {
55///     id: u64,
56///     name: String,
57/// }
58///
59/// async fn get_user() -> Toon<User> {
60///     Toon(User {
61///         id: 1,
62///         name: "Alice".to_string(),
63///     })
64/// }
65/// ```
66#[derive(Debug, Clone, Copy, Default)]
67pub struct Toon<T>(pub T);
68
69impl<T: DeserializeOwned + Send> FromRequest for Toon<T> {
70    async fn from_request(req: &mut Request) -> Result<Self> {
71        // Check content type (optional - if provided, must be toon)
72        if let Some(content_type) = req.headers().get(header::CONTENT_TYPE) {
73            let content_type_str = content_type.to_str().unwrap_or("");
74            let is_toon = content_type_str.starts_with(TOON_CONTENT_TYPE)
75                || content_type_str.starts_with(TOON_CONTENT_TYPE_TEXT);
76
77            if !is_toon && !content_type_str.is_empty() {
78                return Err(ToonError::InvalidContentType.into());
79            }
80        }
81
82        // Get body bytes
83        let body = req
84            .take_body()
85            .ok_or_else(|| ApiError::internal("Body already consumed"))?;
86
87        if body.is_empty() {
88            return Err(ToonError::EmptyBody.into());
89        }
90
91        // Parse TOON
92        let body_str =
93            std::str::from_utf8(&body).map_err(|e| ApiError::bad_request(e.to_string()))?;
94
95        let value: T =
96            toon_format::decode_default(body_str).map_err(|e| ToonError::Decode(e.to_string()))?;
97
98        Ok(Toon(value))
99    }
100}
101
102impl<T> Deref for Toon<T> {
103    type Target = T;
104
105    fn deref(&self) -> &Self::Target {
106        &self.0
107    }
108}
109
110impl<T> DerefMut for Toon<T> {
111    fn deref_mut(&mut self) -> &mut Self::Target {
112        &mut self.0
113    }
114}
115
116impl<T> From<T> for Toon<T> {
117    fn from(value: T) -> Self {
118        Toon(value)
119    }
120}
121
122impl<T: Serialize> IntoResponse for Toon<T> {
123    fn into_response(self) -> Response {
124        match toon_format::encode_default(&self.0) {
125            Ok(body) => http::Response::builder()
126                .status(StatusCode::OK)
127                .header(header::CONTENT_TYPE, TOON_CONTENT_TYPE)
128                .body(Full::new(Bytes::from(body)))
129                .unwrap(),
130            Err(err) => {
131                let error: ApiError = ToonError::Encode(err.to_string()).into();
132                error.into_response()
133            }
134        }
135    }
136}
137
138// OpenAPI support: OperationModifier for Toon extractor
139impl<T: Send> OperationModifier for Toon<T> {
140    fn update_operation(op: &mut Operation) {
141        let mut content = HashMap::new();
142        content.insert(
143            TOON_CONTENT_TYPE.to_string(),
144            MediaType {
145                schema: SchemaRef::Inline(serde_json::json!({
146                    "type": "string",
147                    "description": "TOON (Token-Oriented Object Notation) formatted request body"
148                })),
149            },
150        );
151
152        op.request_body = Some(rustapi_openapi::RequestBody {
153            required: true,
154            content,
155        });
156    }
157}
158
159// OpenAPI support: ResponseModifier for Toon response
160impl<T: Serialize> ResponseModifier for Toon<T> {
161    fn update_response(op: &mut Operation) {
162        let mut content = HashMap::new();
163        content.insert(
164            TOON_CONTENT_TYPE.to_string(),
165            MediaType {
166                schema: SchemaRef::Inline(serde_json::json!({
167                    "type": "string",
168                    "description": "TOON (Token-Oriented Object Notation) formatted response"
169                })),
170            },
171        );
172
173        let response = ResponseSpec {
174            description: "TOON formatted response - token-optimized for LLMs".to_string(),
175            content: Some(content),
176        };
177        op.responses.insert("200".to_string(), response);
178    }
179}
180
181#[cfg(test)]
182mod tests {
183    use serde::{Deserialize, Serialize};
184
185    #[derive(Debug, Serialize, Deserialize, PartialEq)]
186    struct User {
187        name: String,
188        age: u32,
189    }
190
191    #[test]
192    fn test_toon_encode() {
193        let user = User {
194            name: "Alice".to_string(),
195            age: 30,
196        };
197
198        let toon_str = toon_format::encode_default(&user).unwrap();
199        assert!(toon_str.contains("name:"));
200        assert!(toon_str.contains("Alice"));
201        assert!(toon_str.contains("age:"));
202        assert!(toon_str.contains("30"));
203    }
204
205    #[test]
206    fn test_toon_decode() {
207        let toon_str = "name: Alice\nage: 30";
208        let user: User = toon_format::decode_default(toon_str).unwrap();
209
210        assert_eq!(user.name, "Alice");
211        assert_eq!(user.age, 30);
212    }
213
214    #[test]
215    fn test_toon_roundtrip() {
216        let original = User {
217            name: "Bob".to_string(),
218            age: 25,
219        };
220
221        let encoded = toon_format::encode_default(&original).unwrap();
222        let decoded: User = toon_format::decode_default(&encoded).unwrap();
223
224        assert_eq!(original, decoded);
225    }
226}