14 files changed
+325
-185
Cargo.lock
+13
@@ -1082,6 +1082,7 @@ dependencies = [
1082
"reqwest",
1083
"serde",
1084
"serde_json",
1085
+ "smbpndk-model",
1086
"smbpndk-networking",
1087
"spinners",
1088
"tokio",
@@ -1091,16 +1092,28 @@ dependencies = [
1092
"url-builder",
1093
]
1094
1095
+[[package]]
1096
+name = "smbpndk-model"
1097
+version = "0.1.0"
1098
+dependencies = [
1099
+ "serde",
1100
+ "serde_json",
1101
+]
1102
+
1103
[[package]]
1104
name = "smbpndk-networking"
1105
version = "0.1.0"
1106
dependencies = [
1107
+ "anyhow",
1108
"async-trait",
1109
+ "dirs",
1110
+ "home",
1111
"log",
1112
"maybe-async",
1113
"reqwest",
1114
"serde",
1115
"serde_json",
1116
+ "smbpndk-model",
1117
"thiserror",
1118
]
1119
Cargo.toml
+2
-1
@@ -1,5 +1,6 @@
1
[workspace]
2
members = [
3
"smbpndk-networking",
4
- "smbpndk-cli"
4
+ "smbpndk-cli",
5
+ "smbpndk-model"
6
]
\ No newline at end of file
smbpndk-cli/Cargo.toml
+2
@@ -8,6 +8,7 @@ repository = "https://github.com/smbpndk/smbpndk-cli"
8
documentation = "https://docs.smbpndk.com/cli"
9
keywords = ["cli", "cloud", "developer", "platform"]
10
categories = ["command-line-utilities"]
11
+readme = "../README.md"
12
13
[[bin]]
14
name = "smb"
@@ -28,6 +29,7 @@ reqwest = { version = "0.11", features = ["json"] }
29
serde = { version = "1", features = ["derive"] }
30
serde_json = "1.0.82"
31
smbpndk-networking = { path = "../smbpndk-networking" }
32
+smbpndk-model = { path = "../smbpndk-model" }
33
spinners = "4.1.0"
34
tokio = { version = "1.14.1", features = ["full"] }
35
tracing = { version = "0.1.35", features = ["log"] }
smbpndk-cli/src/auth_app/cli.rs
+5
-5
@@ -2,27 +2,27 @@ use clap::Subcommand;
2
3
#[derive(Subcommand)]
4
pub enum Commands {
5
- #[clap(about = "Add new project.")]
5
+ #[clap(about = "Add new auth app.")]
6
New {},
7
8
- #[clap(about = "List all your projects.")]
8
+ #[clap(about = "List all your auth apps.")]
9
List {},
10
11
- #[clap(about = "Show detail of a project.")]
11
+ #[clap(about = "Show detail of a auth app.")]
12
Show {
13
/// Project Id
14
#[clap(short, long, required = true)]
15
id: String,
16
},
17
18
- #[clap(about = "Delete a project.")]
18
+ #[clap(about = "Delete a auth app.")]
19
Delete {
20
/// Project name
21
#[clap(short, long, required = true)]
22
id: String,
23
},
24
25
- #[clap(about = "Use project for current CLI session.")]
25
+ #[clap(about = "Use auth app for current CLI session.")]
26
Use {
27
#[clap(short, long, required = true)]
28
id: String,
smbpndk-cli/src/auth_app/mod.rs
+149
-7
@@ -1,24 +1,166 @@
1
-use crate::{auth_app::cli::Commands, util::CommandResult};
2
-use anyhow::Result;
1
+use std::{fs::OpenOptions, io::Write};
2
+
3
+use crate::{auth_app::cli::Commands, debug, util::CommandResult};
4
+use anyhow::{anyhow, Result};
5
+use console::style;
6
+use dialoguer::{theme::ColorfulTheme, Input};
7
+use smbpndk_model::{AuthAppCreate, Config};
8
+use smbpndk_networking::{create_auth_app, delete_auth_app, get_auth_app, get_auth_apps};
9
+use spinners::Spinner;
10
11
pub(crate) mod cli;
12
13
pub async fn process_auth_app(commands: Commands) -> Result<CommandResult> {
14
match commands {
15
Commands::New {} => {
9
- todo!()
16
+ let project_name = Input::<String>::with_theme(&ColorfulTheme::default())
17
+ .with_prompt("Project name")
18
+ .interact()
19
+ .unwrap();
20
+ let description = Input::<String>::with_theme(&ColorfulTheme::default())
21
+ .with_prompt("Description")
22
+ .interact()
23
+ .unwrap();
24
+
25
+ let spinner = Spinner::new(
26
+ spinners::Spinners::SimpleDotsScrolling,
27
+ style("Creating project...").green().bold().to_string(),
28
+ );
29
+
30
+ match create_auth_app(AuthAppCreate {
31
+ name: project_name.clone(),
32
+ description: description.clone(),
33
+ })
34
+ .await
35
+ {
36
+ Ok(_) => Ok(CommandResult {
37
+ spinner,
38
+ symbol: "✅".to_owned(),
39
+ msg: format!("Creating a project {project_name}."),
40
+ }),
41
+ Err(e) => {
42
+ println!("Error: {e:#?}");
43
+ Ok(CommandResult {
44
+ spinner,
45
+ symbol: "❌".to_owned(),
46
+ msg: format!("Failed to create a project {project_name}."),
47
+ })
48
+ }
49
+ }
50
}
51
Commands::List {} => {
12
- todo!()
52
+ let spinner = Spinner::new(
53
+ spinners::Spinners::SimpleDotsScrolling,
54
+ style("Loading...").green().bold().to_string(),
55
+ );
56
+
57
+ // Get all
58
+ match get_auth_apps().await {
59
+ Ok(auth_apps) => {
60
+ println!("auth_apps: {auth_apps:#?}");
61
+ println!(
62
+ "{0: <5} | {1: <20} | {2: <30} | {3: <30}",
63
+ "ID", "Name", "Created at", "Updated at"
64
+ );
65
+ for project in auth_apps {
66
+ println!(
67
+ "{0: <5} | {1: <20} | {2: <30} | {3: <30}",
68
+ project.id, project.name, project.created_at, project.updated_at
69
+ );
70
+ }
71
+ Ok(CommandResult {
72
+ spinner,
73
+ symbol: "✅".to_owned(),
74
+ msg: "Showing all projects.".to_owned(),
75
+ })
76
+ }
77
+ Err(e) => {
78
+ println!("Error: {e:#?}");
79
+ Ok(CommandResult {
80
+ spinner,
81
+ symbol: "❌".to_owned(),
82
+ msg: "Failed to get all projects.".to_owned(),
83
+ })
84
+ }
85
+ }
86
}
87
Commands::Show { id } => {
15
- todo!()
88
+ let spinner = Spinner::new(
89
+ spinners::Spinners::SimpleDotsScrolling,
90
+ style("Loading...").green().bold().to_string(),
91
+ );
92
+ // Get Detail
93
+ match get_auth_app(id).await {
94
+ Ok(_) => Ok(CommandResult {
95
+ spinner,
96
+ symbol: "✅".to_owned(),
97
+ msg: "Showing all projects.".to_owned(),
98
+ }),
99
+ Err(e) => {
100
+ println!("Error: {e:#?}");
101
+ Ok(CommandResult {
102
+ spinner,
103
+ symbol: "❌".to_owned(),
104
+ msg: "Failed to get all projects.".to_owned(),
105
+ })
106
+ }
107
+ }
108
}
109
Commands::Delete { id } => {
18
- todo!()
110
+ let spinner = Spinner::new(
111
+ spinners::Spinners::SimpleDotsScrolling,
112
+ style("Loading...").green().bold().to_string(),
113
+ );
114
+ // Get Detail
115
+ match delete_auth_app(id).await {
116
+ Ok(_) => Ok(CommandResult {
117
+ spinner,
118
+ symbol: "✅".to_owned(),
119
+ msg: "Showing all projects.".to_owned(),
120
+ }),
121
+ Err(e) => {
122
+ println!("Error: {e:#?}");
123
+ Ok(CommandResult {
124
+ spinner,
125
+ symbol: "❌".to_owned(),
126
+ msg: "Failed to get all projects.".to_owned(),
127
+ })
128
+ }
129
+ }
130
}
131
Commands::Use { id } => {
21
- todo!()
132
+ let project = get_auth_app(id).await?;
133
+
134
+ let config = Config {
135
+ current_project: None,
136
+ current_auth_app: Some(project),
137
+ };
138
+
139
+ let spinner = Spinner::new(
140
+ spinners::Spinners::SimpleDotsScrolling,
141
+ style("Loading...").green().bold().to_string(),
142
+ );
143
+ match home::home_dir() {
144
+ Some(path) => {
145
+ debug!(path.to_str().unwrap());
146
+ let mut file = OpenOptions::new()
147
+ .create(true)
148
+ .write(true)
149
+ .open([path.to_str().unwrap(), "/.smb/config.json"].join(""))?;
150
+ let json = serde_json::to_string(&config)?;
151
+ file.write_all(json.as_bytes())?;
152
+
153
+ Ok(CommandResult {
154
+ spinner,
155
+ symbol: "✅".to_owned(),
156
+ msg: "Use project successful.".to_string(),
157
+ })
158
+ }
159
+ None => {
160
+ let error = anyhow!("Failed to get home directory.");
161
+ Err(error)
162
+ }
163
+ }
164
}
165
}
166
}
smbpndk-cli/src/projects/crud/mod.rs
+2
-30
@@ -1,25 +1,9 @@
1
use crate::{constants::BASE_URL, debug};
2
use anyhow::{anyhow, Result};
3
use reqwest::Client;
4
-use serde::{Deserialize, Serialize};
4
6
-#[derive(Deserialize, Debug, Serialize)]
7
-pub struct Project {
8
- pub id: i32,
9
- pub name: String,
10
- pub created_at: String,
11
- pub updated_at: String,
12
-}
13
-#[derive(Serialize, Debug)]
14
-pub struct ProjectCreate {
15
- pub name: String,
16
- pub description: String,
17
-}
18
-
19
-#[derive(Deserialize, Debug, Serialize)]
20
-pub struct Config {
21
- pub(crate) current_project: Option<Project>,
22
-}
5
+use smbpndk_model::{Project, ProjectCreate};
6
+use smbpndk_networking::get_token;
7
8
pub async fn get_all() -> Result<Vec<Project>> {
9
// Get current token
@@ -111,15 +95,3 @@ pub async fn delete_project(id: String) -> Result<()> {
95
}
96
}
97
}
114
-
115
-async fn get_token() -> Result<String> {
116
- if let Some(mut path) = dirs::home_dir() {
117
- path.push(".smb/token");
118
- std::fs::read_to_string(path).map_err(|e| {
119
- debug!("Error while reading token: {}", &e);
120
- anyhow!("Are you logged in?")
121
- })
122
- } else {
123
- Err(anyhow!("Failed to get home directory."))
124
- }
125
-}
smbpndk-cli/src/projects/mod.rs
+3
-1
@@ -6,11 +6,12 @@ use anyhow::{anyhow, Result};
6
use clap::Subcommand;
7
use console::style;
8
use dialoguer::{theme::ColorfulTheme, Input};
9
+use smbpndk_model::{Config, ProjectCreate};
10
use spinners::Spinner;
11
12
use crate::{debug, util::CommandResult};
13
13
-use self::crud::{create_project, delete_project, get_all, get_project, Config, ProjectCreate};
14
+use self::crud::{create_project, delete_project, get_all, get_project};
15
16
#[derive(Subcommand)]
17
pub enum Commands {
@@ -159,6 +160,7 @@ pub async fn process_projects(commands: Commands) -> Result<CommandResult> {
160
161
let config = Config {
162
current_project: Some(project),
163
+ current_auth_app: None,
164
};
165
166
let spinner = Spinner::new(
smbpndk-model/Cargo.toml
new
+10
@@ -0,0 +1,10 @@
1
+[package]
2
+name = "smbpndk-model"
3
+version = "0.1.0"
4
+edition = "2021"
5
+
6
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
7
+
8
+[dependencies]
9
+serde = { version = "1", features = ["derive"] }
10
+serde_json = "1.0.82"
\ No newline at end of file
smbpndk-model/src/lib.rs
new
+47
@@ -0,0 +1,47 @@
1
+use serde::{Deserialize, Serialize};
2
+
3
+#[derive(Serialize, Deserialize, Debug)]
4
+pub struct AuthApp {
5
+ pub id: i32,
6
+ pub name: String,
7
+ pub created_at: String,
8
+ pub updated_at: String,
9
+}
10
+
11
+#[derive(Serialize, Deserialize, Debug)]
12
+pub struct AuthAppCreate {
13
+ pub name: String,
14
+ pub description: String,
15
+}
16
+
17
+#[derive(Deserialize, Debug, Serialize)]
18
+pub struct Config {
19
+ pub current_project: Option<Project>,
20
+ pub current_auth_app: Option<AuthApp>,
21
+}
22
+
23
+#[derive(Deserialize, Debug, Serialize)]
24
+pub struct Project {
25
+ pub id: i32,
26
+ pub name: String,
27
+ pub created_at: String,
28
+ pub updated_at: String,
29
+}
30
+#[derive(Serialize, Debug)]
31
+pub struct ProjectCreate {
32
+ pub name: String,
33
+ pub description: String,
34
+}
35
+
36
+#[cfg(test)]
37
+mod tests {
38
+ use super::*;
39
+
40
+ #[test]
41
+ fn it_parse_auth_app() {
42
+ let data = r#"{
43
+
44
+ }"#;
45
+ //let auth_app = serde_json::from_value(data.into());
46
+ }
47
+}
smbpndk-networking/Cargo.toml
+6
-1
@@ -2,6 +2,7 @@
2
name = "smbpndk-networking"
3
version = "0.1.0"
4
edition = "2021"
5
+readme = "README.md"
6
7
[dependencies]
8
reqwest = { version = "0.11", features = ["json"] }
@@ -10,4 +11,8 @@ serde_json = "1.0.82"
11
thiserror = "1.0.29"
12
async-trait = { version = "0.1.51", optional = true }
13
log = "0.4.14"
13
-maybe-async = { version = "0.2", features = ["is_sync"] }
\ No newline at end of file
14
+maybe-async = { version = "0.2", features = ["is_sync"] }
15
+home = "0.5.4"
16
+anyhow = "1.0.58"
17
+dirs = "4.0.0"
18
+smbpndk-model = { path = "../smbpndk-model" }
\ No newline at end of file
smbpndk-networking/README.md
new
+3
@@ -0,0 +1,3 @@
1
+# smbpndk-networking
2
+
3
+This crate inspired by this excelent blog [post](https://nullderef.com/blog/web-api-client/).
\ No newline at end of file
smbpndk-networking/src/common.rs
deleted
-60
@@ -1,60 +0,0 @@
1
-use std::collections::HashMap;
2
-use std::fmt;
3
-
4
-use maybe_async::maybe_async;
5
-use serde_json::Value;
6
-
7
-pub type Headers = HashMap<String, String>;
8
-pub type Query<'a> = HashMap<&'a str, &'a str>;
9
-pub type Form<'a> = HashMap<&'a str, &'a str>;
10
-
11
-/// This trait represents the interface to be implemented for an HTTP client,
12
-/// which is kept separate from the Spotify client for cleaner code. Thus, it
13
-/// also requires other basic traits that are needed for the Spotify client.
14
-///
15
-/// When a request doesn't need to pass parameters, the empty or default value
16
-/// of the payload type should be passed, like `json!({})` or `Query::new()`.
17
-/// This avoids using `Option<T>` because `Value` itself may be null in other
18
-/// different ways (`Value::Null`, an empty `Value::Object`...), so this removes
19
-/// redundancy and edge cases (a `Some(Value::Null), for example, doesn't make
20
-/// much sense).
21
-#[maybe_async]
22
-pub trait BaseHttpClient: Send + Default + Clone + fmt::Debug {
23
- type Error;
24
-
25
- // This internal function should always be given an object value in JSON.
26
- async fn get(
27
- &self,
28
- url: &str,
29
- headers: Option<&Headers>,
30
- payload: &Query,
31
- ) -> Result<String, Self::Error>;
32
-
33
- async fn post(
34
- &self,
35
- url: &str,
36
- headers: Option<&Headers>,
37
- payload: &Value,
38
- ) -> Result<String, Self::Error>;
39
-
40
- async fn post_form(
41
- &self,
42
- url: &str,
43
- headers: Option<&Headers>,
44
- payload: &Form<'_>,
45
- ) -> Result<String, Self::Error>;
46
-
47
- async fn put(
48
- &self,
49
- url: &str,
50
- headers: Option<&Headers>,
51
- payload: &Value,
52
- ) -> Result<String, Self::Error>;
53
-
54
- async fn delete(
55
- &self,
56
- url: &str,
57
- headers: Option<&Headers>,
58
- payload: &Value,
59
- ) -> Result<String, Self::Error>;
60
-}
smbpndk-networking/src/constants.rs
new
+5
@@ -0,0 +1,5 @@
1
+#[cfg(debug_assertions)]
2
+pub const BASE_URL: &str = "http://localhost:3000";
3
+
4
+#[cfg(not(debug_assertions))]
5
+pub const BASE_URL: &str = "https://api.smbpndk.com";
smbpndk-networking/src/lib.rs
+78
-80
@@ -1,100 +1,98 @@
1
-mod common;
1
+mod constants;
2
3
-//use async_trait::async_trait;
4
-use common::{BaseHttpClient, Form, Headers, Query};
5
-use maybe_async::async_impl;
6
-use reqwest::Method;
7
-use reqwest::RequestBuilder;
8
-use serde_json::Value;
9
-use std::time::Duration;
3
+use anyhow::{anyhow, Result};
4
+use constants::BASE_URL;
5
+use log::debug;
6
+use reqwest::Client;
7
+use smbpndk_model::{AuthApp, AuthAppCreate};
8
11
-pub trait HttpClient: Send + Default {
12
- type Error;
13
-
14
- fn get(
15
- &self,
16
- url: &str,
17
- headers: Option<&Headers>,
18
- payload: &Query,
19
- ) -> Result<String, Self::Error>;
20
-
21
- fn post(
22
- &self,
23
- url: &str,
24
- headers: Option<&Headers>,
25
- payload: &Value,
26
- ) -> Result<String, Self::Error>;
9
+pub async fn get_token() -> Result<String> {
10
+ if let Some(mut path) = dirs::home_dir() {
11
+ path.push(".smb/token");
12
+ std::fs::read_to_string(path).map_err(|e| {
13
+ debug!("Error while reading token: {}", &e);
14
+ anyhow!("Are you logged in?")
15
+ })
16
+ } else {
17
+ Err(anyhow!("Failed to get home directory."))
18
+ }
19
}
20
29
-#[derive(thiserror::Error, Debug)]
30
-pub enum ReqwestError {
31
- /// The request couldn't be completed because there was an error when trying
32
- /// to do so
33
- #[error("request: {0}")]
34
- Client(#[from] reqwest::Error),
21
+pub async fn get_auth_apps() -> Result<Vec<AuthApp>> {
22
+ // Get current token
23
+ let token = get_token().await.unwrap();
24
36
- /// The request was made, but the server returned an unsuccessful status
37
- /// code, such as 404 or 503. In some cases, the response may contain a
38
- /// custom message from Spotify with more information, which can be
39
- /// serialized into `rspotify_model::ApiError`.
40
- #[error("status code {}", reqwest::Response::status(.0))]
41
- StatusCode(reqwest::Response),
42
-}
25
+ let response = Client::new()
26
+ .get([BASE_URL, "v1/auth_apps"].join(""))
27
+ .header("Authorization", token)
28
+ .send()
29
+ .await?;
30
44
-pub struct ReqwestClient {
45
- // An instance of reqwest::Client.
46
- client: reqwest::Client,
31
+ match response.status() {
32
+ reqwest::StatusCode::OK => {
33
+ let projects: Vec<AuthApp> = response.json().await?;
34
+ Ok(projects)
35
+ }
36
+ _ => Err(anyhow!("Failed to fetch auth apps.")),
37
+ }
38
}
39
49
-impl Default for ReqwestClient {
50
- fn default() -> Self {
51
- let client = reqwest::ClientBuilder::new()
52
- .user_agent("smbpndk-cli")
53
- .timeout(Duration::from_secs(10))
54
- .build()
55
- .unwrap();
56
- Self { client }
40
+pub async fn get_auth_app(id: String) -> Result<AuthApp> {
41
+ // Get current token
42
+ let token = get_token().await.unwrap();
43
+
44
+ let response = Client::new()
45
+ .get([BASE_URL, "v1/auth_apps/", &id].join(""))
46
+ .header("Authorization", token)
47
+ .send()
48
+ .await?;
49
+
50
+ match response.status() {
51
+ reqwest::StatusCode::OK => {
52
+ let project: AuthApp = response.json().await?;
53
+ println!("Project requested: {project:#?}");
54
+ Ok(project)
55
+ }
56
+ _ => Err(anyhow!("Failed to request an auth app.")),
57
}
58
}
59
60
-impl ReqwestClient {
61
- async fn request<D>(
62
- &self,
63
- method: Method,
64
- url: &str,
65
- headers: Option<&Headers>,
66
- add_data: D,
67
- ) -> Result<String, ReqwestError>
68
- where
69
- D: Fn(RequestBuilder) -> RequestBuilder,
70
- {
71
- let mut request = self.client.request(method.clone(), url);
60
+pub async fn delete_auth_app(id: String) -> Result<()> {
61
+ // Get current token
62
+ let token = get_token().await?;
63
73
- // Setting the headers, if any
74
- if let Some(headers) = headers {
75
- // The headers need to be converted into a `reqwest::HeaderMap`,
76
- // which won't fail as long as its contents are ASCII. This is an
77
- // internal function, so the condition cannot be broken by the user
78
- // and will always be true.
79
- //
80
- // The content-type header will be set automatically.
81
- let headers = headers.try_into().unwrap();
64
+ let response = Client::new()
65
+ .delete([BASE_URL, "v1/auth_apps/", &id].join(""))
66
+ .header("Authorization", token)
67
+ .send()
68
+ .await?;
69
83
- request = request.headers(headers);
70
+ match response.status() {
71
+ reqwest::StatusCode::OK => {
72
+ debug!("Project deleted.");
73
+ Ok(())
74
}
75
+ _ => Err(anyhow!("Failed to delete a project.")),
76
+ }
77
+}
78
86
- // Configuring the request for the specific type (get/post/put/delete)
87
- request = add_data(request);
79
+pub async fn create_auth_app(auth_app: AuthAppCreate) -> Result<AuthApp> {
80
+ // Get current token
81
+ let token = get_token().await?;
82
89
- // Finally performing the request and handling the response
90
- log::info!("Making request {:?}", request);
91
- let response = request.send().await?;
83
+ let response = Client::new()
84
+ .post([BASE_URL, "v1/auth_apps"].join(""))
85
+ .json(&auth_app)
86
+ .header("Authorization", token)
87
+ .send()
88
+ .await?;
89
93
- // Making sure that the status code is OK
94
- if response.status().is_success() {
95
- response.text().await.map_err(Into::into)
96
- } else {
97
- Err(ReqwestError::StatusCode(response))
90
+ match response.status() {
91
+ reqwest::StatusCode::CREATED => {
92
+ let project: AuthApp = response.json().await?;
93
+ println!("Project created: {project:#?}");
94
+ Ok(project)
95
}
96
+ _ => Err(anyhow!("Failed to create a project.")),
97
}
98
}