Skip to main content

foundation_models/
content.rs

1//! [`GeneratedContent`] — structured model output represented as JSON.
2
3use serde::de::DeserializeOwned;
4use serde::Serialize;
5use serde_json::Value;
6
7use crate::error::FMError;
8
9/// Rust analogue of FoundationModels' `ConvertibleFromGeneratedContent`.
10pub trait FromGeneratedContent: Sized {
11    /// Decode a Rust value from generated content.
12    fn from_generated_content(content: &GeneratedContent) -> Result<Self, FMError>;
13}
14
15/// Rust analogue of FoundationModels' `ConvertibleToGeneratedContent`.
16pub trait ToGeneratedContent {
17    /// Convert a Rust value into generated content.
18    fn to_generated_content(&self) -> Result<GeneratedContent, FMError>;
19}
20
21/// A piece of generated structured content.
22///
23/// Apple models structured generations as `GeneratedContent`; the Rust wrapper
24/// stores the JSON value plus the metadata that Apple's streaming API exposes.
25#[derive(Debug, Clone, PartialEq)]
26pub struct GeneratedContent {
27    value: Value,
28    generation_id: Option<String>,
29    is_complete: bool,
30}
31
32impl GeneratedContent {
33    /// Parse a JSON string into generated content.
34    ///
35    /// # Errors
36    ///
37    /// Returns [`FMError::InvalidArgument`] if `json` is not valid JSON.
38    pub fn from_json_str(json: &str) -> Result<Self, FMError> {
39        let value = serde_json::from_str(json).map_err(|error| {
40            FMError::InvalidArgument(format!("generated content JSON is invalid: {error}"))
41        })?;
42        Ok(Self {
43            value,
44            generation_id: None,
45            is_complete: true,
46        })
47    }
48
49    /// Convert a serializable Rust value into generated content.
50    ///
51    /// # Errors
52    ///
53    /// Returns [`FMError::InvalidArgument`] if `value` cannot be encoded as JSON.
54    pub fn from_value<T>(value: T) -> Result<Self, FMError>
55    where
56        T: Serialize,
57    {
58        let value = serde_json::to_value(value).map_err(|error| {
59            FMError::InvalidArgument(format!(
60                "generated content value is not JSON-serializable: {error}"
61            ))
62        })?;
63        Ok(Self {
64            value,
65            generation_id: None,
66            is_complete: true,
67        })
68    }
69
70    /// Build a value from bridge metadata.
71    pub(crate) fn from_bridge_json(
72        json: &str,
73        is_complete: bool,
74        generation_id: Option<String>,
75    ) -> Result<Self, FMError> {
76        let mut content = Self::from_json_str(json)?;
77        content.is_complete = is_complete;
78        content.generation_id = generation_id;
79        Ok(content)
80    }
81
82    /// Return the underlying JSON value.
83    #[must_use]
84    pub const fn raw_value(&self) -> &Value {
85        &self.value
86    }
87
88    /// Consume the content and return the underlying JSON value.
89    #[must_use]
90    pub fn into_raw_value(self) -> Value {
91        self.value
92    }
93
94    /// Serialize the content back to a compact JSON string.
95    ///
96    /// # Errors
97    ///
98    /// Returns [`FMError::Unknown`] if serialization fails.
99    pub fn json_string(&self) -> Result<String, FMError> {
100        serde_json::to_string(&self.value).map_err(|error| FMError::Unknown {
101            code: crate::ffi::status::UNKNOWN,
102            message: format!("failed to serialize generated content: {error}"),
103        })
104    }
105
106    /// Serialize the content as pretty JSON.
107    ///
108    /// # Errors
109    ///
110    /// Returns [`FMError::Unknown`] if serialization fails.
111    pub fn json_string_pretty(&self) -> Result<String, FMError> {
112        serde_json::to_string_pretty(&self.value).map_err(|error| FMError::Unknown {
113            code: crate::ffi::status::UNKNOWN,
114            message: format!("failed to serialize generated content: {error}"),
115        })
116    }
117
118    /// Decode the content into a Rust value.
119    ///
120    /// # Errors
121    ///
122    /// Returns [`FMError::DecodingFailure`] if the JSON value does not match `T`.
123    pub fn value<T>(&self) -> Result<T, FMError>
124    where
125        T: DeserializeOwned,
126    {
127        serde_json::from_value(self.value.clone())
128            .map_err(|error| FMError::DecodingFailure(error.to_string()))
129    }
130
131    /// Decode a named property from an object content value.
132    ///
133    /// # Errors
134    ///
135    /// Returns [`FMError::DecodingFailure`] if the value is not an object, the
136    /// property does not exist, or the property cannot be decoded as `T`.
137    pub fn value_for_property<T>(&self, property: &str) -> Result<T, FMError>
138    where
139        T: DeserializeOwned,
140    {
141        let Value::Object(map) = &self.value else {
142            return Err(FMError::DecodingFailure(
143                "generated content is not an object".into(),
144            ));
145        };
146        let value = map.get(property).cloned().ok_or_else(|| {
147            FMError::DecodingFailure(format!(
148                "generated content is missing property `{property}`"
149            ))
150        })?;
151        serde_json::from_value(value).map_err(|error| FMError::DecodingFailure(error.to_string()))
152    }
153
154    /// Whether Apple's structured stream reported this content as complete.
155    #[must_use]
156    pub const fn is_complete(&self) -> bool {
157        self.is_complete
158    }
159
160    /// Apple's opaque generation identifier, if one was attached.
161    #[must_use]
162    pub fn generation_id(&self) -> Option<&str> {
163        self.generation_id.as_deref()
164    }
165}
166
167impl TryFrom<Value> for GeneratedContent {
168    type Error = FMError;
169
170    fn try_from(value: Value) -> Result<Self, Self::Error> {
171        Ok(Self {
172            value,
173            generation_id: None,
174            is_complete: true,
175        })
176    }
177}
178
179impl From<GeneratedContent> for Value {
180    fn from(value: GeneratedContent) -> Self {
181        value.value
182    }
183}
184
185macro_rules! impl_scalar_content {
186    ($($ty:ty),+ $(,)?) => {
187        $(
188            impl From<$ty> for GeneratedContent {
189                fn from(value: $ty) -> Self {
190                    Self {
191                        value: serde_json::to_value(value)
192                            .expect("scalar values must always be JSON-serializable"),
193                        generation_id: None,
194                        is_complete: true,
195                    }
196                }
197            }
198        )+
199    };
200}
201
202impl_scalar_content!(bool, f32, f64, i8, i16, i32, i64, u8, u16, u32, u64);
203
204impl From<String> for GeneratedContent {
205    fn from(value: String) -> Self {
206        Self {
207            value: Value::String(value),
208            generation_id: None,
209            is_complete: true,
210        }
211    }
212}
213
214impl From<&str> for GeneratedContent {
215    fn from(value: &str) -> Self {
216        Self::from(value.to_owned())
217    }
218}
219
220impl<T> From<Vec<T>> for GeneratedContent
221where
222    T: Into<GeneratedContent>,
223{
224    fn from(values: Vec<T>) -> Self {
225        Self {
226            value: Value::Array(values.into_iter().map(|value| value.into().value).collect()),
227            generation_id: None,
228            is_complete: true,
229        }
230    }
231}
232
233impl FromGeneratedContent for GeneratedContent {
234    fn from_generated_content(content: &GeneratedContent) -> Result<Self, FMError> {
235        Ok(content.clone())
236    }
237}
238
239impl ToGeneratedContent for GeneratedContent {
240    fn to_generated_content(&self) -> Result<GeneratedContent, FMError> {
241        Ok(self.clone())
242    }
243}
244
245impl FromGeneratedContent for Value {
246    fn from_generated_content(content: &GeneratedContent) -> Result<Self, FMError> {
247        Ok(content.raw_value().clone())
248    }
249}
250
251impl ToGeneratedContent for Value {
252    fn to_generated_content(&self) -> Result<GeneratedContent, FMError> {
253        GeneratedContent::from_value(self)
254    }
255}
256
257impl FromGeneratedContent for String {
258    fn from_generated_content(content: &GeneratedContent) -> Result<Self, FMError> {
259        content.value()
260    }
261}
262
263impl ToGeneratedContent for String {
264    fn to_generated_content(&self) -> Result<GeneratedContent, FMError> {
265        Ok(GeneratedContent::from(self.clone()))
266    }
267}
268
269impl ToGeneratedContent for str {
270    fn to_generated_content(&self) -> Result<GeneratedContent, FMError> {
271        Ok(GeneratedContent::from(self))
272    }
273}
274
275impl FromGeneratedContent for bool {
276    fn from_generated_content(content: &GeneratedContent) -> Result<Self, FMError> {
277        content.value()
278    }
279}
280
281impl ToGeneratedContent for bool {
282    fn to_generated_content(&self) -> Result<GeneratedContent, FMError> {
283        Ok(GeneratedContent::from(*self))
284    }
285}
286
287macro_rules! impl_numeric_conversion {
288    ($($ty:ty),+ $(,)?) => {
289        $(
290            impl FromGeneratedContent for $ty {
291                fn from_generated_content(content: &GeneratedContent) -> Result<Self, FMError> {
292                    content.value()
293                }
294            }
295
296            impl ToGeneratedContent for $ty {
297                fn to_generated_content(&self) -> Result<GeneratedContent, FMError> {
298                    Ok(GeneratedContent::from(*self))
299                }
300            }
301        )+
302    };
303}
304
305impl_numeric_conversion!(f32, f64, i8, i16, i32, i64, u8, u16, u32, u64);
306
307impl<T> FromGeneratedContent for Vec<T>
308where
309    T: FromGeneratedContent,
310{
311    fn from_generated_content(content: &GeneratedContent) -> Result<Self, FMError> {
312        let values: Vec<Value> = content.value()?;
313        values
314            .iter()
315            .map(|value| {
316                let nested = GeneratedContent::try_from(value.clone())?;
317                T::from_generated_content(&nested)
318            })
319            .collect()
320    }
321}
322
323impl<T> ToGeneratedContent for Vec<T>
324where
325    T: ToGeneratedContent,
326{
327    fn to_generated_content(&self) -> Result<GeneratedContent, FMError> {
328        let values = self
329            .iter()
330            .map(ToGeneratedContent::to_generated_content)
331            .collect::<Result<Vec<_>, _>>()?;
332        Ok(GeneratedContent {
333            value: Value::Array(
334                values
335                    .into_iter()
336                    .map(GeneratedContent::into_raw_value)
337                    .collect(),
338            ),
339            generation_id: None,
340            is_complete: true,
341        })
342    }
343}
344
345impl<T> FromGeneratedContent for Option<T>
346where
347    T: FromGeneratedContent,
348{
349    fn from_generated_content(content: &GeneratedContent) -> Result<Self, FMError> {
350        if content.raw_value().is_null() {
351            return Ok(None);
352        }
353        T::from_generated_content(content).map(Some)
354    }
355}
356
357impl<T> ToGeneratedContent for Option<T>
358where
359    T: ToGeneratedContent,
360{
361    fn to_generated_content(&self) -> Result<GeneratedContent, FMError> {
362        match self {
363            Some(value) => value.to_generated_content(),
364            None => GeneratedContent::from_value(Option::<Value>::None),
365        }
366    }
367}