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