rustapi_toon/
extractor.rs1use 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#[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 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 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 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
136impl<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
157impl<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}