use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, HashMap};
use std::path::Path;
use std::time::Duration;
use url::Url;
use url::form_urlencoded;
use crate::{
Credentials, Error, GouqiConfig, Result,
env::{load_config_from_env, load_credentials_from_env, load_host_from_env},
};
#[derive(Default, Clone, Debug)]
pub struct SearchOptions {
params: BTreeMap<&'static str, String>,
fields_explicitly_set: bool,
}
impl SearchOptions {
pub fn builder() -> SearchOptionsBuilder {
SearchOptionsBuilder::new()
}
pub fn serialize(&self) -> Option<String> {
if self.params.is_empty() {
None
} else {
Some(
form_urlencoded::Serializer::new(String::new())
.extend_pairs(&self.params)
.finish(),
)
}
}
pub fn as_builder(&self) -> SearchOptionsBuilder {
SearchOptionsBuilder::copy_from(self)
}
pub fn fields_explicitly_set(&self) -> bool {
self.fields_explicitly_set
}
pub fn start_at(&self) -> Option<u64> {
self.params.get("startAt").and_then(|s| s.parse().ok())
}
pub fn max_results(&self) -> Option<u64> {
self.params.get("maxResults").and_then(|s| s.parse().ok())
}
}
#[derive(Default, Debug)]
pub struct SearchOptionsBuilder {
params: BTreeMap<&'static str, String>,
fields_explicitly_set: bool,
}
impl SearchOptionsBuilder {
pub fn new() -> SearchOptionsBuilder {
SearchOptionsBuilder {
..Default::default()
}
}
fn copy_from(search_options: &SearchOptions) -> SearchOptionsBuilder {
SearchOptionsBuilder {
params: search_options.params.clone(),
fields_explicitly_set: search_options.fields_explicitly_set,
}
}
pub fn fields<F>(&mut self, fs: Vec<F>) -> &mut SearchOptionsBuilder
where
F: Into<String>,
{
self.params.insert(
"fields",
fs.into_iter()
.map(|f| f.into())
.collect::<Vec<String>>()
.join(","),
);
self.fields_explicitly_set = true;
self
}
pub fn validate(&mut self, v: bool) -> &mut SearchOptionsBuilder {
self.params.insert("validateQuery", v.to_string());
self
}
pub fn max_results(&mut self, m: u64) -> &mut SearchOptionsBuilder {
self.params.insert("maxResults", m.to_string());
self
}
pub fn start_at(&mut self, s: u64) -> &mut SearchOptionsBuilder {
self.params.insert("startAt", s.to_string());
self
}
pub fn type_name(&mut self, t: &str) -> &mut SearchOptionsBuilder {
self.params.insert("type", t.to_string());
self
}
pub fn name(&mut self, n: &str) -> &mut SearchOptionsBuilder {
self.params.insert("name", n.to_string());
self
}
pub fn project_key_or_id(&mut self, id: &str) -> &mut SearchOptionsBuilder {
self.params.insert("projectKeyOrId", id.to_string());
self
}
pub fn expand<E>(&mut self, ex: Vec<E>) -> &mut SearchOptionsBuilder
where
E: Into<String>,
{
self.params.insert(
"expand",
ex.into_iter()
.map(|e| e.into())
.collect::<Vec<String>>()
.join(","),
);
self
}
pub fn properties<P>(&mut self, props: Vec<P>) -> &mut SearchOptionsBuilder
where
P: Into<String>,
{
self.params.insert(
"properties",
props
.into_iter()
.map(|p| p.into())
.collect::<Vec<String>>()
.join(","),
);
self
}
pub fn state(&mut self, s: &str) -> &mut SearchOptionsBuilder {
self.params.insert("state", s.to_string());
self
}
pub fn jql(&mut self, s: &str) -> &mut SearchOptionsBuilder {
self.params.insert("jql", s.to_string());
self
}
pub fn validate_query(&mut self, v: bool) -> &mut SearchOptionsBuilder {
self.params.insert("validateQuery", v.to_string());
self
}
#[doc(hidden)]
pub fn next_page_token(&mut self, token: &str) -> &mut SearchOptionsBuilder {
self.params.insert("nextPageToken", token.to_string());
self
}
pub fn essential_fields(&mut self) -> &mut SearchOptionsBuilder {
self.fields(vec!["id", "self", "key", "summary", "status"])
}
pub fn standard_fields(&mut self) -> &mut SearchOptionsBuilder {
self.fields(vec![
"id", "self", "key", "summary", "status", "assignee", "reporter", "created", "updated",
])
}
pub fn all_fields(&mut self) -> &mut SearchOptionsBuilder {
self.fields(vec!["*all"])
}
pub fn minimal_fields(&mut self) -> &mut SearchOptionsBuilder {
self.fields(vec!["id"])
}
pub fn build(&self) -> SearchOptions {
SearchOptions {
params: self.params.clone(),
fields_explicitly_set: self.fields_explicitly_set,
}
}
}
#[derive(Debug, Clone)]
pub struct JiraBuilder {
host: Option<String>,
credentials: Option<Credentials>,
config: GouqiConfig,
custom_fields: HashMap<String, FieldSchema>,
validate_ssl: bool,
user_agent: Option<String>,
}
impl JiraBuilder {
pub fn new() -> Self {
Self {
host: None,
credentials: None,
config: GouqiConfig::default(),
custom_fields: HashMap::new(),
validate_ssl: true,
user_agent: None,
}
}
pub fn host<H: Into<String>>(mut self, host: H) -> Self {
let host_str = host.into();
if Url::parse(&host_str).is_err() {
panic!("Invalid host URL: {}", host_str);
}
self.host = Some(host_str);
self
}
pub fn credentials(mut self, credentials: Credentials) -> Self {
self.credentials = Some(credentials);
self
}
pub fn config_from_file<P: AsRef<Path>>(mut self, path: P) -> Result<Self> {
let config = GouqiConfig::from_file(path)?;
self.config = self.config.merge(config);
Ok(self)
}
pub fn config_from_env(mut self) -> Result<Self> {
if self.host.is_none() {
if let Some(host) = load_host_from_env() {
self = self.host(host);
}
}
if self.credentials.is_none() {
let creds = load_credentials_from_env();
if !matches!(creds, Credentials::Anonymous) {
self = self.credentials(creds);
}
}
let env_config = load_config_from_env();
self.config = self.config.merge(env_config);
Ok(self)
}
pub fn config_template(mut self, template: ConfigTemplate) -> Self {
self.config = match template {
ConfigTemplate::Default => GouqiConfig::default(),
ConfigTemplate::HighThroughput => GouqiConfig::high_throughput(),
ConfigTemplate::LowResource => GouqiConfig::low_resource(),
};
self
}
pub fn timeout(mut self, timeout: Duration) -> Self {
self.config.timeout.default = timeout;
self
}
pub fn connect_timeout(mut self, timeout: Duration) -> Self {
self.config.timeout.connect = timeout;
self
}
pub fn read_timeout(mut self, timeout: Duration) -> Self {
self.config.timeout.read = timeout;
self
}
pub fn retry_policy(
mut self,
max_attempts: u32,
base_delay: Duration,
max_delay: Duration,
) -> Self {
self.config.retry.max_attempts = max_attempts;
self.config.retry.base_delay = base_delay;
self.config.retry.max_delay = max_delay;
self
}
pub fn retry_backoff(mut self, multiplier: f64) -> Self {
self.config.retry.backoff_multiplier = multiplier;
self
}
pub fn retry_status_codes(mut self, codes: Vec<u16>) -> Self {
self.config.retry.retry_status_codes = codes;
self
}
pub fn connection_pool_size(mut self, size: usize) -> Self {
self.config.connection_pool.max_connections_per_host = size;
self
}
pub fn connection_pool(
mut self,
max_connections: usize,
idle_timeout: Duration,
http2: bool,
) -> Self {
self.config.connection_pool.max_connections_per_host = max_connections;
self.config.connection_pool.idle_timeout = idle_timeout;
self.config.connection_pool.http2 = http2;
self
}
pub fn validate_ssl(mut self, validate: bool) -> Self {
self.validate_ssl = validate;
self
}
pub fn user_agent<S: Into<String>>(mut self, user_agent: S) -> Self {
self.user_agent = Some(user_agent.into());
self
}
pub fn enable_cache(mut self) -> Self {
self.config.cache.enabled = true;
self
}
pub fn disable_cache(mut self) -> Self {
self.config.cache.enabled = false;
self
}
pub fn memory_cache(mut self, default_ttl: Duration, max_entries: usize) -> Self {
self.config.cache.enabled = true;
self.config.cache.default_ttl = default_ttl;
self.config.cache.max_entries = max_entries;
self
}
pub fn rate_limit(mut self, requests_per_second: f64, burst_capacity: u32) -> Self {
self.config.rate_limiting.enabled = true;
self.config.rate_limiting.requests_per_second = requests_per_second;
self.config.rate_limiting.burst_capacity = burst_capacity;
self
}
pub fn disable_rate_limiting(mut self) -> Self {
self.config.rate_limiting.enabled = false;
self
}
pub fn enable_metrics(mut self) -> Self {
self.config.metrics.enabled = true;
self
}
pub fn disable_metrics(mut self) -> Self {
self.config.metrics.enabled = false;
self
}
pub fn metrics_config(mut self, collection_interval: Duration, export_format: &str) -> Self {
self.config.metrics.enabled = true;
self.config.metrics.collection_interval = collection_interval;
self.config.metrics.export.format = export_format.to_string();
self
}
pub fn custom_field<N: Into<String>>(mut self, name: N, schema: FieldSchema) -> Self {
let field_name = name.into();
assert!(!field_name.is_empty(), "Field name cannot be empty");
self.custom_fields.insert(field_name, schema);
self
}
pub fn custom_fields(mut self, fields: HashMap<String, FieldSchema>) -> Self {
self.custom_fields.extend(fields);
self
}
pub fn build_with_validation(self) -> Result<crate::Jira> {
let host = self.host.clone().ok_or_else(|| Error::ConfigError {
message: "Host URL is required".to_string(),
})?;
let credentials = self.credentials.clone().ok_or_else(|| Error::ConfigError {
message: "Credentials are required".to_string(),
})?;
let _parsed_url = Url::parse(&host).map_err(|e| Error::ConfigError {
message: format!("Invalid host URL '{}': {}", host, e),
})?;
self.config.validate()?;
self.validate_credentials(&credentials)?;
self.validate_custom_fields()?;
self.build()
}
pub fn build(self) -> Result<crate::Jira> {
let host = self
.host
.unwrap_or_else(|| "http://localhost:8080".to_string());
let credentials = self.credentials.unwrap_or(Credentials::Anonymous);
let client = crate::Jira::new(host, credentials)?;
Ok(client)
}
fn validate_credentials(&self, credentials: &Credentials) -> Result<()> {
match credentials {
Credentials::Basic(user, pass) => {
if user.is_empty() || pass.is_empty() {
return Err(Error::ConfigError {
message: "Username and password cannot be empty for Basic auth".to_string(),
});
}
}
Credentials::Bearer(token) => {
if token.is_empty() {
return Err(Error::ConfigError {
message: "Bearer token cannot be empty".to_string(),
});
}
}
Credentials::Cookie(cookie) => {
if cookie.is_empty() {
return Err(Error::ConfigError {
message: "Cookie cannot be empty".to_string(),
});
}
}
#[cfg(feature = "oauth")]
Credentials::OAuth1a {
consumer_key,
private_key_pem,
access_token,
access_token_secret,
} => {
if consumer_key.is_empty() {
return Err(Error::ConfigError {
message: "OAuth consumer key cannot be empty".to_string(),
});
}
if private_key_pem.is_empty() {
return Err(Error::ConfigError {
message: "OAuth private key cannot be empty".to_string(),
});
}
if access_token.is_empty() {
return Err(Error::ConfigError {
message: "OAuth access token cannot be empty".to_string(),
});
}
if access_token_secret.is_empty() {
return Err(Error::ConfigError {
message: "OAuth access token secret cannot be empty".to_string(),
});
}
}
Credentials::Anonymous => {
}
}
Ok(())
}
fn validate_custom_fields(&self) -> Result<()> {
for (field_name, schema) in &self.custom_fields {
schema.validate(field_name)?;
}
Ok(())
}
#[cfg(test)]
pub fn get_host(&self) -> &Option<String> {
&self.host
}
#[cfg(test)]
pub fn get_credentials(&self) -> &Option<Credentials> {
&self.credentials
}
#[cfg(test)]
pub fn get_config(&self) -> &GouqiConfig {
&self.config
}
#[cfg(test)]
pub fn get_custom_fields(&self) -> &HashMap<String, FieldSchema> {
&self.custom_fields
}
}
impl Default for JiraBuilder {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Copy)]
pub enum ConfigTemplate {
Default,
HighThroughput,
LowResource,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FieldSchema {
pub field_type: String,
pub required: bool,
pub default_value: Option<serde_json::Value>,
pub allowed_values: Option<Vec<serde_json::Value>>,
pub custom_properties: HashMap<String, serde_json::Value>,
}
impl FieldSchema {
pub fn text(required: bool) -> Self {
Self {
field_type: "string".to_string(),
required,
default_value: None,
allowed_values: None,
custom_properties: HashMap::new(),
}
}
pub fn number(required: bool, min: Option<f64>, max: Option<f64>) -> Self {
let mut properties = HashMap::new();
if let (Some(min_val), Some(max_val)) = (min, max) {
assert!(
min_val <= max_val,
"Minimum value cannot be greater than maximum value"
);
}
if let Some(min_val) = min {
properties.insert(
"minimum".to_string(),
serde_json::Value::Number(serde_json::Number::from_f64(min_val).unwrap()),
);
}
if let Some(max_val) = max {
properties.insert(
"maximum".to_string(),
serde_json::Value::Number(serde_json::Number::from_f64(max_val).unwrap()),
);
}
Self {
field_type: "number".to_string(),
required,
default_value: None,
allowed_values: None,
custom_properties: properties,
}
}
pub fn integer(required: bool, min: Option<i64>, max: Option<i64>) -> Self {
let mut properties = HashMap::new();
if let (Some(min_val), Some(max_val)) = (min, max) {
assert!(
min_val <= max_val,
"Minimum value cannot be greater than maximum value"
);
}
if let Some(min_val) = min {
properties.insert(
"minimum".to_string(),
serde_json::Value::Number(serde_json::Number::from(min_val)),
);
}
if let Some(max_val) = max {
properties.insert(
"maximum".to_string(),
serde_json::Value::Number(serde_json::Number::from(max_val)),
);
}
Self {
field_type: "integer".to_string(),
required,
default_value: None,
allowed_values: None,
custom_properties: properties,
}
}
pub fn boolean(required: bool, default: Option<bool>) -> Self {
Self {
field_type: "boolean".to_string(),
required,
default_value: default.map(serde_json::Value::Bool),
allowed_values: None,
custom_properties: HashMap::new(),
}
}
pub fn enumeration<V: Serialize>(required: bool, allowed_values: Vec<V>) -> Result<Self> {
let values: std::result::Result<Vec<serde_json::Value>, crate::Error> = allowed_values
.into_iter()
.map(|v| serde_json::to_value(v).map_err(Error::Serde))
.collect();
Ok(Self {
field_type: "enum".to_string(),
required,
default_value: None,
allowed_values: Some(values?),
custom_properties: HashMap::new(),
})
}
pub fn date(required: bool) -> Self {
Self {
field_type: "date".to_string(),
required,
default_value: None,
allowed_values: None,
custom_properties: HashMap::new(),
}
}
pub fn datetime(required: bool) -> Self {
Self {
field_type: "datetime".to_string(),
required,
default_value: None,
allowed_values: None,
custom_properties: HashMap::new(),
}
}
pub fn array(required: bool, item_type: &str) -> Self {
let mut properties = HashMap::new();
properties.insert(
"item_type".to_string(),
serde_json::Value::String(item_type.to_string()),
);
Self {
field_type: "array".to_string(),
required,
default_value: None,
allowed_values: None,
custom_properties: properties,
}
}
pub fn with_default<V: Serialize>(mut self, default: V) -> Result<Self> {
self.default_value = Some(serde_json::to_value(default).map_err(Error::Serde)?);
Ok(self)
}
pub fn with_property<V: Serialize>(mut self, key: &str, value: V) -> Result<Self> {
self.custom_properties.insert(
key.to_string(),
serde_json::to_value(value).map_err(Error::Serde)?,
);
Ok(self)
}
fn validate(&self, field_name: &str) -> Result<()> {
if self.field_type.is_empty() {
return Err(Error::FieldSchemaError {
field: field_name.to_string(),
message: "Field type cannot be empty".to_string(),
});
}
if self.field_type == "enum" && self.allowed_values.is_none() {
return Err(Error::FieldSchemaError {
field: field_name.to_string(),
message: "Enum fields must specify allowed values".to_string(),
});
}
if matches!(self.field_type.as_str(), "number" | "integer") {
if let (Some(min), Some(max)) = (
self.custom_properties
.get("minimum")
.and_then(|v| v.as_f64()),
self.custom_properties
.get("maximum")
.and_then(|v| v.as_f64()),
) {
if min > max {
return Err(Error::FieldSchemaError {
field: field_name.to_string(),
message: "Minimum value cannot be greater than maximum value".to_string(),
});
}
}
}
if self.field_type == "array" && !self.custom_properties.contains_key("item_type") {
return Err(Error::FieldSchemaError {
field: field_name.to_string(),
message: "Array fields must specify item_type".to_string(),
});
}
Ok(())
}
}