Skip to main content

atproto_attestation/
input.rs

1//! Input types for attestation functions supporting multiple input formats.
2
3use serde::Serialize;
4use serde_json::{Map, Value};
5use std::convert::TryFrom;
6use std::str::FromStr;
7use thiserror::Error;
8
9/// Flexible input type for attestation functions.
10///
11/// Allows passing records and metadata as JSON strings or any serde serializable types.
12#[derive(Clone)]
13pub enum AnyInput<S: Serialize + Clone> {
14    /// JSON string representation
15    String(String),
16    /// Serializable types
17    Serialize(S),
18}
19
20/// Error types for AnyInput parsing and transformation operations.
21///
22/// This enum provides specific error types for various failure modes when working
23/// with `AnyInput`, including JSON parsing errors, type conversion errors, and
24/// serialization failures.
25#[derive(Debug, Error)]
26pub enum AnyInputError {
27    /// Error when parsing JSON from a string fails.
28    #[error("Failed to parse JSON from string: {0}")]
29    JsonParseError(#[from] serde_json::Error),
30
31    /// Error when the value is not a JSON object.
32    #[error("Expected JSON object, but got {value_type}")]
33    NotAnObject {
34        /// The actual type of the value.
35        value_type: String,
36    },
37
38    /// Error when the string contains invalid JSON.
39    #[error("Invalid JSON string: {message}")]
40    InvalidJson {
41        /// Error message describing what's wrong with the JSON.
42        message: String,
43    },
44}
45
46impl AnyInputError {
47    /// Creates a new `NotAnObject` error with the actual type information.
48    pub fn not_an_object(value: &Value) -> Self {
49        let value_type = match value {
50            Value::Null => "null".to_string(),
51            Value::Bool(_) => "boolean".to_string(),
52            Value::Number(_) => "number".to_string(),
53            Value::String(_) => "string".to_string(),
54            Value::Array(_) => "array".to_string(),
55            Value::Object(_) => "object".to_string(), // Should not happen
56        };
57
58        AnyInputError::NotAnObject { value_type }
59    }
60}
61
62/// Implementation of `FromStr` for `AnyInput` that deserializes JSON strings.
63///
64/// This allows parsing JSON strings directly into `AnyInput<serde_json::Value>` using
65/// the standard `FromStr` trait. The string is deserialized using `serde_json::from_str`
66/// and wrapped in `AnyInput::Serialize`.
67///
68/// # Errors
69///
70/// Returns `AnyInputError::JsonParseError` if the string contains invalid JSON.
71///
72/// # Example
73///
74/// ```
75/// use atproto_attestation::input::AnyInput;
76/// use std::str::FromStr;
77///
78/// let input: AnyInput<serde_json::Value> = r#"{"type": "post", "text": "Hello"}"#.parse().unwrap();
79/// ```
80impl FromStr for AnyInput<serde_json::Value> {
81    type Err = AnyInputError;
82
83    fn from_str(s: &str) -> Result<Self, Self::Err> {
84        let value = serde_json::from_str(s)?;
85        Ok(AnyInput::Serialize(value))
86    }
87}
88
89impl<S: Serialize + Clone> From<S> for AnyInput<S> {
90    fn from(value: S) -> Self {
91        AnyInput::Serialize(value)
92    }
93}
94
95/// Implementation of `TryFrom` for converting `AnyInput` into a JSON object map.
96///
97/// This allows converting any `AnyInput` into a `serde_json::Map<String, Value>`, which
98/// represents a JSON object. Both string and serializable inputs are converted to JSON
99/// objects, with appropriate error handling for non-object values.
100///
101/// # Example
102///
103/// ```
104/// use atproto_attestation::input::AnyInput;
105/// use serde_json::{json, Map, Value};
106/// use std::convert::TryInto;
107///
108/// let input = AnyInput::Serialize(json!({"type": "post", "text": "Hello"}));
109/// let map: Map<String, Value> = input.try_into().unwrap();
110/// assert_eq!(map.get("type").unwrap(), "post");
111/// ```
112impl<S: Serialize + Clone> TryFrom<AnyInput<S>> for Map<String, Value> {
113    type Error = AnyInputError;
114
115    fn try_from(input: AnyInput<S>) -> Result<Self, Self::Error> {
116        match input {
117            AnyInput::String(value) => {
118                // Parse string as JSON
119                let json_value = serde_json::from_str::<Value>(&value)?;
120
121                // Extract as object
122                json_value
123                    .as_object()
124                    .cloned()
125                    .ok_or_else(|| AnyInputError::not_an_object(&json_value))
126            }
127            AnyInput::Serialize(value) => {
128                // Convert to JSON value
129                let json_value = serde_json::to_value(value)?;
130
131                // Extract as object
132                json_value
133                    .as_object()
134                    .cloned()
135                    .ok_or_else(|| AnyInputError::not_an_object(&json_value))
136            }
137        }
138    }
139}
140
141/// Default phantom type for AnyInput when no specific lexicon type is needed.
142///
143/// This type serves as the default generic parameter for `AnyInput`, allowing
144/// for simpler usage when working with untyped JSON values.
145#[derive(Serialize, PartialEq, Clone)]
146pub struct PhantomSignature {}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151
152    #[test]
153    fn test_from_str_valid_json() {
154        let json_str = r#"{"type": "post", "text": "Hello", "count": 42}"#;
155        let result: Result<AnyInput<serde_json::Value>, _> = json_str.parse();
156
157        assert!(result.is_ok());
158
159        let input = result.unwrap();
160        match input {
161            AnyInput::Serialize(value) => {
162                assert_eq!(value["type"], "post");
163                assert_eq!(value["text"], "Hello");
164                assert_eq!(value["count"], 42);
165            }
166            _ => panic!("Expected AnyInput::Serialize variant"),
167        }
168    }
169
170    #[test]
171    fn test_from_str_invalid_json() {
172        let invalid_json = r#"{"type": "post", "text": "Hello" invalid json"#;
173        let result: Result<AnyInput<serde_json::Value>, _> = invalid_json.parse();
174
175        assert!(result.is_err());
176    }
177
178    #[test]
179    fn test_from_str_array() {
180        let json_array = r#"[1, 2, 3, "four"]"#;
181        let result: Result<AnyInput<serde_json::Value>, _> = json_array.parse();
182
183        assert!(result.is_ok());
184
185        let input = result.unwrap();
186        match input {
187            AnyInput::Serialize(value) => {
188                assert!(value.is_array());
189                let array = value.as_array().unwrap();
190                assert_eq!(array.len(), 4);
191                assert_eq!(array[0], 1);
192                assert_eq!(array[3], "four");
193            }
194            _ => panic!("Expected AnyInput::Serialize variant"),
195        }
196    }
197
198    #[test]
199    fn test_from_str_null() {
200        let null_str = "null";
201        let result: Result<AnyInput<serde_json::Value>, _> = null_str.parse();
202
203        assert!(result.is_ok());
204
205        let input = result.unwrap();
206        match input {
207            AnyInput::Serialize(value) => {
208                assert!(value.is_null());
209            }
210            _ => panic!("Expected AnyInput::Serialize variant"),
211        }
212    }
213
214    #[test]
215    fn test_from_str_with_use() {
216        // Test using the parse method directly with type inference
217        let input: AnyInput<serde_json::Value> = r#"{"$type": "app.bsky.feed.post"}"#
218            .parse()
219            .expect("Failed to parse JSON");
220
221        match input {
222            AnyInput::Serialize(value) => {
223                assert_eq!(value["$type"], "app.bsky.feed.post");
224            }
225            _ => panic!("Expected AnyInput::Serialize variant"),
226        }
227    }
228
229    #[test]
230    fn test_try_into_from_string() {
231        use std::convert::TryInto;
232
233        let input = AnyInput::<Value>::String(r#"{"type": "post", "text": "Hello"}"#.to_string());
234        let result: Result<Map<String, Value>, _> = input.try_into();
235
236        assert!(result.is_ok());
237        let map = result.unwrap();
238        assert_eq!(map.get("type").unwrap(), "post");
239        assert_eq!(map.get("text").unwrap(), "Hello");
240    }
241
242    #[test]
243    fn test_try_into_from_serialize() {
244        use serde_json::json;
245        use std::convert::TryInto;
246
247        let input = AnyInput::Serialize(json!({"$type": "app.bsky.feed.post", "count": 42}));
248        let result: Result<Map<String, Value>, _> = input.try_into();
249
250        assert!(result.is_ok());
251        let map = result.unwrap();
252        assert_eq!(map.get("$type").unwrap(), "app.bsky.feed.post");
253        assert_eq!(map.get("count").unwrap(), 42);
254    }
255
256    #[test]
257    fn test_try_into_string_not_object() {
258        use std::convert::TryInto;
259
260        let input = AnyInput::<Value>::String(r#"["array", "not", "object"]"#.to_string());
261        let result: Result<Map<String, Value>, AnyInputError> = input.try_into();
262
263        assert!(result.is_err());
264        match result.unwrap_err() {
265            AnyInputError::NotAnObject { value_type } => {
266                assert_eq!(value_type, "array");
267            }
268            _ => panic!("Expected NotAnObject error"),
269        }
270    }
271
272    #[test]
273    fn test_try_into_serialize_not_object() {
274        use serde_json::json;
275        use std::convert::TryInto;
276
277        let input = AnyInput::Serialize(json!([1, 2, 3]));
278        let result: Result<Map<String, Value>, AnyInputError> = input.try_into();
279
280        assert!(result.is_err());
281        match result.unwrap_err() {
282            AnyInputError::NotAnObject { value_type } => {
283                assert_eq!(value_type, "array");
284            }
285            _ => panic!("Expected NotAnObject error"),
286        }
287    }
288
289    #[test]
290    fn test_try_into_invalid_json_string() {
291        use std::convert::TryInto;
292
293        let input = AnyInput::<Value>::String("not valid json".to_string());
294        let result: Result<Map<String, Value>, AnyInputError> = input.try_into();
295
296        assert!(result.is_err());
297        match result.unwrap_err() {
298            AnyInputError::JsonParseError(_) => {}
299            _ => panic!("Expected JsonParseError"),
300        }
301    }
302
303    #[test]
304    fn test_try_into_null() {
305        use serde_json::json;
306        use std::convert::TryInto;
307
308        let input = AnyInput::Serialize(json!(null));
309        let result: Result<Map<String, Value>, AnyInputError> = input.try_into();
310
311        assert!(result.is_err());
312        match result.unwrap_err() {
313            AnyInputError::NotAnObject { value_type } => {
314                assert_eq!(value_type, "null");
315            }
316            _ => panic!("Expected NotAnObject error"),
317        }
318    }
319
320    #[test]
321    fn test_any_input_error_not_an_object() {
322        use serde_json::json;
323
324        // Test null
325        let err = AnyInputError::not_an_object(&json!(null));
326        match err {
327            AnyInputError::NotAnObject { value_type } => {
328                assert_eq!(value_type, "null");
329            }
330            _ => panic!("Expected NotAnObject error"),
331        }
332
333        // Test boolean
334        let err = AnyInputError::not_an_object(&json!(true));
335        match err {
336            AnyInputError::NotAnObject { value_type } => {
337                assert_eq!(value_type, "boolean");
338            }
339            _ => panic!("Expected NotAnObject error"),
340        }
341
342        // Test number
343        let err = AnyInputError::not_an_object(&json!(42));
344        match err {
345            AnyInputError::NotAnObject { value_type } => {
346                assert_eq!(value_type, "number");
347            }
348            _ => panic!("Expected NotAnObject error"),
349        }
350
351        // Test string
352        let err = AnyInputError::not_an_object(&json!("hello"));
353        match err {
354            AnyInputError::NotAnObject { value_type } => {
355                assert_eq!(value_type, "string");
356            }
357            _ => panic!("Expected NotAnObject error"),
358        }
359
360        // Test array
361        let err = AnyInputError::not_an_object(&json!([1, 2, 3]));
362        match err {
363            AnyInputError::NotAnObject { value_type } => {
364                assert_eq!(value_type, "array");
365            }
366            _ => panic!("Expected NotAnObject error"),
367        }
368    }
369
370    #[test]
371    fn test_error_display() {
372        use serde_json::json;
373
374        // Test NotAnObject error display
375        let err = AnyInputError::not_an_object(&json!(42));
376        assert_eq!(err.to_string(), "Expected JSON object, but got number");
377
378        // Test InvalidJson display
379        let err = AnyInputError::InvalidJson {
380            message: "unexpected token".to_string(),
381        };
382        assert_eq!(err.to_string(), "Invalid JSON string: unexpected token");
383    }
384}