Skip to main content

mesa_dev/low_level/
content.rs

1//! Content API with correct `anyOf` union handling.
2//!
3//! The `OpenAPI` code generator flattens `anyOf` responses into a single struct
4//! with every field required, which fails at runtime
5//! ([openapi-generator#9497](https://github.com/OpenAPITools/openapi-generator/issues/9497)).
6//!
7//! This module provides a hand-written [`Content`] enum that correctly
8//! discriminates on the `"type"` field and deserializes into the appropriate
9//! variant.
10
11use mesa_dev_oapi::apis::configuration::Configuration;
12use mesa_dev_oapi::apis::{Error, ResponseContent};
13use mesa_dev_oapi::models;
14use serde::de::Error as _;
15use serde::{Deserialize, Deserializer, Serialize};
16
17/// Typed errors returned by [`get_content`].
18pub use mesa_dev_oapi::apis::content_api::GetByOrgByRepoContentError as GetContentError;
19
20/// Content returned by the content endpoint.
21///
22/// The API returns one of three shapes depending on whether the path points to
23/// a file, a symbolic link, or a directory.
24#[derive(Clone, Debug, PartialEq, Serialize)]
25#[serde(untagged)]
26pub enum Content {
27    /// A regular file.
28    File(models::GetByOrgByRepoContent200ResponseAnyOf),
29    /// A symbolic link.
30    Symlink(models::GetByOrgByRepoContent200ResponseAnyOf1),
31    /// A directory listing.
32    Dir(DirContent),
33}
34
35impl<'de> Deserialize<'de> for Content {
36    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
37        let value = serde_json::Value::deserialize(deserializer)?;
38        let type_str = value
39            .get("type")
40            .and_then(serde_json::Value::as_str)
41            .ok_or_else(|| D::Error::missing_field("type"))?;
42
43        match type_str {
44            "file" => serde_json::from_value(value)
45                .map(Content::File)
46                .map_err(D::Error::custom),
47            "symlink" => serde_json::from_value(value)
48                .map(Content::Symlink)
49                .map_err(D::Error::custom),
50            "dir" => serde_json::from_value(value)
51                .map(Content::Dir)
52                .map_err(D::Error::custom),
53            other => Err(D::Error::unknown_variant(
54                other,
55                &["file", "symlink", "dir"],
56            )),
57        }
58    }
59}
60
61/// A directory listing with correctly typed entries.
62#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
63pub struct DirContent {
64    /// Always `"dir"`.
65    #[serde(rename = "type")]
66    pub r#type: models::get_by_org_by_repo_content_200_response_any_of_2::Type,
67    /// Directory name, or `None` for the repository root.
68    #[serde(rename = "name", deserialize_with = "Option::deserialize")]
69    pub name: Option<String>,
70    /// Directory path relative to the repository root.
71    #[serde(rename = "path", deserialize_with = "Option::deserialize")]
72    pub path: Option<String>,
73    /// Tree SHA.
74    #[serde(rename = "sha", deserialize_with = "Option::deserialize")]
75    pub sha: Option<String>,
76    /// Total number of children in this directory.
77    pub child_count: i64,
78    /// Directory entries (files, symlinks, or subdirectories).
79    pub entries: Vec<DirEntry>,
80    /// Cursor for paginating entries, or `None` when there are no more pages.
81    #[serde(rename = "next_cursor", deserialize_with = "Option::deserialize")]
82    pub next_cursor: Option<String>,
83    /// Whether more entries are available beyond this page.
84    pub has_more: bool,
85}
86
87/// A single entry inside a directory listing.
88#[derive(Clone, Debug, PartialEq, Serialize)]
89#[serde(untagged)]
90pub enum DirEntry {
91    /// A file entry.
92    File(models::GetByOrgByRepoContent200ResponseAnyOf2EntriesInnerAnyOf),
93    /// A symbolic-link entry.
94    Symlink(models::GetByOrgByRepoContent200ResponseAnyOf2EntriesInnerAnyOf1),
95    /// A subdirectory entry.
96    Dir(models::GetByOrgByRepoContent200ResponseAnyOf2EntriesInnerAnyOf2),
97}
98
99impl<'de> Deserialize<'de> for DirEntry {
100    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
101        let value = serde_json::Value::deserialize(deserializer)?;
102        let type_str = value
103            .get("type")
104            .and_then(serde_json::Value::as_str)
105            .ok_or_else(|| D::Error::missing_field("type"))?;
106
107        match type_str {
108            "file" => serde_json::from_value(value)
109                .map(DirEntry::File)
110                .map_err(D::Error::custom),
111            "symlink" => serde_json::from_value(value)
112                .map(DirEntry::Symlink)
113                .map_err(D::Error::custom),
114            "dir" => serde_json::from_value(value)
115                .map(DirEntry::Dir)
116                .map_err(D::Error::custom),
117            other => Err(D::Error::unknown_variant(
118                other,
119                &["file", "symlink", "dir"],
120            )),
121        }
122    }
123}
124
125/// Get file content or directory listing at a path.
126///
127/// This wraps the generated content API endpoint with correct `anyOf`
128/// deserialization. Returns a [`Content`] enum discriminated on the `"type"`
129/// field.
130///
131/// # Errors
132///
133/// Returns an error if the request fails or the response cannot be
134/// deserialized.
135pub async fn get_content(
136    configuration: &Configuration,
137    org: &str,
138    repo: &str,
139    r#ref: Option<&str>,
140    path: Option<&str>,
141    depth: Option<u64>,
142) -> Result<Content, Error<GetContentError>> {
143    let uri_str = format!(
144        "{}/{org}/{repo}/content",
145        configuration.base_path,
146        org = mesa_dev_oapi::apis::urlencode(org),
147        repo = mesa_dev_oapi::apis::urlencode(repo),
148    );
149    let mut req_builder = configuration.client.request(reqwest::Method::GET, &uri_str);
150
151    if let Some(ref param_value) = r#ref {
152        req_builder = req_builder.query(&[("ref", &param_value.to_string())]);
153    }
154    if let Some(ref param_value) = path {
155        req_builder = req_builder.query(&[("path", &param_value.to_string())]);
156    }
157    if let Some(ref param_value) = depth {
158        req_builder = req_builder.query(&[("depth", &param_value.to_string())]);
159    }
160    if let Some(ref user_agent) = configuration.user_agent {
161        req_builder = req_builder.header(reqwest::header::USER_AGENT, user_agent.clone());
162    }
163    if let Some(ref token) = configuration.bearer_access_token {
164        req_builder = req_builder.bearer_auth(token.to_owned());
165    }
166
167    let req = req_builder.build()?;
168    let resp = configuration.client.execute(req).await?;
169
170    let status = resp.status();
171
172    if !status.is_client_error() && !status.is_server_error() {
173        let text = resp.text().await?;
174        serde_path_to_error::deserialize(&mut serde_json::Deserializer::from_str(&text))
175            .map_err(Error::from)
176    } else {
177        let text = resp.text().await?;
178        let entity: Option<GetContentError> = serde_json::from_str(&text).ok();
179        Err(Error::ResponseError(ResponseContent {
180            status,
181            content: text,
182            entity,
183        }))
184    }
185}