Skip to content

Commit 7b3e96d

Browse files
Merge pull request #198 from code0-tech/#196-expect-runtime-error
expect runtime error from taurus
2 parents c6a3445 + 9adc6a3 commit 7b3e96d

4 files changed

Lines changed: 566 additions & 154 deletions

File tree

adapter/rest/src/content_type.rs

Lines changed: 318 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,318 @@
1+
use hyper::{
2+
HeaderMap,
3+
header::{CONTENT_TYPE, HeaderValue},
4+
};
5+
use tucana::shared::{
6+
Value, number_value,
7+
value::Kind::{self, StringValue},
8+
};
9+
10+
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
11+
pub enum BodyFormat {
12+
Json,
13+
TextPlain,
14+
Unknown,
15+
}
16+
17+
#[derive(Debug)]
18+
pub enum BodyParseError {
19+
UnsupportedContentType { observed: String },
20+
InvalidUtf8(std::str::Utf8Error),
21+
InvalidJson(serde_json::Error),
22+
}
23+
24+
impl std::fmt::Display for BodyParseError {
25+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26+
match self {
27+
Self::UnsupportedContentType { observed } => {
28+
write!(f, "unsupported content type: {}", observed)
29+
}
30+
Self::InvalidUtf8(err) => write!(f, "invalid UTF-8 body: {}", err),
31+
Self::InvalidJson(err) => write!(f, "invalid JSON body: {}", err),
32+
}
33+
}
34+
}
35+
36+
impl std::error::Error for BodyParseError {}
37+
38+
#[derive(Debug)]
39+
pub enum BodyEncodeError {
40+
UnsupportedContentType { observed: String },
41+
InvalidJson(serde_json::Error),
42+
}
43+
44+
impl std::fmt::Display for BodyEncodeError {
45+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46+
match self {
47+
Self::UnsupportedContentType { observed } => {
48+
write!(f, "unsupported content type: {}", observed)
49+
}
50+
Self::InvalidJson(err) => write!(f, "failed to encode JSON body: {}", err),
51+
}
52+
}
53+
}
54+
55+
impl std::error::Error for BodyEncodeError {}
56+
57+
pub fn parse_body_from_headers(
58+
headers: &HeaderMap<HeaderValue>,
59+
body: &[u8],
60+
) -> Result<Option<Value>, BodyParseError> {
61+
parse_body(get_content_type(headers), body)
62+
}
63+
64+
pub fn parse_body(
65+
content_type: Option<&str>,
66+
body: &[u8],
67+
) -> Result<Option<Value>, BodyParseError> {
68+
if body.is_empty() {
69+
return Ok(None);
70+
}
71+
72+
match classify_content_type(content_type) {
73+
BodyFormat::Json => parse_json_body(body),
74+
BodyFormat::TextPlain => parse_text_body(body),
75+
BodyFormat::Unknown => {
76+
// If there is no content type
77+
if content_type.is_none()
78+
&& let Ok(value) = parse_text_body(body)
79+
{
80+
return Ok(value);
81+
}
82+
83+
Err(BodyParseError::UnsupportedContentType {
84+
observed: content_type.unwrap_or("<missing>").to_string(),
85+
})
86+
}
87+
}
88+
}
89+
90+
pub fn encode_body(content_type: Option<&str>, value: Value) -> Result<Vec<u8>, BodyEncodeError> {
91+
match classify_content_type(content_type) {
92+
BodyFormat::Json => encode_json_body(value),
93+
BodyFormat::TextPlain => encode_text_body(value),
94+
BodyFormat::Unknown => {
95+
// Missing content type falls back to JSON.
96+
if content_type.is_none() {
97+
return encode_json_body(value);
98+
}
99+
100+
Err(BodyEncodeError::UnsupportedContentType {
101+
observed: content_type.unwrap_or("<missing>").to_string(),
102+
})
103+
}
104+
}
105+
}
106+
107+
pub fn classify_content_type(content_type: Option<&str>) -> BodyFormat {
108+
let Some(raw) = content_type else {
109+
return BodyFormat::Unknown;
110+
};
111+
112+
let essence = raw
113+
.split(';')
114+
.next()
115+
.unwrap_or(raw)
116+
.trim()
117+
.to_ascii_lowercase();
118+
119+
if essence == "application/json" || essence.ends_with("+json") {
120+
return BodyFormat::Json;
121+
}
122+
123+
if essence == "text/plain" {
124+
return BodyFormat::TextPlain;
125+
}
126+
127+
BodyFormat::Unknown
128+
}
129+
130+
fn parse_json_body(body: &[u8]) -> Result<Option<Value>, BodyParseError> {
131+
let json_value =
132+
serde_json::from_slice::<serde_json::Value>(body).map_err(BodyParseError::InvalidJson)?;
133+
Ok(Some(tucana::shared::helper::value::from_json_value(
134+
json_value,
135+
)))
136+
}
137+
138+
fn parse_text_body(body: &[u8]) -> Result<Option<Value>, BodyParseError> {
139+
let text = std::str::from_utf8(body).map_err(BodyParseError::InvalidUtf8)?;
140+
Ok(Some(Value {
141+
kind: Some(StringValue(text.to_string())),
142+
}))
143+
}
144+
145+
fn encode_json_body(value: Value) -> Result<Vec<u8>, BodyEncodeError> {
146+
let json_val = tucana::shared::helper::value::to_json_value(value);
147+
serde_json::to_vec_pretty(&json_val).map_err(BodyEncodeError::InvalidJson)
148+
}
149+
150+
fn encode_text_body(value: Value) -> Result<Vec<u8>, BodyEncodeError> {
151+
if let Some(text) = scalar_to_text(&value) {
152+
return Ok(text.into_bytes());
153+
}
154+
155+
// For lists/objects, return valid JSON text as the plain-text body.
156+
encode_json_body(value)
157+
}
158+
159+
fn scalar_to_text(value: &Value) -> Option<String> {
160+
match value.kind.as_ref() {
161+
Some(Kind::NullValue(_)) | None => Some("null".to_string()),
162+
Some(Kind::BoolValue(v)) => Some(v.to_string()),
163+
Some(Kind::StringValue(v)) => Some(v.clone()),
164+
Some(Kind::NumberValue(v)) => match v.number.as_ref() {
165+
Some(number_value::Number::Integer(i)) => Some(i.to_string()),
166+
Some(number_value::Number::Float(f)) => Some(f.to_string()),
167+
None => Some("null".to_string()),
168+
},
169+
_ => None,
170+
}
171+
}
172+
173+
fn get_content_type(headers: &HeaderMap<HeaderValue>) -> Option<&str> {
174+
headers.get(CONTENT_TYPE).and_then(|h| h.to_str().ok())
175+
}
176+
177+
#[cfg(test)]
178+
mod tests {
179+
use super::*;
180+
use tucana::shared::{NumberValue, Struct, Value};
181+
182+
#[test]
183+
fn classify_json_content_type_with_charset() {
184+
let format = classify_content_type(Some("application/json; charset=utf-8"));
185+
assert_eq!(format, BodyFormat::Json);
186+
}
187+
188+
#[test]
189+
fn classify_vendor_json_content_type() {
190+
let format = classify_content_type(Some("application/problem+json"));
191+
assert_eq!(format, BodyFormat::Json);
192+
}
193+
194+
#[test]
195+
fn classify_text_plain_content_type() {
196+
let format = classify_content_type(Some("text/plain; charset=utf-8"));
197+
assert_eq!(format, BodyFormat::TextPlain);
198+
}
199+
200+
#[test]
201+
fn parse_json_body_to_struct_value() {
202+
let body = br#"{"hello":"world","ok":true}"#;
203+
let parsed = parse_body(Some("application/json"), body).unwrap();
204+
205+
let Some(Value {
206+
kind: Some(Kind::StructValue(Struct { fields })),
207+
}) = parsed
208+
else {
209+
panic!("expected struct value");
210+
};
211+
212+
assert!(fields.contains_key("hello"));
213+
assert!(fields.contains_key("ok"));
214+
}
215+
216+
#[test]
217+
fn parse_text_body_to_string_value() {
218+
let body = b"hello";
219+
let parsed = parse_body(Some("text/plain"), body).unwrap();
220+
221+
let Some(Value {
222+
kind: Some(Kind::StringValue(v)),
223+
}) = parsed
224+
else {
225+
panic!("expected string value");
226+
};
227+
228+
assert_eq!(v, "hello");
229+
}
230+
231+
#[test]
232+
fn parse_unsupported_content_type_fails() {
233+
let body = br#"<root />"#;
234+
let err = parse_body(Some("application/xml"), body).unwrap_err();
235+
236+
assert!(matches!(err, BodyParseError::UnsupportedContentType { .. }));
237+
}
238+
239+
#[test]
240+
fn encode_json_body_from_struct_value() {
241+
let value = Value {
242+
kind: Some(Kind::StructValue(Struct {
243+
fields: [(
244+
"hello".to_string(),
245+
Value {
246+
kind: Some(Kind::StringValue("world".to_string())),
247+
},
248+
)]
249+
.into_iter()
250+
.collect(),
251+
})),
252+
};
253+
254+
let encoded = encode_body(Some("application/json"), value).unwrap();
255+
let parsed: serde_json::Value = serde_json::from_slice(&encoded).unwrap();
256+
257+
assert_eq!(parsed["hello"], "world");
258+
}
259+
260+
#[test]
261+
fn encode_text_body_from_string_value() {
262+
let value = Value {
263+
kind: Some(Kind::StringValue("hello".to_string())),
264+
};
265+
266+
let encoded = encode_body(Some("text/plain"), value).unwrap();
267+
assert_eq!(encoded, b"hello".to_vec());
268+
}
269+
270+
#[test]
271+
fn encode_text_body_from_number_value() {
272+
let value = Value {
273+
kind: Some(Kind::NumberValue(NumberValue {
274+
number: Some(number_value::Number::Integer(42)),
275+
})),
276+
};
277+
278+
let encoded = encode_body(Some("text/plain"), value).unwrap();
279+
assert_eq!(encoded, b"42".to_vec());
280+
}
281+
282+
#[test]
283+
fn encode_text_body_from_struct_value_falls_back_to_json_text() {
284+
let value = Value {
285+
kind: Some(Kind::StructValue(Struct {
286+
fields: [(
287+
"answer".to_string(),
288+
Value {
289+
kind: Some(Kind::NumberValue(NumberValue {
290+
number: Some(number_value::Number::Integer(42)),
291+
})),
292+
},
293+
)]
294+
.into_iter()
295+
.collect(),
296+
})),
297+
};
298+
299+
let encoded = encode_body(Some("text/plain"), value).unwrap();
300+
let body_text = String::from_utf8(encoded).unwrap();
301+
302+
assert!(body_text.contains("\"answer\""));
303+
assert!(body_text.contains("42"));
304+
}
305+
306+
#[test]
307+
fn encode_unknown_content_type_fails() {
308+
let value = Value {
309+
kind: Some(Kind::StringValue("x".to_string())),
310+
};
311+
312+
let err = encode_body(Some("application/xml"), value).unwrap_err();
313+
assert!(matches!(
314+
err,
315+
BodyEncodeError::UnsupportedContentType { .. }
316+
));
317+
}
318+
}

0 commit comments

Comments
 (0)