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.
135#[tracing::instrument(skip(configuration), fields(http.method = "GET", http.status_code), err(Debug))]
136pub async fn get_content(
137    configuration: &Configuration,
138    org: &str,
139    repo: &str,
140    r#ref: Option<&str>,
141    path: Option<&str>,
142    depth: Option<u64>,
143) -> Result<Content, Error<GetContentError>> {
144    let uri_str = format!(
145        "{}/{org}/{repo}/content",
146        configuration.base_path,
147        org = mesa_dev_oapi::apis::urlencode(org),
148        repo = mesa_dev_oapi::apis::urlencode(repo),
149    );
150    let mut req_builder = configuration.client.request(reqwest::Method::GET, &uri_str);
151
152    if let Some(ref param_value) = r#ref {
153        req_builder = req_builder.query(&[("ref", &param_value.to_string())]);
154    }
155    if let Some(ref param_value) = path {
156        req_builder = req_builder.query(&[("path", &param_value.to_string())]);
157    }
158    if let Some(ref param_value) = depth {
159        req_builder = req_builder.query(&[("depth", &param_value.to_string())]);
160    }
161    if let Some(ref user_agent) = configuration.user_agent {
162        req_builder = req_builder.header(reqwest::header::USER_AGENT, user_agent.clone());
163    }
164    if let Some(ref token) = configuration.bearer_access_token {
165        req_builder = req_builder.bearer_auth(token.to_owned());
166    }
167
168    let req = req_builder.build()?;
169    let resp = configuration.client.execute(req).await?;
170
171    let status = resp.status();
172    tracing::Span::current().record("http.status_code", status.as_u16());
173
174    if !status.is_client_error() && !status.is_server_error() {
175        let text = resp.text().await?;
176        serde_path_to_error::deserialize(&mut serde_json::Deserializer::from_str(&text))
177            .map_err(Error::from)
178    } else {
179        let text = resp.text().await?;
180        let entity: Option<GetContentError> = serde_json::from_str(&text).ok();
181        Err(Error::ResponseError(ResponseContent {
182            status,
183            content: text,
184            entity,
185        }))
186    }
187}