Skip to main content

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::BTreeMap;
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 = BTreeMap::new();
140        content.insert(
141            TOON_CONTENT_TYPE.to_string(),
142            MediaType {
143                schema: Some(SchemaRef::Inline(serde_json::json!({
144                    "type": "string",
145                    "description": "TOON (Token-Oriented Object Notation) formatted request body"
146                }))),
147                example: None,
148            },
149        );
150
151        op.request_body = Some(rustapi_openapi::RequestBody {
152            description: None,
153            required: Some(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 = BTreeMap::new();
163        content.insert(
164            TOON_CONTENT_TYPE.to_string(),
165            MediaType {
166                schema: Some(SchemaRef::Inline(serde_json::json!({
167                    "type": "string",
168                    "description": "TOON (Token-Oriented Object Notation) formatted response"
169                }))),
170                example: None,
171            },
172        );
173
174        let response = ResponseSpec {
175            description: "TOON formatted response - token-optimized for LLMs".to_string(),
176            content,
177            headers: BTreeMap::new(),
178        };
179        op.responses.insert("200".to_string(), response);
180    }
181}
182
183#[cfg(test)]
184mod tests {
185    use serde::{Deserialize, Serialize};
186
187    #[derive(Debug, Serialize, Deserialize, PartialEq)]
188    struct User {
189        name: String,
190        age: u32,
191    }
192
193    #[test]
194    fn test_toon_encode() {
195        let user = User {
196            name: "Alice".to_string(),
197            age: 30,
198        };
199
200        let toon_str = toon_format::encode_default(&user).unwrap();
201        assert!(toon_str.contains("name:"));
202        assert!(toon_str.contains("Alice"));
203        assert!(toon_str.contains("age:"));
204        assert!(toon_str.contains("30"));
205    }
206
207    #[test]
208    fn test_toon_decode() {
209        let toon_str = "name: Alice\nage: 30";
210        let user: User = toon_format::decode_default(toon_str).unwrap();
211
212        assert_eq!(user.name, "Alice");
213        assert_eq!(user.age, 30);
214    }
215
216    #[test]
217    fn test_toon_roundtrip() {
218        let original = User {
219            name: "Bob".to_string(),
220            age: 25,
221        };
222
223        let encoded = toon_format::encode_default(&original).unwrap();
224        let decoded: User = toon_format::decode_default(&encoded).unwrap();
225
226        assert_eq!(original, decoded);
227    }
228}