@smbcloud / cli / commits / 946c948

add error response

Seto Elkahfi committed May 31, 2025 at 16:16 UTC 946c94874f9433a6fa9ac28dda0afd6732bc5390
15 files changed +234 -48
Cargo.lock
+23 -2
@@ -1691,6 +1691,8 @@ dependencies = [
1691 "serde_json",
1692 "serde_repr",
1693 "spinners",
1694 + "strum 0.27.1",
1695 + "strum_macros 0.27.1",
1696 ]
1697
1698 [[package]]
@@ -1762,7 +1764,7 @@ checksum = "a0ef947f358b9c238923f764c72a4a9d42f2d637c46e059dbd319d6e7cfb4f82"
1764 dependencies = [
1765 "lazy_static",
1766 "maplit",
1765 - "strum",
1767 + "strum 0.24.1",
1768 ]
1769
1770 [[package]]
@@ -1783,9 +1785,15 @@ version = "0.24.1"
1785 source = "registry+https://github.com/rust-lang/crates.io-index"
1786 checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f"
1787 dependencies = [
1786 - "strum_macros",
1788 + "strum_macros 0.24.3",
1789 ]
1790
1791 +[[package]]
1792 +name = "strum"
1793 +version = "0.27.1"
1794 +source = "registry+https://github.com/rust-lang/crates.io-index"
1795 +checksum = "f64def088c51c9510a8579e3c5d67c65349dcf755e5479ad3d010aa6454e2c32"
1796 +
1797 [[package]]
1798 name = "strum_macros"
1799 version = "0.24.3"
@@ -1799,6 +1807,19 @@ dependencies = [
1807 "syn 1.0.109",
1808 ]
1809
1810 +[[package]]
1811 +name = "strum_macros"
1812 +version = "0.27.1"
1813 +source = "registry+https://github.com/rust-lang/crates.io-index"
1814 +checksum = "c77a8c5abcaf0f9ce05d62342b7d298c346515365c36b673df4ebe3ced01fde8"
1815 +dependencies = [
1816 + "heck 0.5.0",
1817 + "proc-macro2",
1818 + "quote",
1819 + "rustversion",
1820 + "syn 2.0.101",
1821 +]
1822 +
1823 [[package]]
1824 name = "subtle"
1825 version = "2.6.1"
Cargo.toml
+2
@@ -42,3 +42,5 @@ tracing-bunyan-formatter = "0.3.3"
42 tracing-subscriber = "0.3.14"
43 url = "2"
44 url-builder = "0.1.1"
45 +strum = "0.27"
46 +strum_macros = "0.27"
crates/cli/src/deploy/config.rs
+6 -2
@@ -53,12 +53,16 @@ fn handle_config_error() -> Result<Config, ConfigError> {
53 todo!()
54 }
55
56 -pub(crate) async fn check_project(env: Environment, id: i32) -> Result<(), ConfigError> {
56 +pub(crate) async fn check_project(
57 + env: Environment,
58 + access_token: String,
59 + id: i32
60 +) -> Result<(), ConfigError> {
61 let mut spinner: Spinner = Spinner::new(
62 spinners::Spinners::Hamburger,
63 succeed_message("Validate project"),
64 );
61 - match get_project(env, id.to_string()).await {
65 + match get_project(env, access_token, id.to_string()).await {
66 Ok(_) => {
67 spinner.stop_and_persist(&succeed_symbol(), succeed_message("Valid project"));
68 Ok(())
crates/cli/src/deploy/mod.rs
+5 -2
@@ -16,7 +16,7 @@ use git2::{PushOptions, RemoteCallbacks, Repository};
16 use remote_messages::{build_next_app, start_server};
17 use smbcloud_model::project::DeploymentPayload;
18 use smbcloud_model::project::DeploymentStatus;
19 -use smbcloud_networking::environment::Environment;
19 +use smbcloud_networking::{environment::Environment, get_smb_token};
20 use smbcloud_networking_project::crud_project_deployment_create::create;
21 use spinners::Spinner;
22
@@ -26,11 +26,14 @@ pub async fn process_deploy(env: Environment) -> Result<CommandResult> {
26 let _ = process_login(env).await;
27 }
28
29 + // Get current token
30 + let access_token = get_smb_token(env).await?;
31 +
32 // Check config.
33 let config = check_config(env).await?;
34
35 // Validate config with project.
33 - check_project(env, config.project.id).await?;
36 + check_project(env, access_token, config.project.id).await?;
37
38 // Check remote repository setup.
39 let repo = match Repository::open(".") {
crates/cli/src/project/crud_read.rs
+3 -2
@@ -4,7 +4,7 @@ use crate::{
4 };
5 use anyhow::{anyhow, Result};
6 use smbcloud_model::project::Project;
7 -use smbcloud_networking::environment::Environment;
7 +use smbcloud_networking::{environment::Environment, get_smb_token};
8 use smbcloud_networking_project::{get_all, get_project};
9 use spinners::Spinner;
10 use tabled::{Table, Tabled};
@@ -65,7 +65,8 @@ pub async fn process_project_show(env: Environment, id: String) -> Result<Comman
65 spinners::Spinners::SimpleDotsScrolling,
66 succeed_message("Loading"),
67 );
68 - match get_project(env, id).await {
68 + let access_token = get_smb_token(env).await?;
69 + match get_project(env, access_token, id).await {
70 Ok(project) => {
71 spinner.stop_and_persist(&succeed_symbol(), succeed_message("Loaded."));
72 let message = succeed_message(&format!("Showing project {}.", &project.name));
crates/cli/src/project/deployment.rs
+6 -3
@@ -5,7 +5,7 @@ use crate::{
5 };
6 use anyhow::Result;
7 use smbcloud_model::project::Deployment;
8 -use smbcloud_networking::environment::Environment;
8 +use smbcloud_networking::{environment::Environment, get_smb_token};
9 use smbcloud_networking_project::crud_project_deployment_read::{
10 get_deployment_detail, list_deployments,
11 };
@@ -21,7 +21,9 @@ pub(crate) async fn process_deployment(
21 // Load project id from .smb/config.toml
22 let config = check_config(env).await?;
23
24 - check_project(env, config.project.id).await?;
24 + let access_token = get_smb_token(env).await?;
25 +
26 + check_project(env,access_token, config.project.id).await?;
27
28 if let Some(deployment_id) = id {
29 // Show detail for a specific deployment
@@ -31,7 +33,8 @@ pub(crate) async fn process_deployment(
33 show_deployment_detail(&deployment);
34 } else {
35 // List all deployments for the project
34 - let deployments = list_deployments(env, config.project.id).await?;
36 + let access_token = get_smb_token(env).await?;
37 + let deployments = list_deployments(env, access_token, config.project.id).await?;
38 spinner.stop_and_persist(&succeed_symbol(), succeed_message("Load all deployments"));
39 show_project_deployments(&deployments);
40 };
crates/cli/src/project/mod.rs
+1 -1
@@ -30,7 +30,7 @@ pub async fn process_project(env: Environment, commands: Commands) -> Result<Com
30 Commands::Show { id } => process_project_show(env, id).await,
31 Commands::Delete { id } => process_project_delete(env, id).await,
32 Commands::Use { id } => {
33 - let project = get_project(env, id).await?;
33 + let project = get_project(env,"access token".to_owned(), id).await?;
34
35 let config = Config {
36 current_project: Some(project),
crates/smbcloud-model/Cargo.toml
+2
@@ -23,3 +23,5 @@ serde = { workspace = true, features = ["derive"] }
23 serde_json = { workspace = true }
24 serde_repr = { workspace = true }
25 spinners = { workspace = true }
26 +strum = { workspace = true }
27 +strum_macros = { workspace = true }
crates/smbcloud-model/src/error_codes.rs new
+71
@@ -0,0 +1,71 @@
1 +use core::fmt;
2 +use std::{
3 + error::Error,
4 + fmt::{Display, Formatter},
5 +};
6 +
7 +use serde::{Deserialize, Serialize};
8 +use serde_repr::{Deserialize_repr, Serialize_repr};
9 +use strum_macros::EnumIter;
10 +
11 +#[derive(Serialize, Deserialize, Debug)]
12 +#[serde(untagged)]
13 +pub enum ErrorResponse {
14 + Error {
15 + error_code: ErrorCode,
16 + message: String,
17 + },
18 +}
19 +
20 +impl Error for ErrorResponse {}
21 +
22 +impl Display for ErrorResponse {
23 + fn fmt(&self, f: &mut Formatter) -> fmt::Result {
24 + write!(f, "{:?}", self)
25 + }
26 +}
27 +
28 +#[derive(Serialize_repr, Deserialize_repr, Debug, EnumIter)]
29 +#[repr(i32)]
30 +pub enum ErrorCode {
31 + Unknown = 0,
32 + ParseError = 1,
33 + NetworkError = 2,
34 + // Projects
35 + ProjectNotFound = 1000
36 +}
37 +
38 +impl Display for ErrorCode {
39 + fn fmt(&self, f: &mut Formatter) -> fmt::Result {
40 + write!(f, "{:?}", self)
41 + }
42 +}
43 +
44 +impl ErrorCode {
45 + /// Cannot expose the ErrorCode enum directly to Ruby,
46 + /// so we need to get it from i32.
47 + pub fn from_i32(value: i32) -> Self {
48 + match value {
49 + // Projects
50 + 1000 => ErrorCode::ProjectNotFound,
51 + // Fallback
52 + 2 => ErrorCode::ParseError,
53 + 1 => ErrorCode::NetworkError,
54 + _ => ErrorCode::Unknown,
55 + }
56 + }
57 +
58 + pub fn message(&self, l: Option<String>) -> &str {
59 + print!("Language code: {:?}, {}", l, self);
60 + match self {
61 + ErrorCode::Unknown => "Unknown error.",
62 + ErrorCode::ProjectNotFound => "Project not found.",
63 + ErrorCode::ParseError => "Parse error.",
64 + ErrorCode::NetworkError => "Network error.",
65 + }
66 + }
67 +
68 + pub fn rb_constant_name(&self) -> String {
69 + self.to_string()
70 + }
71 +}
crates/smbcloud-model/src/lib.rs
+1
@@ -4,6 +4,7 @@ pub mod forgot;
4 pub mod login;
5 pub mod project;
6 pub mod signup;
7 +pub mod error_codes;
8
9 pub mod ar_date_format {
10 use chrono::{DateTime, Utc};
crates/smbcloud-networking-project/src/crud_project_deployment_read.rs
+11 -17
@@ -1,26 +1,20 @@
1 use crate::{build_project_deployment, build_project_deployment_index};
2 use anyhow::{anyhow, Result};
3 use reqwest::Client;
4 -use smbcloud_model::project::Deployment;
5 -use smbcloud_networking::{environment::Environment, get_smb_token};
4 +use smbcloud_model::{error_codes::ErrorResponse, project::Deployment};
5 +use smbcloud_networking::{constants::SMB_USER_AGENT, environment::Environment, get_smb_token, network::request};
6
7 -pub async fn list_deployments(env: Environment, project_id: i32) -> Result<Vec<Deployment>> {
8 - let token = get_smb_token(env).await?;
9 -
10 - let response = Client::new()
7 +pub async fn list_deployments(
8 + env: Environment,
9 + access_token: String,
10 + project_id: i32
11 +) -> Result<Vec<Deployment>, ErrorResponse> {
12 + let builder = Client::new()
13 .get(build_project_deployment_index(env, project_id.to_string()))
12 - .header("Authorization", token)
13 - .header("User-agent", "smbcloud-cli")
14 - .send()
15 - .await?;
14 + .header("Authorization", access_token)
15 + .header("User-agent", SMB_USER_AGENT);
16
17 - match response.status() {
18 - reqwest::StatusCode::OK => {
19 - let deployments: Vec<Deployment> = response.json().await?;
20 - Ok(deployments)
21 - }
22 - _ => Err(anyhow!("Failed to list deployments.")),
23 - }
17 + request(builder).await
18 }
19
20 pub async fn get_deployment_detail(
crates/smbcloud-networking-project/src/lib.rs
+11 -19
@@ -5,10 +5,9 @@ use anyhow::{anyhow, Result};
5 use log::debug;
6 use reqwest::{Client, StatusCode};
7 use smbcloud_model::{
8 - self,
9 - project::{Project, ProjectCreate},
8 + self, error_codes::ErrorResponse, project::{Project, ProjectCreate}
9 };
11 -use smbcloud_networking::{environment::Environment, get_smb_token, smb_base_url_builder};
10 +use smbcloud_networking::{constants::SMB_USER_AGENT, environment::Environment, get_smb_token, network::request, smb_base_url_builder};
11
12 pub async fn get_all(env: Environment) -> Result<Vec<Project>> {
13 // Get current token
@@ -53,24 +52,17 @@ pub async fn create_project(env: Environment, project: ProjectCreate) -> Result<
52 }
53 }
54
56 -pub async fn get_project(env: Environment, id: String) -> Result<Project> {
57 - // Get current token
58 - let token = get_smb_token(env).await?;
59 -
60 - let response = Client::new()
55 +pub async fn get_project(
56 + env: Environment,
57 + access_token: String,
58 + id: String
59 +) -> Result<Project, ErrorResponse> {
60 + let builder = Client::new()
61 .get(build_project_url_with_id(env, id))
62 - .header("Authorization", token)
63 - .send()
64 - .await?;
62 + .header("Authorization", access_token)
63 + .header("User-agent", SMB_USER_AGENT);
64
66 - match response.status() {
67 - reqwest::StatusCode::OK => {
68 - let project: Project = response.json().await?;
69 - //println!("Project requested: {project:#?}");
70 - Ok(project)
71 - }
72 - _ => Err(anyhow!("Failed to request a project.")),
73 - }
65 + request(builder).await?
66 }
67
68 pub async fn delete_project(env: Environment, id: String) -> Result<()> {
crates/smbcloud-networking/src/constants.rs
+1
@@ -8,6 +8,7 @@ pub const GH_OAUTH_REDIRECT_PORT: &str = "8808";
8
9 pub const SMB_CLIENT_ID: &str = "cli";
10 pub const SMB_CLIENT_SECRET: &str = "secretttttttt";
11 +pub const SMB_USER_AGENT: &str = "smbcloud-cli";
12
13 // Paths
14 pub const PATH_USERS_ME: &str = "v1/me";
crates/smbcloud-networking/src/lib.rs
+1
@@ -1,5 +1,6 @@
1 pub mod constants;
2 pub mod environment;
3 +pub mod network;
4
5 use anyhow::{anyhow, Result};
6 use constants::{SMB_CLIENT_ID, SMB_CLIENT_SECRET};
crates/smbcloud-networking/src/network.rs new
+90
@@ -0,0 +1,90 @@
1 +
2 +use log::{debug, error};
3 +use reqwest::{RequestBuilder, Response};
4 +use serde::de::DeserializeOwned;
5 +use smbcloud_model::error_codes::{ErrorCode, ErrorResponse};
6 +#[cfg(debug_assertions)]
7 +const LOG_RESPONSE_BODY: bool = true; // You know what to do here.
8 +#[cfg(not(debug_assertions))]
9 +const LOG_RESPONSE_BODY: bool = false;
10 +
11 +pub async fn parse_error_response<T: DeserializeOwned>(
12 + response: Response,
13 +) -> Result<T, ErrorResponse> {
14 + let response_body = match response.text().await {
15 + Ok(body) => body,
16 + Err(e) => {
17 + error!("Failed to get response body: {:?}", e);
18 + return Err(ErrorResponse::Error {
19 + error_code: ErrorCode::NetworkError,
20 + message: ErrorCode::NetworkError.message(None).to_string(),
21 + });
22 + }
23 + };
24 +
25 + if LOG_RESPONSE_BODY {
26 + println!("{:?}", serde_json::to_string_pretty(&response_body));
27 + }
28 +
29 + let e = match serde_json::from_str::<ErrorResponse>(&response_body) {
30 + Ok(json) => json,
31 + Err(e) => {
32 + error!("Failed to parse error response: {:?}", e);
33 + return Err(ErrorResponse::Error {
34 + error_code: ErrorCode::ParseError,
35 + message: ErrorCode::ParseError.message(None).to_string(),
36 + });
37 + }
38 + };
39 + error!("Error response: {:?}", e);
40 + return Err(e);
41 +}
42 +
43 +pub async fn request<R: DeserializeOwned>(builder: RequestBuilder) -> Result<R, ErrorResponse> {
44 + let response = builder.send().await;
45 + let response = match response {
46 + Ok(response) => response,
47 + Err(e) => {
48 + error!("Failed to get response: {:?}", e);
49 + return Err(ErrorResponse::Error {
50 + error_code: ErrorCode::NetworkError,
51 + message: ErrorCode::NetworkError.message(None).to_string(),
52 + });
53 + }
54 + };
55 + let response = match response.status() {
56 + reqwest::StatusCode::OK => response,
57 + status => {
58 + error!("Failed to get response: {:?}", status);
59 + return parse_error_response(response).await;
60 + }
61 + };
62 +
63 + let response_body = match response.text().await {
64 + Ok(body) => body,
65 + Err(e) => {
66 + error!("Failed to get response body: {:?}", e);
67 + return Err(ErrorResponse::Error {
68 + error_code: ErrorCode::NetworkError,
69 + message: ErrorCode::NetworkError.message(None).to_string(),
70 + });
71 + }
72 + };
73 +
74 + if LOG_RESPONSE_BODY {
75 + debug!("{:?}", serde_json::to_string_pretty(&response_body));
76 + }
77 +
78 + let response = match serde_json::from_str::<R>(&response_body) {
79 + Ok(response) => response,
80 + Err(e) => {
81 + error!("Failed to parse response: {:?}", e);
82 + return Err(ErrorResponse::Error {
83 + error_code: ErrorCode::ParseError,
84 + message: e.to_string(),
85 + });
86 + }
87 + };
88 +
89 + Ok(response)
90 +}
\ No newline at end of file