Skip to main content

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}