#![warn(missing_debug_implementations, rust_2018_idioms, missing_docs)]
use anyhow::{anyhow, Result};
use serde_json::{json, Value};
use std::collections::HashMap;
use std::string::ToString;
use strum_macros;
use url::Url;
pub const DEFAULT_API_URL: &str = "na.myconnectwise.net";
pub const DEFAULT_API_CODEBASE: &str = "v4_6_release";
pub const DEFAULT_API_VERSION: &str = "3.0";
#[derive(Debug, strum_macros::ToString)]
pub enum PatchOp {
#[strum(serialize = "add")]
Add,
#[strum(serialize = "replace")]
Replace,
#[strum(serialize = "remove")]
Remove,
}
#[derive(Debug, PartialEq)]
pub struct Client {
company_id: String,
public_key: String,
private_key: String,
client_id: String,
api_url: String,
codebase: String,
api_version: String,
}
impl Client {
pub fn new(
company_id: String,
public_key: String,
private_key: String,
client_id: String,
) -> Client {
Client {
company_id,
public_key,
private_key,
client_id,
api_url: DEFAULT_API_URL.to_string(),
codebase: DEFAULT_API_CODEBASE.to_string(),
api_version: DEFAULT_API_VERSION.to_string(),
}
}
pub fn build(&self) -> Client {
Client {
company_id: self.company_id.to_owned(),
public_key: self.public_key.to_owned(),
private_key: self.private_key.to_owned(),
client_id: self.client_id.to_owned(),
api_url: self.api_url.to_owned(),
codebase: self.codebase.to_owned(),
api_version: self.api_version.to_owned(),
}
}
pub fn api_version(mut self, api_version: String) -> Client {
self.api_version = api_version;
self
}
pub fn api_url(mut self, api_url: String) -> Client {
self.api_url = api_url;
self
}
pub fn codebase(mut self, codebase: String) -> Client {
self.codebase = codebase;
self
}
fn gen_basic_auth(&self) -> String {
let encoded = base64::encode(format!(
"{}+{}:{}",
self.company_id, self.public_key, self.private_key
));
format!("Basic {}", encoded)
}
fn gen_api_url(&self, path: &str) -> String {
format!(
"https://{}/{}/apis/{}{}",
self.api_url, self.codebase, self.api_version, path
)
}
pub fn get_single(&self, path: &str, query: &[(&str, &str)]) -> Result<Value> {
let res = reqwest::blocking::Client::new()
.get(&self.gen_api_url(path))
.header("Authorization", &self.gen_basic_auth())
.header("Content-Type", "application/json")
.header("clientid", self.client_id.to_owned())
.header("pagination-type", "forward-only")
.query(&query)
.send()
.unwrap()
.text()
.unwrap();
let v: Value = serde_json::from_str(&res).unwrap();
Ok(v)
}
pub fn get(&self, path: &str, query: &[(&str, &str)]) -> Result<Vec<Value>> {
let mut collected_res: Vec<Value> = Vec::new();
let mut page: String = "1".to_string();
let mut next: bool = true;
while next {
let res = reqwest::blocking::Client::new()
.get(&self.gen_api_url(path))
.header("Authorization", self.gen_basic_auth())
.header("Content-Type", "application/json")
.header("clientid", self.client_id.to_owned())
.header("pagination-type", "forward-only")
.query(&[("pageid", &page)])
.query(&query)
.send()
.unwrap();
let hdrs = res.headers();
next = match hdrs.get("link") {
Some(link) => {
if link.is_empty() {
false
} else {
page = get_page_id(hdrs);
true
}
}
None => false,
};
let body = res.text().unwrap();
let mut v: Vec<Value> = serde_json::from_str(&body).unwrap();
collected_res.append(&mut v);
}
Ok(collected_res)
}
pub fn post(&self, path: &str, body: String) -> Result<Value> {
let res = reqwest::blocking::Client::new()
.post(&self.gen_api_url(path))
.header("Authorization", &self.gen_basic_auth())
.header("Content-Type", "application/json")
.header("clientid", self.client_id.to_owned())
.header("pagination-type", "forward-only")
.body(body)
.send()
.unwrap()
.text()
.unwrap();
let v: Value = serde_json::from_str(&res)?;
match &v["errors"].as_array() {
Some(_e) => Err(anyhow!("we got some errors: {:?}", &v["errors"].as_array())),
None => {
match &v["message"].as_str() {
Some(_e) => Err(anyhow!("we got some errors: {:?}", &v["message"].as_str())),
None => Ok(v),
}
}
}
}
pub fn patch(&self, path: &str, op: PatchOp, patch_path: &str, value: serde_json::Value) -> Result<Value> {
let body = json!([{
"op": op.to_string(),
"path": patch_path,
"value": value,
}])
.to_string();
let res = reqwest::blocking::Client::new()
.patch(&self.gen_api_url(path))
.header("Authorization", &self.gen_basic_auth())
.header("Content-Type", "application/json")
.header("clientid", self.client_id.to_owned())
.header("pagination-type", "forward-only")
.body(body)
.send()
.unwrap()
.text()
.unwrap();
let v: Value = serde_json::from_str(&res)?;
match &v["message"].as_str() {
Some(_e) => Err(anyhow!("we got some errors: {:?}", &v)),
None => Ok(v),
}
}
}
fn get_page_id(hdrs: &reqwest::header::HeaderMap) -> String {
let url = hdrs
.get("link")
.unwrap()
.to_str()
.unwrap()
.split("link =")
.collect::<Vec<&str>>()[0]
.split('<')
.collect::<Vec<&str>>()[1]
.split('>')
.collect::<Vec<&str>>()[0];
let parsed_url = Url::parse(url).unwrap();
let hash_query: HashMap<_, _> = parsed_url.query_pairs().into_owned().collect();
match hash_query.contains_key("pageId") {
false => "".to_string(),
true => hash_query["pageId"].to_string(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use dotenv::dotenv;
use serde_json::json;
fn testing_client() -> Client {
dotenv().ok();
let company_id: String =
dotenv::var("CWMANAGE_COMPANY_ID").expect("CWMANAGE_COMPANY_ID needs to be set");
let public_key: String =
dotenv::var("CWMANAGE_PUBLIC_KEY").expect("CWMANAGE_PUBLIC_KEY needs to be set");
let private_key: String =
dotenv::var("CWMANAGE_PRIVATE_KEY").expect("CWMANAGE_PRIVATE_KEY needs to be set");
let client_id: String =
dotenv::var("CWMANAGE_CLIENT_ID").expect("CWMANAGE_CLIENT_ID needs to be set");
Client::new(company_id, public_key, private_key, client_id).build()
}
#[test]
fn test_basic_auth() {
let expected: String = "Basic bXljbytwdWI6cHJpdg==".to_string();
let client = Client::new(
String::from("myco"),
String::from("pub"),
String::from("priv"),
String::from("something"),
)
.build();
let result = client.gen_basic_auth();
assert_eq!(result, expected);
}
#[test]
fn test_gen_url() {
let expected = "https://na.myconnectwise.net/v4_6_release/apis/3.0/system/info";
let client = Client::new(
String::from("myco"),
String::from("pub"),
String::from("priv"),
String::from("something"),
)
.build();
let result = client.gen_api_url("/system/info");
assert_eq!(result, expected);
}
#[test]
#[should_panic]
fn test_basic_get_panic() {
let query = [];
let _result = testing_client()
.get_single("/this/is/a/bad/path", &query)
.unwrap();
}
#[test]
fn test_basic_get_single() {
let query = [];
let result = testing_client().get_single("/system/info", &query).unwrap();
assert_eq!(&result["cloudRegion"], "NA");
assert_eq!(&result["isCloud"], true);
assert_eq!(&result["serverTimeZone"], "Eastern Standard Time");
}
#[test]
fn test_basic_get() {
let query = [];
let result = testing_client().get("/system/members", &query).unwrap();
assert!(result.len() > 40);
let zach = &result[0];
assert_eq!(&zach["adminFlag"], true);
assert_eq!(&zach["dailyCapacity"], 8.0);
assert_eq!(&zach["identifier"], "ZPeters");
}
#[test]
fn test_basic_post() {
let body = json!({
"name": "test from rust cwmanage",
"assignTo": {
"id": 149,
}
})
.to_string();
let result = testing_client().post("/sales/activities", body);
assert!(!result.is_err());
}
#[test]
fn test_project_post_error() {
let body = json!({}).to_string();
let result = testing_client().post("/project/projects/1/notes", body);
assert!(result.is_err());
}
#[test]
fn test_basic_post_error() {
let body = json!({"name": "test from rust cwmanage"}).to_string();
let result = testing_client().post("/sales/activities", body);
dbg!(&result);
assert!(result.is_err());
}
#[test]
fn test_new_client_default() {
let input_company_id = "myco".to_string();
let input_public_key = "public".to_string();
let input_private_key = "private".to_string();
let input_client_id = "clientid".to_string();
let expected = Client {
company_id: "myco".to_string(),
public_key: "public".to_string(),
private_key: "private".to_string(),
client_id: "clientid".to_string(),
api_version: "3.0".to_string(),
api_url: "na.myconnectwise.net".to_string(),
codebase: "v4_6_release".to_string(),
};
let result = Client::new(
input_company_id,
input_public_key,
input_private_key,
input_client_id,
)
.build();
assert_eq!(result, expected);
}
#[test]
fn test_new_client_api_version() {
let input_company_id = "myco".to_string();
let input_public_key = "public".to_string();
let input_private_key = "private".to_string();
let input_client_id = "clientid".to_string();
let input_api_version = "version".to_string();
let expected_api_version = "version";
let result = Client::new(
input_company_id,
input_public_key,
input_private_key,
input_client_id,
)
.api_version(input_api_version)
.build();
assert_eq!(result.api_version, expected_api_version);
}
#[test]
fn test_new_client_codebase() {
let input_company_id = "myco".to_string();
let input_public_key = "public".to_string();
let input_private_key = "private".to_string();
let input_client_id = "clientid".to_string();
let input_codebase = "codebase".to_string();
let expected_codebase = "codebase";
let result = Client::new(
input_company_id,
input_public_key,
input_private_key,
input_client_id,
)
.codebase(input_codebase)
.build();
assert_eq!(result.codebase, expected_codebase);
}
#[test]
fn test_new_client_chained_options() {
let result = Client::new(
"myco".to_string(),
"public".to_string(),
"private".to_string(),
"clientid".to_string(),
)
.codebase("codebase".to_string())
.api_url("api".to_string())
.build();
assert_eq!(result.api_url, "api".to_string());
assert_eq!(result.codebase, "codebase".to_string());
}
#[test]
fn test_basic_patch_add_should_fail() {
let op = PatchOp::Add;
let path = "name";
let value = "test_basic_patch_add";
let result = testing_client().patch("/sales/activities/99", op, path, value);
assert!(result.is_err());
}
#[test]
fn test_basic_patch_replace() {
let op = PatchOp::Replace;
let path = "name";
let value = "test_basic_patch_replace";
let result = testing_client().patch("/sales/activities/100", op, path, value);
assert!(!result.is_err());
}
#[test]
fn test_basic_patch_error() {
let op = PatchOp::Add;
let path = "summary";
let value = "test_basic_patch_error_test";
let result = testing_client().patch("/sales/activities/123", op, path, value);
assert!(result.is_err());
}
}