Skip to main content

tripo_api/
image.rs

1//! Image inputs: URL, pre-uploaded file token, or local path.
2//!
3//! On the wire every image is wrapped as `{"type":"jpg", ...}` regardless of the
4//! actual file format (this is what the Tripo server expects). The body is one of:
5//! `{"url": "..."}`, `{"file_token": "<uuid>"}`. Local paths must be uploaded
6//! before serialization — the client's `upload_images` helper handles this.
7
8use std::path::PathBuf;
9
10use serde::de::{Deserialize, Deserializer, Error as DeError};
11use serde::ser::{Serialize, SerializeStruct, Serializer};
12use url::Url;
13use uuid::Uuid;
14
15/// A reference to an image, accepted by all image-consuming variants.
16#[derive(Debug, Clone, PartialEq, Eq)]
17#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
18pub enum ImageInput {
19    /// A publicly fetchable URL.
20    Url(Url),
21    /// A token returned by a prior upload.
22    FileToken(Uuid),
23    /// A local file path — must be uploaded before the request is sent.
24    Path(PathBuf),
25}
26
27impl ImageInput {
28    /// Classify a string into a variant.
29    ///
30    /// `http://` / `https://` → [`ImageInput::Url`]. A canonical UUID → [`ImageInput::FileToken`].
31    /// Anything else → [`ImageInput::Path`].
32    #[must_use]
33    pub fn parse(s: &str) -> Self {
34        if let Ok(url) = Url::parse(s)
35            && matches!(url.scheme(), "http" | "https")
36        {
37            return Self::Url(url);
38        }
39        if let Ok(uuid) = Uuid::parse_str(s) {
40            return Self::FileToken(uuid);
41        }
42        Self::Path(PathBuf::from(s))
43    }
44}
45
46impl Serialize for ImageInput {
47    fn serialize<S: Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
48        let mut st = ser.serialize_struct("FileContent", 2)?;
49        st.serialize_field("type", "jpg")?;
50        match self {
51            Self::Url(u) => st.serialize_field("url", u.as_str())?,
52            Self::FileToken(t) => st.serialize_field("file_token", &t.to_string())?,
53            Self::Path(p) => {
54                return Err(serde::ser::Error::custom(format!(
55                    "ImageInput::Path({}) must be uploaded before serialization — call Client::upload_images on the request first",
56                    p.display()
57                )));
58            }
59        }
60        st.end()
61    }
62}
63
64impl<'de> Deserialize<'de> for ImageInput {
65    fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
66        let v = serde_json::Value::deserialize(d)?;
67        match v {
68            serde_json::Value::String(s) => Ok(Self::parse(&s)),
69            serde_json::Value::Object(mut m) => {
70                m.remove("type");
71                if let Some(url) = m.remove("url").and_then(|v| v.as_str().map(str::to_string)) {
72                    Url::parse(&url).map(Self::Url).map_err(DeError::custom)
73                } else if let Some(tok) = m
74                    .remove("file_token")
75                    .and_then(|v| v.as_str().map(str::to_string))
76                {
77                    Uuid::parse_str(&tok)
78                        .map(Self::FileToken)
79                        .map_err(DeError::custom)
80                } else {
81                    Err(DeError::custom(
82                        "expected `url` or `file_token` in ImageInput object",
83                    ))
84                }
85            }
86            other => Err(DeError::custom(format!(
87                "unexpected ImageInput shape: {other}"
88            ))),
89        }
90    }
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96
97    #[test]
98    fn parse_url() {
99        let i = ImageInput::parse("https://example.com/x.jpg");
100        assert!(matches!(i, ImageInput::Url(_)));
101    }
102
103    #[test]
104    fn parse_file_token() {
105        let uuid = "550e8400-e29b-41d4-a716-446655440000";
106        assert!(matches!(ImageInput::parse(uuid), ImageInput::FileToken(_)));
107    }
108
109    #[test]
110    fn parse_file_token_case_insensitive() {
111        let uuid = "550E8400-E29B-41D4-A716-446655440000";
112        assert!(matches!(ImageInput::parse(uuid), ImageInput::FileToken(_)));
113    }
114
115    #[test]
116    fn parse_path() {
117        let i = ImageInput::parse("./photo.png");
118        assert!(matches!(i, ImageInput::Path(_)));
119    }
120
121    #[test]
122    fn serialize_url() {
123        let i = ImageInput::Url("https://example.com/x.jpg".parse().unwrap());
124        let got: serde_json::Value = serde_json::to_value(&i).unwrap();
125        assert_eq!(
126            got,
127            serde_json::json!({"type":"jpg","url":"https://example.com/x.jpg"})
128        );
129    }
130
131    #[test]
132    fn serialize_file_token() {
133        let t = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
134        let got: serde_json::Value = serde_json::to_value(ImageInput::FileToken(t)).unwrap();
135        assert_eq!(
136            got,
137            serde_json::json!({"type":"jpg","file_token":"550e8400-e29b-41d4-a716-446655440000"})
138        );
139    }
140
141    #[test]
142    fn serialize_path_errors() {
143        let err = serde_json::to_value(ImageInput::Path("./x.png".into())).unwrap_err();
144        assert!(err.to_string().contains("must be uploaded"));
145    }
146}