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::BTreeMap;
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 = 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
159impl<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}