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