mirror of
https://github.com/gosticks/acari.git
synced 2025-10-16 11:45:37 +00:00
Initial connection test
This commit is contained in:
commit
a5e0570ef5
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
/target
|
||||
**/*.rs.bk
|
||||
1408
Cargo.lock
generated
Normal file
1408
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
5
Cargo.toml
Normal file
5
Cargo.toml
Normal file
@ -0,0 +1,5 @@
|
||||
[workspace]
|
||||
members = [
|
||||
"cli",
|
||||
"lib",
|
||||
]
|
||||
16
cli/Cargo.toml
Normal file
16
cli/Cargo.toml
Normal file
@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "acari-cli"
|
||||
version = "0.1.0"
|
||||
authors = ["Bodo Junglas <junglas@objectcode.de>"]
|
||||
edition = "2018"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
clap = "2"
|
||||
dirs = "2"
|
||||
toml = "0"
|
||||
text_io = "0"
|
||||
prettytable-rs = "0"
|
||||
acari-lib = { path = "../lib" }
|
||||
38
cli/src/commands/check.rs
Normal file
38
cli/src/commands/check.rs
Normal file
@ -0,0 +1,38 @@
|
||||
use crate::config::Config;
|
||||
use crate::error::AppError;
|
||||
use prettytable::{cell, format, row, table};
|
||||
|
||||
pub fn check(config: &Config) -> Result<(), AppError> {
|
||||
let client = config.client();
|
||||
let account = client.get_account()?;
|
||||
let user = client.get_myself()?;
|
||||
|
||||
let mut account_table = table!(
|
||||
["Id", account.id.to_string()],
|
||||
["Name", account.name],
|
||||
["Title", account.title],
|
||||
["Currency", account.currency],
|
||||
["Created at", account.created_at.to_string()],
|
||||
["Updated at", account.updated_at.to_string()]
|
||||
);
|
||||
|
||||
println!("Account");
|
||||
account_table.set_format(*format::consts::FORMAT_CLEAN);
|
||||
account_table.printstd();
|
||||
|
||||
let mut user_table = table!(
|
||||
["Id", user.id.to_string()],
|
||||
["Name", user.name],
|
||||
["Email", user.email],
|
||||
["Role", user.role],
|
||||
["Language", user.language],
|
||||
["Created at", user.created_at.to_string()],
|
||||
["Updated at", user.updated_at.to_string()]
|
||||
);
|
||||
|
||||
println!();
|
||||
println!("User");
|
||||
user_table.set_format(*format::consts::FORMAT_CLEAN);
|
||||
user_table.printstd();
|
||||
Ok(())
|
||||
}
|
||||
19
cli/src/commands/init.rs
Normal file
19
cli/src/commands/init.rs
Normal file
@ -0,0 +1,19 @@
|
||||
use crate::config::Config;
|
||||
use crate::error::AppError;
|
||||
use std::io::{stdout, Write};
|
||||
use text_io::try_read;
|
||||
|
||||
pub fn init() -> Result<(), AppError> {
|
||||
print!("Mite domain: ");
|
||||
stdout().flush()?;
|
||||
let domain: String = try_read!("{}\n")?;
|
||||
print!("API Token: ");
|
||||
stdout().flush()?;
|
||||
let token: String = try_read!("{}\n")?;
|
||||
|
||||
Config { domain, token }.write()?;
|
||||
|
||||
println!("Configuration updated");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
5
cli/src/commands/mod.rs
Normal file
5
cli/src/commands/mod.rs
Normal file
@ -0,0 +1,5 @@
|
||||
mod check;
|
||||
mod init;
|
||||
|
||||
pub use check::*;
|
||||
pub use init::*;
|
||||
52
cli/src/config.rs
Normal file
52
cli/src/config.rs
Normal file
@ -0,0 +1,52 @@
|
||||
use crate::error::AppError;
|
||||
use acari_lib::Client;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs::{self, File};
|
||||
use std::io::{self, Read, Write};
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Config {
|
||||
pub domain: String,
|
||||
pub token: String,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn read() -> Result<Option<Config>, AppError> {
|
||||
let config_file = config_file();
|
||||
match File::open(&config_file) {
|
||||
Ok(mut file) => {
|
||||
let mut content = vec![];
|
||||
file.read_to_end(&mut content)?;
|
||||
Ok(Some(toml::from_slice::<Config>(&content)?))
|
||||
}
|
||||
Err(ref err) if err.kind() == io::ErrorKind::NotFound => Ok(None),
|
||||
Err(err) => Err(err.into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn client(&self) -> Client {
|
||||
Client::new(&self.domain, &self.token)
|
||||
}
|
||||
|
||||
pub fn write(&self) -> Result<(), AppError> {
|
||||
let content = toml::to_string_pretty(self)?;
|
||||
let config_file = config_file();
|
||||
|
||||
fs::create_dir_all(&config_file.parent().ok_or_else(|| AppError::InternalError("Invalid config path".to_string()))?)?;
|
||||
|
||||
let mut file = File::create(&config_file)?;
|
||||
|
||||
file.write_all(content.as_bytes())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn config_file() -> PathBuf {
|
||||
let home_dir = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
|
||||
dirs::config_dir()
|
||||
.map(|configs| configs.join("acari"))
|
||||
.unwrap_or_else(|| home_dir.join(".acari"))
|
||||
.join("config.toml")
|
||||
}
|
||||
57
cli/src/error.rs
Normal file
57
cli/src/error.rs
Normal file
@ -0,0 +1,57 @@
|
||||
use std::error::Error;
|
||||
use std::fmt;
|
||||
use std::io;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum AppError {
|
||||
IO(io::Error),
|
||||
TextIO(text_io::Error),
|
||||
TomlRead(toml::de::Error),
|
||||
TomlWrite(toml::ser::Error),
|
||||
AcariError(acari_lib::AcariError),
|
||||
UserError(String),
|
||||
InternalError(String),
|
||||
}
|
||||
|
||||
impl fmt::Display for AppError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
AppError::IO(err) => write!(f, "IO error: {}", err),
|
||||
AppError::TextIO(err) => write!(f, "IO error: {}", err),
|
||||
AppError::TomlRead(err) => write!(f, "Toml error: {}", err),
|
||||
AppError::TomlWrite(err) => write!(f, "Toml error: {}", err),
|
||||
AppError::AcariError(err) => write!(f, "Error: {}", err),
|
||||
AppError::UserError(s) => write!(f, "User error: {}", s),
|
||||
AppError::InternalError(s) => write!(f, "Internal error: {}", s),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for AppError {
|
||||
fn source(&self) -> Option<&(dyn Error + 'static)> {
|
||||
match self {
|
||||
AppError::IO(err) => Some(err),
|
||||
AppError::TextIO(err) => Some(err),
|
||||
AppError::TomlRead(err) => Some(err),
|
||||
AppError::TomlWrite(err) => Some(err),
|
||||
AppError::AcariError(err) => Some(err),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! app_error_from {
|
||||
($error: ty, $app_error: ident) => {
|
||||
impl From<$error> for AppError {
|
||||
fn from(err: $error) -> AppError {
|
||||
AppError::$app_error(err)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
app_error_from!(io::Error, IO);
|
||||
app_error_from!(text_io::Error, TextIO);
|
||||
app_error_from!(toml::de::Error, TomlRead);
|
||||
app_error_from!(toml::ser::Error, TomlWrite);
|
||||
app_error_from!(acari_lib::AcariError, AcariError);
|
||||
29
cli/src/main.rs
Normal file
29
cli/src/main.rs
Normal file
@ -0,0 +1,29 @@
|
||||
use clap::{App, Arg, SubCommand};
|
||||
|
||||
mod commands;
|
||||
mod config;
|
||||
mod error;
|
||||
|
||||
use config::Config;
|
||||
use error::AppError;
|
||||
|
||||
fn main() -> Result<(), AppError> {
|
||||
let app = App::new("acarid")
|
||||
.version("0.1")
|
||||
.about("Commandline interface for mite")
|
||||
.subcommand(SubCommand::with_name("init").about("Initialize connection to mite"))
|
||||
.subcommand(SubCommand::with_name("check").about("Check connection to mite"));
|
||||
let matches = app.get_matches();
|
||||
|
||||
match Config::read()? {
|
||||
Some(config) => match matches.subcommand() {
|
||||
("init", _) => commands::init(),
|
||||
("check", _) => commands::check(&config),
|
||||
(invalid, _) => Err(AppError::UserError(format!("Unknown command: {}", invalid))),
|
||||
},
|
||||
None => match matches.subcommand() {
|
||||
("init", _) => commands::init(),
|
||||
(_, _) => Err(AppError::UserError("Missing configuration, run init first".to_string())),
|
||||
},
|
||||
}
|
||||
}
|
||||
12
lib/Cargo.toml
Normal file
12
lib/Cargo.toml
Normal file
@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "acari-lib"
|
||||
version = "0.1.0"
|
||||
authors = ["Bodo Junglas <junglas@objectcode.de>"]
|
||||
edition = "2018"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
reqwest = { version = "0.10", features = ["blocking", "json"] }
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
43
lib/src/error.rs
Normal file
43
lib/src/error.rs
Normal file
@ -0,0 +1,43 @@
|
||||
use std::error::Error;
|
||||
use std::fmt;
|
||||
use std::io;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum AcariError {
|
||||
IO(io::Error),
|
||||
Request(reqwest::Error),
|
||||
Mite(u16, String),
|
||||
}
|
||||
|
||||
impl fmt::Display for AcariError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
AcariError::IO(err) => write!(f, "IO error: {}", err),
|
||||
AcariError::Request(err) => write!(f, "Request error: {}", err),
|
||||
AcariError::Mite(status, error) => write!(f, "Mite error ({}): {}", status, error),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for AcariError {
|
||||
fn source(&self) -> Option<&(dyn Error + 'static)> {
|
||||
match self {
|
||||
AcariError::IO(err) => Some(err),
|
||||
AcariError::Request(err) => Some(err),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! acarid_error_from {
|
||||
($error: ty, $app_error: ident) => {
|
||||
impl From<$error> for AcariError {
|
||||
fn from(err: $error) -> AcariError {
|
||||
AcariError::$app_error(err)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
acarid_error_from!(io::Error, IO);
|
||||
acarid_error_from!(reqwest::Error, Request);
|
||||
65
lib/src/lib.rs
Normal file
65
lib/src/lib.rs
Normal file
@ -0,0 +1,65 @@
|
||||
mod error;
|
||||
mod model;
|
||||
|
||||
pub use error::AcariError;
|
||||
pub use model::{Account, User};
|
||||
pub use serde::de::DeserializeOwned;
|
||||
|
||||
use model::MiteResponse;
|
||||
|
||||
use reqwest::{blocking, header, StatusCode};
|
||||
|
||||
const USER_AGENT: &str = "acari-lib (https://github.com/untoldwind/acari)";
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Client {
|
||||
domain: String,
|
||||
token: String,
|
||||
client: blocking::Client,
|
||||
}
|
||||
|
||||
impl Client {
|
||||
pub fn new(domain: &str, token: &str) -> Client {
|
||||
Client {
|
||||
domain: domain.to_string(),
|
||||
token: token.to_string(),
|
||||
client: blocking::Client::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn get(&self, uri: &str) -> Result<MiteResponse, AcariError> {
|
||||
let response = self
|
||||
.client
|
||||
.get(&format!("https://{}{}", self.domain, uri))
|
||||
.header(header::USER_AGENT, USER_AGENT)
|
||||
.header(header::HOST, &self.domain)
|
||||
.header("X-MiteApiKey", &self.token)
|
||||
.send()?;
|
||||
|
||||
handle_response(response)
|
||||
}
|
||||
|
||||
pub fn get_account(&self) -> Result<Account, AcariError> {
|
||||
match self.get("/account.json")? {
|
||||
MiteResponse::Account(account) => Ok(account),
|
||||
response => Err(AcariError::Mite(400, format!("Unexpected response: {:?}", response))),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_myself(&self) -> Result<User, AcariError> {
|
||||
match self.get("/myself.json")? {
|
||||
MiteResponse::User(user) => Ok(user),
|
||||
response => Err(AcariError::Mite(400, format!("Unexpected response: {:?}", response))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_response(response: blocking::Response) -> Result<MiteResponse, AcariError> {
|
||||
match response.status() {
|
||||
StatusCode::OK => Ok(response.json()?),
|
||||
status => match response.json::<MiteResponse>() {
|
||||
Ok(MiteResponse::Error(msg)) => Err(AcariError::Mite(status.as_u16(), msg)),
|
||||
_ => Err(AcariError::Mite(status.as_u16(), status.to_string())),
|
||||
},
|
||||
}
|
||||
}
|
||||
33
lib/src/model.rs
Normal file
33
lib/src/model.rs
Normal file
@ -0,0 +1,33 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct Account {
|
||||
pub id: u32,
|
||||
pub name: String,
|
||||
pub title: String,
|
||||
pub currency: String,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct User {
|
||||
pub id: u32,
|
||||
pub name: String,
|
||||
pub email: String,
|
||||
pub note: String,
|
||||
pub archived: bool,
|
||||
pub role: String,
|
||||
pub language: String,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum MiteResponse {
|
||||
Account(Account),
|
||||
User(User),
|
||||
Error(String),
|
||||
}
|
||||
4
rustfmt.toml
Normal file
4
rustfmt.toml
Normal file
@ -0,0 +1,4 @@
|
||||
tab_spaces = 2
|
||||
max_width = 160
|
||||
use_try_shorthand = true
|
||||
reorder_imports = true
|
||||
Loading…
Reference in New Issue
Block a user