forc_publish/
error.rs

1use reqwest::StatusCode;
2use serde::Deserialize;
3
4pub type Result<T> = std::result::Result<T, Error>;
5
6#[derive(Debug, thiserror::Error)]
7pub enum Error {
8    #[error("I/O error: {0}")]
9    IoError(#[from] std::io::Error),
10
11    #[error("Json error: {0}")]
12    JsonError(#[from] serde_json::Error),
13
14    #[error("HTTP error: {0}")]
15    HttpError(#[from] reqwest::Error),
16
17    #[error("TOML error: {0}")]
18    TomlError(#[from] toml::ser::Error),
19
20    #[error("URL error: {0}")]
21    UrlError(#[from] url::ParseError),
22
23    #[error("Failed to get relative path")]
24    RelativePathError(#[from] std::path::StripPrefixError),
25
26    #[error("{error}")]
27    ApiResponseError { status: StatusCode, error: String },
28
29    #[error("Forc.toml not found in the current directory")]
30    ForcTomlNotFound,
31
32    #[error("Invalid forc.toml: {0}")]
33    InvalidForcToml(#[from] anyhow::Error),
34
35    #[error("missing version field in [project] section of Forc.toml")]
36    MissingVersionField,
37
38    #[error("Workspace is not supported yet, deploy each member separately")]
39    WorkspaceNotSupported,
40
41    #[error("{0} is not a forc.pub dependency, depend on it using version.")]
42    DependencyMissingVersion(String),
43
44    #[error("Server error")]
45    ServerError,
46
47    #[error("Readme pre-process error: {0}")]
48    MDPreProcessError(#[from] crate::md_pre_process::error::MDPreProcessError),
49}
50
51#[derive(Deserialize)]
52pub struct ApiErrorResponse {
53    error: String,
54}
55
56impl Error {
57    /// Converts a `reqwest::Response` into an `ApiError`
58    pub async fn from_response(response: reqwest::Response) -> Self {
59        let status = response.status();
60        match response.json::<ApiErrorResponse>().await {
61            Ok(parsed_error) => Error::ApiResponseError {
62                status,
63                error: parsed_error.error,
64            },
65            Err(err) => Error::ApiResponseError {
66                status,
67                error: format!("Unexpected API error: {err}"),
68            },
69        }
70    }
71}
72
73#[cfg(test)]
74mod test {
75    use super::*;
76    use reqwest::StatusCode;
77    use serde_json::json;
78    use wiremock::matchers::{method, path};
79    use wiremock::{Mock, MockServer, ResponseTemplate};
80
81    #[tokio::test]
82    async fn test_error_from_response_with_valid_json() {
83        let mock_server = MockServer::start().await;
84
85        // Simulated JSON API error response
86        let error_json = json!({
87            "error": "Invalid request data"
88        });
89
90        Mock::given(method("POST"))
91            .and(path("/test"))
92            .respond_with(ResponseTemplate::new(400).set_body_json(&error_json))
93            .mount(&mock_server)
94            .await;
95
96        let client = reqwest::Client::new();
97        let response = client
98            .post(format!("{}/test", mock_server.uri()))
99            .send()
100            .await
101            .unwrap();
102
103        let error = Error::from_response(response).await;
104
105        match error {
106            Error::ApiResponseError { status, error } => {
107                assert_eq!(status, StatusCode::BAD_REQUEST);
108                assert_eq!(error, "Invalid request data");
109            }
110            _ => panic!("Expected ApiResponseError"),
111        }
112    }
113
114    #[tokio::test]
115    async fn test_error_from_response_with_invalid_json() {
116        let mock_server = MockServer::start().await;
117
118        // Simulated invalid JSON response (causing deserialization failure)
119        let invalid_json = "not a json object";
120
121        Mock::given(method("POST"))
122            .and(path("/test"))
123            .respond_with(ResponseTemplate::new(500).set_body_string(invalid_json))
124            .mount(&mock_server)
125            .await;
126
127        let client = reqwest::Client::new();
128        let response = client
129            .post(format!("{}/test", mock_server.uri()))
130            .send()
131            .await
132            .unwrap();
133
134        let error = Error::from_response(response).await;
135
136        match error {
137            Error::ApiResponseError { status, error } => {
138                assert_eq!(status, StatusCode::INTERNAL_SERVER_ERROR);
139                assert_eq!(error, "Unexpected API error: error decoding response body");
140            }
141            _ => panic!("Expected ApiResponseError"),
142        }
143    }
144}