Initial connection test

This commit is contained in:
Bodo Junglas 2020-02-01 14:36:42 +01:00
commit a5e0570ef5
No known key found for this signature in database
GPG Key ID: A7C4E6F450E47C3A
15 changed files with 1788 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/target
**/*.rs.bk

1408
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

5
Cargo.toml Normal file
View File

@ -0,0 +1,5 @@
[workspace]
members = [
"cli",
"lib",
]

16
cli/Cargo.toml Normal file
View 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
View 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
View 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
View File

@ -0,0 +1,5 @@
mod check;
mod init;
pub use check::*;
pub use init::*;

52
cli/src/config.rs Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1,4 @@
tab_spaces = 2
max_width = 160
use_try_shorthand = true
reorder_imports = true