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