typesafe_jev/content.rs
1use serde::{Deserialize, Serialize};
2use serde_json::{Map, Value};
3
4use crate::Error;
5
6/// Text or structured JSON: what the API reference writes as `string | object | array`, and
7/// TypeSafe's own SDKs call `JSONContent`.
8///
9/// Instructions, the descriptions of a [`Choice`](crate::Choice)'s options, a
10/// [`Score`](crate::Score)'s levels and a [`Noul`](crate::Noul)'s two outcomes all take it. Text
11/// is the common case, and a `&str` or `String` converts with `into()`. Structure is for a
12/// question that refers to data of its own: put the data in one field and the question in
13/// another, and name the data field in backticks.
14///
15/// ```
16/// use serde_json::json;
17/// use typesafe_jev::Content;
18///
19/// let text: Content = "Is the resume for the same person?".into();
20/// assert_eq!(text.as_str(), Some("Is the resume for the same person?"));
21///
22/// let structured = Content::try_from(json!({
23/// "potential_duplicate": {"name": "John Smith", "location": "Oakland, California"},
24/// "question": "Is the resume for the same person as `potential_duplicate`?",
25/// }))?;
26/// assert!(matches!(structured, Content::Object(_)));
27///
28/// // A number, a boolean or null is not content.
29/// assert!(Content::try_from(json!(42)).is_err());
30/// # Ok::<(), typesafe_jev::Error>(())
31/// ```
32#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
33#[serde(untagged, expecting = "a string, an object or an array")]
34pub enum Content {
35 /// Plain text.
36 Text(String),
37 /// A JSON object, whose fields the text of a question can name in backticks.
38 Object(Map<String, Value>),
39 /// A JSON array.
40 Array(Vec<Value>),
41}
42
43impl Content {
44 /// The text, if this is [`Content::Text`].
45 pub fn as_str(&self) -> Option<&str> {
46 match self {
47 Content::Text(text) => Some(text),
48 Content::Object(_) | Content::Array(_) => None,
49 }
50 }
51}
52
53impl From<&str> for Content {
54 fn from(text: &str) -> Self {
55 Content::Text(text.to_owned())
56 }
57}
58
59impl From<String> for Content {
60 fn from(text: String) -> Self {
61 Content::Text(text)
62 }
63}
64
65impl From<&String> for Content {
66 fn from(text: &String) -> Self {
67 Content::Text(text.clone())
68 }
69}
70
71impl From<Map<String, Value>> for Content {
72 fn from(object: Map<String, Value>) -> Self {
73 Content::Object(object)
74 }
75}
76
77impl From<Vec<Value>> for Content {
78 fn from(array: Vec<Value>) -> Self {
79 Content::Array(array)
80 }
81}
82
83impl TryFrom<Value> for Content {
84 type Error = Error;
85
86 /// [`Error::InvalidRequest`] for a number, a boolean or null.
87 fn try_from(value: Value) -> Result<Self, Error> {
88 match value {
89 Value::String(text) => Ok(Content::Text(text)),
90 Value::Object(object) => Ok(Content::Object(object)),
91 Value::Array(array) => Ok(Content::Array(array)),
92 Value::Null | Value::Bool(_) | Value::Number(_) => {
93 Err(Error::InvalidRequest("content must be a string, an object or an array".into()))
94 }
95 }
96 }
97}
98
99impl From<Content> for Value {
100 fn from(content: Content) -> Self {
101 match content {
102 Content::Text(text) => Value::String(text),
103 Content::Object(object) => Value::Object(object),
104 Content::Array(array) => Value::Array(array),
105 }
106 }
107}