diff --git a/src/main.rs b/src/main.rs index bfa96b5..70c462a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -28,7 +28,7 @@ fn main() { } let auth = request::Authentication::new(&config); - let request = auth.request_chooser(config.clone(), matches); + let request = auth.request_chooser(config.clone(), &matches); // TODO: get rid of the clone match request.arg_value.subcommand() { ("", None) => println!("No subcommand was given!"), @@ -76,6 +76,26 @@ fn main() { issue.create_issue(&request); } } + ("user", Some(user_matches)) => { + let user = user::User::new(); + + // TODO: match expression should be here + if user_matches.is_present("create") { + user.create_user(&request); + } + + if user_matches.is_present("list") { + user.list_user(&request); + } + + if user_matches.is_present("search") { + user.search_user(&request); + } + + if user_matches.is_present("delete") { + user.delete_user(&request); + } + } _ => println!("Huh?"), } } diff --git a/src/repo.rs b/src/repo.rs index 105eea5..c79d3a6 100644 --- a/src/repo.rs +++ b/src/repo.rs @@ -1,11 +1,15 @@ -use std::{collections::HashMap, path::{Path, self}, file}; +use std::{collections::HashMap, path::Path}; use colored::*; -use git2::{Repository as Repo, build::RepoBuilder, Credentials, Cred, CredentialType, RemoteCallbacks, FetchOptions}; +use git2::{build::RepoBuilder, Cred, CredentialType, RemoteCallbacks}; use reqwest::StatusCode; use serde_derive::{Deserialize, Serialize}; -use crate::{request::Request, config::Configuration}; +use crate::{ + config::Configuration, + request::Request, + util::{self, ErrorKind}, +}; pub struct Repository; @@ -58,6 +62,12 @@ pub struct RepositoryResponse { pub website: String, } +#[derive(Deserialize)] +pub struct RepositoryErrorResponse { + pub message: String, + pub url: String, +} + #[derive(Debug, Serialize, Deserialize)] pub struct ExternalTracker { pub external_tracker_format: String, @@ -127,11 +137,24 @@ impl Repository { match response { Ok(repo) => match repo.status() { StatusCode::CREATED => { + // TODO: implement error handling for the deserialization let deserialized: RepositoryResponse = repo.json().unwrap(); println!("{}", "Repository successfully created!".green()); println!("\tRepository name: {:0}\n\tRepository owner: {:1}\n\tRepository description: {:2}", deserialized.name, deserialized.owner.unwrap().full_name, deserialized.description); } - StatusCode::CONFLICT => println!("{}", "Repository already exists!".red()), + StatusCode::BAD_REQUEST => { + // TODO: implement error handling for the deserialization + let deserialized: RepositoryErrorResponse = repo.json().unwrap(); + println!( + "{}", + util::bad_response_message(&deserialized.message, ErrorKind::BadRequest) + .red() + ); + } + StatusCode::CONFLICT => println!( + "{}", + "Repository has the same name or already exists!".red() + ), StatusCode::UNPROCESSABLE_ENTITY => { println!("{}", "Repository input validation failed!".red()) } @@ -166,8 +189,13 @@ impl Repository { match response { Ok(repo) => match repo.status() { - StatusCode::NO_CONTENT => println!("{}", "Respository successfully deleted!".green()), - StatusCode::FORBIDDEN => println!("{}", "Repository deletion forbidden!".red()), + StatusCode::NO_CONTENT => { + println!("{}", "Respository successfully deleted!".green()) + } + StatusCode::FORBIDDEN => println!( + "{}", + "Unable to authorize deletion. Please report this to the webmaster!".red() + ), _ => println!( "Repository deletion failed! Does the repository exist? HTTP status code: {}", repo.status().as_str() @@ -191,6 +219,7 @@ impl Repository { let mut map: HashMap<&str, &str> = HashMap::new(); let user = request.authentication.credentials.0.as_ref().unwrap(); + // TODO: use new url formatter let url = format!( "{request}/repos/{owner}/{repo}/forks?token={api_token}", request = request.url.as_ref().unwrap(), @@ -210,11 +239,21 @@ impl Repository { println!("{}", "Repository forked successfully".green()); println!("\tOriginal repository name: {:0?}\n\tOriginal repository owner: {:1?}\n\tForked repository name: {:2?}\n\tForked repository owner: {:3?}", deserialized.name, arg_item[0], deserialized.name, deserialized.owner.unwrap().full_name); } - StatusCode::INTERNAL_SERVER_ERROR => println!("{}", "Repository already forked!".red()), - StatusCode::FORBIDDEN => println!("{}", "Repository fork forbidden!".red()), - StatusCode::UNPROCESSABLE_ENTITY => println!("{}", "Repository fork input validation failed!".red()), + StatusCode::INTERNAL_SERVER_ERROR => { + println!("{}", "Repository already forked!".red()) + } + StatusCode::FORBIDDEN => println!( + "{}", + "Unable to authorize fork. Please report this to the webmaster!".red() + ), + StatusCode::UNPROCESSABLE_ENTITY => { + println!("{}", "Repository fork input validation failed!".red()) + } StatusCode::NOT_FOUND => println!("{}", "Repository not found!"), - _ => println!("Repository creation failed! HTTP status code: {}", repo.status().as_str()), + _ => println!( + "Repository creation failed! HTTP status code: {}", + repo.status().as_str() + ), }, Err(e) => panic!("{}", e), } @@ -229,6 +268,7 @@ impl Repository { .unwrap() .value_of("search") .unwrap(); + // TODO: use new url formatter let url = format!( "{request}/repos/search?q={query}&token={api_token}", request = request.url.as_ref().unwrap(), @@ -251,14 +291,22 @@ impl Repository { println!("{}.\tRepository name: {:1}\n\tRepository owner: {:2}\n\tRepository description: {:3}\n", i + 1, data.name, data.owner.as_ref().unwrap().full_name, data.description) } - println!("Total number of repositories indexed: {}", deserialized.data.iter().count()) + println!( + "Total number of repositories indexed: {}", + deserialized.data.iter().count() + ) } false => println!("{}", "Repository searched doesn't exist!".red()), } } StatusCode::NOT_FOUND => println!("{}", "Repository searched doesn't exist!".red()), - StatusCode::UNPROCESSABLE_ENTITY => println!("{}", "Repository input validation failed!".red()), - _ => println!("Repository search failed! HTTP status code: {}", repo.status().as_str()), + StatusCode::UNPROCESSABLE_ENTITY => { + println!("{}", "Repository input validation failed!".red()) + } + _ => println!( + "Repository search failed! HTTP status code: {}", + repo.status().as_str() + ), }, Err(e) => panic!("{}", e), } @@ -292,8 +340,13 @@ impl Repository { false => println!("{}", "The authenticated user doesn't have any repositories. Why not create one?".yellow()) } } - StatusCode::UNPROCESSABLE_ENTITY => println!("{}", "Repository input validation failed!".red()), - _ => println!("Repository search failed! HTTP status code: {}",repo.status().as_str()), + StatusCode::UNPROCESSABLE_ENTITY => { + println!("{}", "Repository input validation failed!".red()) + } + _ => println!( + "Repository search failed! HTTP status code: {}", + repo.status().as_str() + ), }, Err(e) => panic!("{}", e), } @@ -302,7 +355,6 @@ impl Repository { pub fn push_to_remote(&self, request: &Request, config: &Configuration) { // code to push to the remote server goes here // this is based on the user, the remote, and the branch - } pub fn pull_from_remote(&self, request: &Request, config: &Configuration) { @@ -330,15 +382,15 @@ impl Repository { let mut callbacks = RemoteCallbacks::new(); callbacks.credentials(|_url, _user_from_url, _allowed_types| { let user = _user_from_url.unwrap_or("git"); - - if _allowed_types.contains(CredentialType::USERNAME) { - return Cred::username(user) + + if _allowed_types.contains(CredentialType::USERNAME) { + return Cred::username(user); } // if request.url_request() { // Cred::ssh_key( - // user, - // Some(Path::new(&format!("{}/.ssh/id_ed25519.pub", std::env::var("HOME").unwrap()).as_str())), + // user, + // Some(Path::new(&format!("{}/.ssh/id_ed25519.pub", std::env::var("HOME").unwrap()).as_str())), // Path::new(&format!("{}/.ssh/id_ed25519", std::env::var("HOME").unwrap()).as_str()), // None // ) @@ -349,24 +401,25 @@ impl Repository { // ) // } Cred::userpass_plaintext( - request.authentication.credentials.0.as_ref().unwrap().as_str(), - config.password.as_ref().unwrap().as_str() + request + .authentication + .credentials + .0 + .as_ref() + .unwrap() + .as_str(), + config.password.as_ref().unwrap().as_str(), ) }); builder.branch("master"); match builder.clone(url.as_str(), Path::new(arg_value[1])) { Ok(_r) => Ok(println!("Repository cloned successfully!")), Err(_e) => Err(println!("Repository clone failed!")), - _ => panic!("agh") + _ => panic!("agh"), }; - } - pub fn add_to_staging(&self, request: &Request, config: Configuration) { + pub fn add_to_staging(&self, request: &Request, config: Configuration) {} - } - - pub fn create_commit(&self, request: &Request, config: Configuration) { - - } + pub fn create_commit(&self, request: &Request, config: Configuration) {} } diff --git a/src/request.rs b/src/request.rs index d2e57c1..3e2d4b8 100644 --- a/src/request.rs +++ b/src/request.rs @@ -4,6 +4,14 @@ use clap::ArgMatches; use reqwest::blocking::Client; use crate::config::Configuration; +use crate::util; + +pub enum RequestType { + Repository, + Issue, + PullRequest, + User, +} pub enum AuthenticationType { BasicAuth, @@ -13,10 +21,11 @@ pub enum AuthenticationType { pub struct Request<'a> { pub client: Client, - pub arg_value: ArgMatches<'a>, + pub arg_value: &'a ArgMatches<'a>, pub map: HashMap, pub url: Option, pub authentication: Authentication, + pub request_type: RequestType, } pub struct Authentication { @@ -30,30 +39,32 @@ impl<'a> Request<'a> { /// Public constructor for a request with a simple username and password pub fn with_basic_request( config: Configuration, - arg: ArgMatches, + arg: &'a ArgMatches, auth: Authentication, - ) -> Request { + ) -> Request<'a> { Request { client: Client::new(), - arg_value: arg, + arg_value: &arg, map: HashMap::new(), url: Some(format!("{}{}", config.base_url, config.base_api)), authentication: auth, + request_type: util::get_request_type(&arg).unwrap(), } } /// Public constructor for a request with an API token pub fn with_api_request( config: Configuration, - arg: ArgMatches, + arg: &'a ArgMatches, auth: Authentication, - ) -> Request { + ) -> Request<'a> { Request { client: Client::new(), - arg_value: arg, + arg_value: &arg, map: HashMap::new(), url: Some(format!("{}{}", config.base_url, config.base_api)), authentication: auth, + request_type: util::get_request_type(&arg).unwrap(), } } } @@ -114,12 +125,16 @@ impl Authentication { /// Public method that based on the what kind of authentication is being used, it can /// determine what kind of requesting method is going to be used. See the Request /// structure for more details. - pub fn request_chooser(self, config: Configuration, arg: ArgMatches<'static>) -> Request { + pub fn request_chooser<'a>( + self, + config: Configuration, + arg: &'a ArgMatches<'static>, + ) -> Request<'a> { if let true = self.api_token { - Request::with_api_request(config, arg.to_owned(), self) + Request::with_api_request(config, &arg, self) } else { match self.basic { - true => Request::with_basic_request(config, arg.to_owned(), self), + true => Request::with_basic_request(config, &arg, self), false => panic!(), } } diff --git a/src/user.rs b/src/user.rs index 76f64c4..dd6cce3 100644 --- a/src/user.rs +++ b/src/user.rs @@ -1,11 +1,17 @@ use std::collections::HashMap; -use crate::{request::Request, util}; +use crate::{ + request::Request, + util::{self, ErrorKind}, +}; +use colored::*; +use reqwest::StatusCode; use serde_derive::{Deserialize, Serialize}; pub struct User; -pub struct MutlipleUsers { +#[derive(Deserialize)] +pub struct MultipleUsers { pub data: Vec, pub ok: bool, } @@ -29,33 +35,193 @@ pub struct UserResponse { pub restricted: bool, } +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct UserErrorResponse { + pub message: String, + pub url: String, +} + impl User { pub fn new() -> User { User {} } - pub fn create_user(request: &Request) { + pub fn create_user(&self, request: &Request) { let client = &request.client; - let arg_value = &request + let mut user_create_input: HashMap = HashMap::new(); + let url = format!( + "{request}/admin/users?token={api_token}", + request = request.url.as_ref().unwrap(), + api_token = request.authentication.credentials.1.as_ref().unwrap() + ); + + let username = util::get_input(String::from("Please enter a username:")); + let email = util::get_input(String::from("Please enter a email address:")); + let password = util::get_input(String::from("Please enter a password:")); + + user_create_input.insert("login".to_string(), username); + user_create_input.insert("email".to_string(), email); + user_create_input.insert("password".to_string(), password); + + let response = client.post(url.as_str()).json(&user_create_input).send(); + match response { + Ok(repo) => match repo.status() { + StatusCode::CREATED => { + let deserialized: UserResponse = repo.json().unwrap(); + println!("{}", "User successfully created!".green()); + println!( + "\tFull Name: {:0}\n\tUsername: {:1}\n\tEmail: {:2}\n", + deserialized.full_name, deserialized.login, deserialized.email + ); + } + StatusCode::BAD_REQUEST => { + let deserialized: UserErrorResponse = repo.json().unwrap(); + println!( + "{}", + util::bad_response_message(&deserialized.message, ErrorKind::BadRequest) + .red() + ); + } + StatusCode::FORBIDDEN => { + let deserialized: UserErrorResponse = repo.json().unwrap(); + println!( + "{}", + util::bad_response_message( + &deserialized.message, + ErrorKind::ForbiddenRequest + ) + .red() + ); + } + _ => println!("¯\\_(ツ)_/¯"), + }, + Err(e) => println!("{}", e), + } + } + + pub fn list_user(&self, request: &Request) { + let client = &request.client; + + let url = format!( + "{request}/users?token={api_token}", + request = request.url.as_ref().unwrap(), + api_token = request.authentication.credentials.1.as_ref().unwrap(), + ); + let response = client.get(url).send(); + match response { + Ok(repo) => { + match repo.status() { + StatusCode::OK => { + let deserialized: MultipleUsers = repo.json().unwrap(); + + match deserialized.data.len() != 0 { + true => { + println!("{}", "List of users found:"); + + for (i, data) in deserialized.data.iter().enumerate() { + println!("{}.\nUsername: {:1}\n\tEmail address: {:2}\n\tAdmin?: {:3}", i + 1, data.login, data.email, data.is_admin); + } + + println!( + "Total number of users indexed: {}", + deserialized.data.len() + ); + } + false => println!("{}", "No users found :("), + } + } + StatusCode::BAD_REQUEST => { + let deserialized: UserErrorResponse = repo.json().unwrap(); // TODO: handle + println!( + "{}", + util::bad_response_message( + &deserialized.message, + ErrorKind::BadRequest + ) + .red() + ); + } + StatusCode::FORBIDDEN => { + let deserialzed: UserErrorResponse = repo.json().unwrap(); // TODO: handle errs + println!( + "{}", + util::bad_response_message( + &deserialzed.message, + ErrorKind::ForbiddenRequest + ) + .red() + ); + } + _ => println!("huh?"), + } + } + Err(e) => panic!("{}", e), + } + } + + pub fn search_user(&self, request: &Request) { + let client = &request.client; + let arg_value = request .arg_value .subcommand() .1 .unwrap() - .value_of("create") + .value_of("search") .unwrap(); - let mut user_create_input: HashMap = HashMap::new(); - let username = util::get_input(String::from("Please enter a username:")); - let email = util::get_input(String::from("Please enter a email address:")); - let password = util::get_input(String::from("Please enter a password:")); - user_create_input.insert("login".to_string(), username); - user_create_input.insert("email".to_string(), email); - user_create_input.insert("password".to_string(), password); + let url = format!( + "{request}/users?q={query}&token={api_token}", + request = request.url.as_ref().unwrap(), + query = arg_value, + api_token = request.authentication.credentials.1.as_ref().unwrap(), + ); + + let response = client.get(url.as_str()).send(); + + match response { + Ok(repo) => match repo.status() { + _ => println!(""), + }, + Err(e) => panic!("{}", e), + } } - pub fn list_user() {} + pub fn delete_user(&self, request: &Request) { + let client = &request.client; + let arg_value = request + .arg_value + .subcommand() + .1 + .unwrap() + .value_of("search") + .unwrap(); - pub fn search_user() {} + let url = format!( + "{request}/admin/users/{username}", + request = request.url.as_ref().unwrap(), + username = arg_value, + ); + let response = client.delete(url.as_str()).send(); - pub fn delete_user() {} + match response { + Ok(repo) => match repo.status() { + StatusCode::NO_CONTENT => { + println!("User successfully deleted!") + } + StatusCode::FORBIDDEN => { + let deserialized: UserErrorResponse = repo.json().unwrap(); + println!( + "{}", + util::bad_response_message( + &deserialized.message, + ErrorKind::ForbiddenRequest + ) + ); + } + _ => println!(""), + }, + Err(e) => panic!("{}", e), + } + } } diff --git a/src/util.rs b/src/util.rs index b820128..f2d7dce 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,5 +1,16 @@ use std::io; +use clap::{ArgMatches, Error}; + +use crate::request::RequestType; + +pub enum ErrorKind { + BadRequest, + ForbiddenRequest, + NotFound, + UnprocessiableRequest, +} + // TODO: Can't get this function to properly work // Currently, I'm grabbing input and have the server tell me whether or // not input was validated. What I want to have happen is if there's @@ -21,6 +32,51 @@ pub fn get_input(question: String) -> String { break; } } - } + } result -} \ No newline at end of file +} + +/// When you get a bad response from your Gitea server (any HTTP response that has +/// a status code not 2xx), and given the kind of response a user receives, the +/// appropriate message should be returned +pub fn bad_response_message(message: &String, error_kind: ErrorKind) -> String { + let final_message: String; + match error_kind { + ErrorKind::BadRequest => { + final_message = format!( + "Client error - please try again!\nError message: {}", + message + ); + } + ErrorKind::ForbiddenRequest => { + final_message = format!( + "Client error - unauthorized. Please try again!\nError message: {}", + message + ); + } + ErrorKind::NotFound => { + final_message = format!("Client error - not found!\nError message: {}", message); + } + ErrorKind::UnprocessiableRequest => { + final_message = format!("Client error - the request can't be processed. Please try again!\nError message: {}", message); + } + } + + String::from(final_message) +} + +/// Based on a subcommand that is passed, return the appropriate request +/// type +pub fn get_request_type(args: &ArgMatches) -> Result { + let request_type: &str = args.subcommand().0; + match request_type { + "repo" => Ok(RequestType::Repository), + "issue" => Ok(RequestType::Issue), + "pr" => Ok(RequestType::PullRequest), + "user" => Ok(RequestType::User), + _ => Err(clap::Error::with_description( + "Unknown or invalid command", + clap::ErrorKind::InvalidSubcommand, + )), + } +}