commit
f1dd5157d2
@ -0,0 +1 @@ |
||||
/target |
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,16 @@ |
||||
[package] |
||||
name = "IssueTrak" |
||||
version = "0.1.0" |
||||
edition = "2021" |
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html |
||||
|
||||
[dependencies] |
||||
reqwest = {features = ["blocking", "json"] } |
||||
uuid = { version = "1.1.2", features = ["fast-rng", "v4"]} |
||||
hmac = "0.12.1" |
||||
sha2 = "0.10.2" |
||||
base64 = "0.13.0" |
||||
chrono = "0.4.19" |
||||
serde_json = "1.0.81" |
||||
serde = {version = "1.0.144", features = ["derive"]} |
||||
@ -0,0 +1,89 @@ |
||||
use sha2::Sha512; |
||||
use hmac::{Hmac, Mac}; |
||||
use reqwest::header::HeaderMap; |
||||
type HmacSha512 = Hmac<Sha512>; |
||||
use uuid::Uuid; |
||||
use chrono::{Utc,SecondsFormat}; |
||||
use std::collections::HashMap; |
||||
use serde_json::json; |
||||
use crate::dto::{IssueSearchQueryDTO,IssueSearchResponse, Collection}; |
||||
use std::error::Error as StdError; |
||||
|
||||
pub struct IssueTrakClient<'a> { |
||||
api_key : &'a str, |
||||
req_client : reqwest::blocking::Client, |
||||
base_url : &'a str |
||||
} |
||||
impl<'a> IssueTrakClient<'a> { |
||||
pub fn new(api_key: &'a str, base_url: &'a str) -> Self { |
||||
IssueTrakClient { |
||||
api_key : api_key, |
||||
req_client : reqwest::blocking::Client::new(), |
||||
base_url : base_url |
||||
} |
||||
} |
||||
fn gen_hmac(&self, header_data: String) -> String { |
||||
let mut mac = HmacSha512::new_from_slice(self.api_key.as_bytes()).unwrap(); |
||||
mac.update(header_data.as_bytes()); |
||||
let res: Vec<u8> = mac.finalize().into_bytes().iter().map(|b| *b).collect(); |
||||
base64::encode(res) |
||||
} |
||||
fn build_headers(&self, req_type: &str, endpoint_path: &str, body_str: &str) -> HeaderMap { |
||||
let timestamp = Utc::now().to_rfc3339_opts(SecondsFormat::Nanos, true); |
||||
let guid = Uuid::new_v4().to_string(); |
||||
let auth_str = format!("{}\n{}\n{}\n{}\n{}\n{}",req_type, &guid, ×tamp, endpoint_path, "",body_str);
|
||||
println!("auth_str = {:?}", auth_str); |
||||
let auth_hash = self.gen_hmac(auth_str); |
||||
|
||||
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert("Accept", "application/json".parse().unwrap()); |
||||
headers.insert("Content-Type", "application/json; charset=utf-8".parse().unwrap()); |
||||
headers.insert("Accept-Language", "en-US".parse().unwrap()); |
||||
headers.insert("X-IssueTrak-API-Request-ID", guid.parse().unwrap()); |
||||
headers.insert("X-IssueTrak-API-Timestamp", timestamp.parse().unwrap()); |
||||
headers.insert("X-IssueTrak-API-Authorization", auth_hash.parse().unwrap()); |
||||
println!("headers = {:?}\n\n", headers); |
||||
headers |
||||
} |
||||
|
||||
pub fn issue_by_number(&self, issue_number: i32) -> Result<Collection, Box< dyn StdError>>{ |
||||
let endpoint_suffix = format!("api/v1/issues/true/{issue_number}"); |
||||
let full_endpoint = format!("{}api/v1/issues/true/{}", self.base_url, issue_number); |
||||
println!("full_endpoint = {:?}\n", full_endpoint); |
||||
let header_data = self.build_headers("GET", &endpoint_suffix,""); |
||||
let res = self.req_client.get(full_endpoint) |
||||
.headers(header_data) |
||||
.send(); |
||||
Ok(serde_json::from_str(&res?.text()?)?) |
||||
} |
||||
|
||||
pub fn update_issue(&self, updated_issue: Collection) -> bool { |
||||
let endpoint_suffix = format!("api/v1/issues/"); |
||||
let full_endpoint = format!("{}api/v1/issues/", self.base_url); |
||||
let header_data = self.build_headers("POST", &endpoint_suffix,&updated_issue.to_string()); |
||||
let res = self.req_client.post(full_endpoint) |
||||
.headers(header_data) |
||||
.send();//.unwrap_or(return false);
|
||||
let good_resp = match res { |
||||
Ok(r) => r, |
||||
Err(e) => return false
|
||||
}; |
||||
match good_resp.text() { |
||||
Ok(t) => return t.contains(&updated_issue.IssueNumber.to_string()), |
||||
Err(e) => return false |
||||
} |
||||
} |
||||
|
||||
pub fn update_issue_by_id(&self, issue_id: i32, issue_field: String, new_issue_value: String) -> bool { |
||||
// get the issue then change the collection, then return that collection with an updated
|
||||
let mut cur_issue = match self.issue_by_number(issue_id) { |
||||
Ok(r) => r, |
||||
Err(e) => return false |
||||
}; |
||||
let c_string = cur_issue.to_string(); |
||||
println!("{:?}", c_string); |
||||
false |
||||
|
||||
} |
||||
} |
||||
@ -0,0 +1,237 @@ |
||||
use serde::{Serialize, Deserialize}; |
||||
use std::error::Error; |
||||
use std::fs::File; |
||||
use std::io::BufReader; |
||||
|
||||
//
|
||||
// Query Set Expression
|
||||
//
|
||||
#[derive(Debug, Serialize, Deserialize)] |
||||
pub enum QueryExpOperator { |
||||
AND, |
||||
OR |
||||
} |
||||
impl std::fmt::Display for QueryExpOperator { |
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { |
||||
write!(f,"{:?}", self) |
||||
} |
||||
} |
||||
#[derive(Debug, Serialize, Deserialize)] |
||||
|
||||
pub enum QueryExpOperation { |
||||
Equal, |
||||
NotEqual, |
||||
GreaterThan, |
||||
LessThan, |
||||
} |
||||
impl std::fmt::Display for QueryExpOperation { |
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { |
||||
write!(f,"{:?}", self) |
||||
} |
||||
} |
||||
|
||||
#[derive(Debug, Serialize, Deserialize)] |
||||
pub struct QuerySetExpression { |
||||
pub QueryExpressionOperator: QueryExpOperator, |
||||
pub QueryExpressionOperation: QueryExpOperation, |
||||
pub FieldName: String, |
||||
pub FieldFilterValue1: String, |
||||
pub FieldFilterValue2: Option<String>, |
||||
} |
||||
impl QuerySetExpression { |
||||
fn field_comp(field: String, value: String, comparison: QueryExpOperation) -> Self { |
||||
QuerySetExpression { |
||||
QueryExpressionOperator : QueryExpOperator::AND, |
||||
QueryExpressionOperation : comparison, |
||||
FieldName : field, |
||||
FieldFilterValue1: value, |
||||
FieldFilterValue2: None |
||||
} |
||||
} |
||||
|
||||
} |
||||
|
||||
//
|
||||
// Query Ordering Exp
|
||||
//
|
||||
#[derive(Debug, Serialize, Deserialize)] |
||||
pub enum QueryOrd { |
||||
ASC, |
||||
DESC |
||||
} |
||||
impl std::fmt::Display for QueryOrd { |
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { |
||||
write!(f,"{:?}", self) |
||||
} |
||||
} |
||||
#[derive(Debug, Serialize, Deserialize)] |
||||
pub struct QueryOrderingDefinitions { |
||||
pub QueryOrderingDirection: QueryOrd, |
||||
pub FieldName : String |
||||
} |
||||
impl QueryOrderingDefinitions { |
||||
fn new(query_ord_dir: QueryOrd, field_to_ord: String) -> Self { |
||||
QueryOrderingDefinitions { |
||||
QueryOrderingDirection: query_ord_dir, |
||||
FieldName: field_to_ord |
||||
} |
||||
} |
||||
} |
||||
|
||||
#[derive(Debug, Serialize, Deserialize)] |
||||
pub struct QuerySetDefinitions { |
||||
pub QuerySetIndex : usize, |
||||
pub QuerySetOperator : QueryExpOperator, |
||||
pub QuerySetExpressions: Vec<QuerySetExpression> |
||||
} |
||||
impl QuerySetDefinitions { |
||||
fn new_default(query_set_expressions: Vec<QuerySetExpression>, index: Option<usize>, operator: Option<QueryExpOperator>) -> Self { |
||||
QuerySetDefinitions { |
||||
QuerySetIndex : index.unwrap_or(0), |
||||
QuerySetOperator : operator.unwrap_or(QueryExpOperator::AND), |
||||
QuerySetExpressions : query_set_expressions |
||||
} |
||||
} |
||||
} |
||||
|
||||
|
||||
// Full Search Issue Query
|
||||
#[derive(Debug, Serialize, Deserialize)] |
||||
pub struct IssueSearchQueryDTO { |
||||
pub QuerySetDefinitions : Vec<QuerySetDefinitions>, |
||||
pub QueryOrderingDefinitions: Vec<QueryOrderingDefinitions>, |
||||
pub PageIndex: usize, |
||||
pub PageSize: usize, |
||||
pub CanIncludeNotes: bool |
||||
} |
||||
impl IssueSearchQueryDTO { |
||||
fn new(query_set_defs: Vec<QuerySetDefinitions>, query_ord: Vec<QueryOrderingDefinitions>, page_size: Option<usize>, page_index: Option<usize>, include_notes: Option<bool>) -> Self { |
||||
IssueSearchQueryDTO { |
||||
QuerySetDefinitions : query_set_defs, |
||||
QueryOrderingDefinitions : query_ord, |
||||
PageSize : page_size.unwrap_or(100), |
||||
PageIndex : page_index.unwrap_or(0), |
||||
CanIncludeNotes : include_notes.unwrap_or(true) |
||||
} |
||||
} |
||||
fn from_file(file_path: String) -> Result<Self, Box< dyn Error>>{ |
||||
let file = File::open(file_path)?; |
||||
let reader = BufReader::new(file); |
||||
let json = serde_json::from_reader(reader)?; |
||||
Ok(json) |
||||
} |
||||
|
||||
fn add_query_expression<T: std::string::ToString>(&mut self, field_name: String, field_value: T, field_value2: Option<String>, operator : Option<QueryExpOperator>, operation: Option<QueryExpOperation>) { |
||||
let new_query = QuerySetExpression { |
||||
QueryExpressionOperator: operator.unwrap_or(QueryExpOperator::AND), |
||||
QueryExpressionOperation: operation.unwrap_or(QueryExpOperation::Equal), |
||||
FieldName: field_name, |
||||
FieldFilterValue1: field_value.to_string(), |
||||
FieldFilterValue2: field_value2, |
||||
}; |
||||
self.QuerySetDefinitions.push(QuerySetDefinitions::new_default(vec![new_query], None, None)); |
||||
} |
||||
fn replace_field_name(&mut self, cur_field_name: String, new_field_name: String) { |
||||
for qsd in &mut self.QuerySetDefinitions { |
||||
for qes in &mut qsd.QuerySetExpressions { |
||||
match &qes.FieldName == &cur_field_name { |
||||
true => {qes.FieldName = new_field_name; return ()}, |
||||
false => {} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
fn replace_field_values(&mut self, field_name: String, new_field_values: [Option<String>;2]) { |
||||
for qsd in &mut self.QuerySetDefinitions { |
||||
for qes in &mut qsd.QuerySetExpressions { |
||||
match &qes.FieldName == &field_name { |
||||
true => {qes.FieldFilterValue1 = new_field_values[0].clone().unwrap().to_string(); qes.FieldFilterValue2 = new_field_values[1].clone(); return ()}, |
||||
false => {} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
fn to_string(&self) -> String { |
||||
serde_json::to_string(&self).unwrap() |
||||
} |
||||
} |
||||
|
||||
|
||||
///
|
||||
/// These are responses
|
||||
///
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)] |
||||
struct MetaData { |
||||
Key: String, |
||||
Value: String |
||||
} |
||||
#[derive(Debug, Serialize, Deserialize)] |
||||
struct UserDefinedFields { |
||||
ExtensionDate: Vec<String>, |
||||
MetaData : Vec<MetaData>, |
||||
IssueNumber: i32, |
||||
DisplayName: String, |
||||
UserDefinedFieldID: i16, |
||||
Value: String, |
||||
} |
||||
|
||||
#[derive(Debug, Serialize, Deserialize)] |
||||
pub struct Collection { |
||||
pub IssueNumber: i32, |
||||
pub IssueSolition: Option<String>, |
||||
pub Status: String, |
||||
pub ClosedBy: Option<String>, |
||||
pub ClosedDate: Option<String>, |
||||
pub SubmittedDate : String, |
||||
pub CauseID: Option<String>, |
||||
pub Notes: Vec<String>, |
||||
pub UserDefinedFields: Vec<String>, |
||||
pub Subject: String, |
||||
pub Description: String, |
||||
pub IsDescriptionRichText: bool, |
||||
pub IssueTypeID: usize, |
||||
pub IssueSubtypeID: usize, |
||||
pub IssueSubtypeI2D: usize, |
||||
pub IssueSubtype3ID: usize, |
||||
pub IssueSubtype4ID: usize, |
||||
pub Priority: String, |
||||
pub AssetNumber: Option<String>, |
||||
pub LocationID: String, |
||||
pub SubmittedBy: String, |
||||
pub AssignedTo: String, |
||||
pub TargetDate: Option<String>, |
||||
pub RequiredByDate: Option<String>, |
||||
pub NextActionTo: Option<String>, |
||||
pub SubStatusID: usize, |
||||
pub ProjectID : usize, |
||||
pub OrganizationID: usize, |
||||
pub ShouldNeverSendEmailForIssue: Option<bool>, |
||||
pub ClassID: usize, |
||||
pub DeparmentID: usize, |
||||
pub SpecialFunction1: String, |
||||
pub SpecialFunction2: String, |
||||
pub SpecialFunction3: String, |
||||
pub SpecialFunction4: String, |
||||
pub SpecialFunction5: String,
|
||||
} |
||||
impl ToString for Collection { |
||||
fn to_string(&self) -> String { |
||||
serde_json::to_string(&self).unwrap() |
||||
} |
||||
} |
||||
|
||||
#[derive(Debug, Serialize, Deserialize)] |
||||
pub struct IssueSearchResponse { |
||||
IsPageBased: bool, |
||||
PageIndex: usize, |
||||
CountForPage: usize, |
||||
PageSize: usize, |
||||
TotalCount: usize, |
||||
Collection: Vec<Collection>, |
||||
} |
||||
impl IssueSearchResponse { |
||||
fn got_all_results(&self) -> bool { |
||||
self.TotalCount == self.CountForPage |
||||
} |
||||
} |
||||
@ -0,0 +1,3 @@ |
||||
pub mod client; |
||||
pub mod dto; |
||||
pub mod tests; |
||||
@ -0,0 +1,8 @@ |
||||
use crate::client; |
||||
use crate::dto; |
||||
|
||||
#[test] |
||||
fn test_update_issue_by_id() { |
||||
let client = client::IssueTrakClient::new("", "https://itsupport..com/"); |
||||
client.update_issue_by_id(177951 , "test", "Test") |
||||
} |
||||
Loading…
Reference in new issue