use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::str::FromStr;
use std::{hash::Hash, time::Duration};
use crate::match_validation::http_validator_v2::HttpValidatorV2;
use super::aws_validator::AwsValidator;
use super::config_v2::CustomHttpConfigV2;
use super::http_validator::HttpValidator;
use super::match_validator::MatchValidator;
pub const DEFAULT_HTTPS_TIMEOUT_SEC: u64 = 3;
pub const DEFAULT_AWS_STS_ENDPOINT: &str = "https://sts.amazonaws.com";
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub struct AwsConfig {
#[serde(default = "default_aws_sts_endpoint")]
pub aws_sts_endpoint: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub forced_datetime_utc: Option<DateTime<Utc>>,
#[serde(default = "default_timeout")]
pub timeout: Duration,
}
fn default_aws_sts_endpoint() -> String {
DEFAULT_AWS_STS_ENDPOINT.to_string()
}
fn default_timeout() -> Duration {
Duration::from_secs(DEFAULT_HTTPS_TIMEOUT_SEC)
}
impl Default for AwsConfig {
fn default() -> Self {
AwsConfig {
aws_sts_endpoint: default_aws_sts_endpoint(),
forced_datetime_utc: None,
timeout: default_timeout(),
}
}
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
#[serde(tag = "kind")]
pub enum AwsType {
AwsId,
AwsSecret(AwsConfig),
AwsSession,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Hash)]
#[serde(rename_all = "UPPERCASE")]
pub enum HttpMethod {
Get,
Post,
Put,
Delete,
Patch,
}
impl FromStr for HttpMethod {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_uppercase().as_str() {
"GET" => Ok(HttpMethod::Get),
"POST" => Ok(HttpMethod::Post),
"PUT" => Ok(HttpMethod::Put),
"DELETE" => Ok(HttpMethod::Delete),
"PATCH" => Ok(HttpMethod::Patch),
_ => Err(format!("Invalid HTTP method: {s}")),
}
}
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub struct RequestHeader {
pub key: String,
pub value: String,
}
impl RequestHeader {
pub fn get_value_with_match(&self, matche: &str) -> String {
let mut value = self.value.clone();
value = value.replace("$MATCH", matche);
value
}
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub struct HttpValidatorOption {
pub timeout: Duration,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub struct CustomHttpConfig {
pub endpoint: String,
#[serde(default)]
pub hosts: Vec<String>,
#[serde(default = "default_http_method")]
pub http_method: HttpMethod,
pub request_headers: BTreeMap<String, String>,
#[serde(default = "default_valid_http_status_code")]
pub valid_http_status_code: Vec<HttpStatusCodeRange>,
#[serde(default = "default_invalid_http_status_code")]
pub invalid_http_status_code: Vec<HttpStatusCodeRange>,
#[serde(default = "default_timeout_seconds")]
pub timeout_seconds: u32,
}
impl Default for CustomHttpConfig {
fn default() -> Self {
CustomHttpConfig {
endpoint: "".to_string(),
hosts: vec![],
http_method: HttpMethod::Get,
request_headers: BTreeMap::new(),
valid_http_status_code: vec![],
invalid_http_status_code: vec![],
timeout_seconds: DEFAULT_HTTPS_TIMEOUT_SEC as u32,
}
}
}
impl CustomHttpConfig {
pub fn get_endpoints(&self) -> Result<Vec<String>, String> {
if self.endpoint.contains("$HOST") && self.hosts.is_empty() {
return Err("Endpoint contains $HOST but no hosts are provided".to_string());
}
if !self.endpoint.contains("$HOST") && !self.hosts.is_empty() {
return Err("Endpoint does not contain $HOST but hosts are provided".to_string());
}
let mut endpoints = vec![];
for host in self.hosts.clone() {
endpoints.push(self.endpoint.replace("$HOST", &host));
}
if endpoints.is_empty() {
endpoints.push(self.endpoint.to_string());
}
Ok(endpoints)
}
pub fn with_endpoint(mut self, endpoint: String) -> Self {
self.endpoint = endpoint;
self
}
pub fn with_hosts(mut self, hosts: Vec<String>) -> Self {
self.hosts = hosts;
self
}
pub fn with_request_headers(mut self, request_headers: BTreeMap<String, String>) -> Self {
self.request_headers = request_headers;
self
}
pub fn with_valid_http_status_code(
mut self,
valid_http_status_code: Vec<HttpStatusCodeRange>,
) -> Self {
self.valid_http_status_code = valid_http_status_code;
self
}
pub fn with_invalid_http_status_code(
mut self,
invalid_http_status_code: Vec<HttpStatusCodeRange>,
) -> Self {
self.invalid_http_status_code = invalid_http_status_code;
self
}
pub fn set_endpoint(&mut self, endpoint: String) {
self.endpoint = endpoint;
}
pub fn set_hosts(&mut self, hosts: Vec<String>) {
self.hosts = hosts;
}
pub fn set_http_method(&mut self, http_method: HttpMethod) {
self.http_method = http_method;
}
pub fn set_request_headers(&mut self, request_headers: BTreeMap<String, String>) {
self.request_headers = request_headers;
}
pub fn set_valid_http_status_code(&mut self, valid_http_status_code: Vec<HttpStatusCodeRange>) {
self.valid_http_status_code = valid_http_status_code;
}
pub fn set_invalid_http_status_code(
&mut self,
invalid_http_status_code: Vec<HttpStatusCodeRange>,
) {
self.invalid_http_status_code = invalid_http_status_code;
}
pub fn set_timeout_seconds(&mut self, timeout_seconds: u32) {
self.timeout_seconds = timeout_seconds;
}
}
fn default_timeout_seconds() -> u32 {
DEFAULT_HTTPS_TIMEOUT_SEC as u32
}
fn default_http_method() -> HttpMethod {
HttpMethod::Get
}
fn default_valid_http_status_code() -> Vec<HttpStatusCodeRange> {
vec![HttpStatusCodeRange {
start: 200,
end: 300,
}]
}
fn default_invalid_http_status_code() -> Vec<HttpStatusCodeRange> {
vec![HttpStatusCodeRange {
start: 400,
end: 500,
}]
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub struct HttpStatusCodeRange {
pub start: u16,
pub end: u16,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
#[serde(tag = "type", content = "config")]
pub enum MatchValidationType {
Aws(AwsType),
CustomHttp(CustomHttpConfig),
CustomHttpV2(CustomHttpConfigV2),
}
impl MatchValidationType {
pub fn can_create_match_validator(&self) -> bool {
match self {
MatchValidationType::Aws(aws_type) => matches!(aws_type, AwsType::AwsSecret(_)),
MatchValidationType::CustomHttp(_) => true,
MatchValidationType::CustomHttpV2(http_config_v2) => !http_config_v2.calls.is_empty(),
}
}
pub fn get_internal_match_validation_type(&self) -> InternalMatchValidationType {
match self {
MatchValidationType::Aws(_) => InternalMatchValidationType::Aws,
MatchValidationType::CustomHttp(http_config) => {
InternalMatchValidationType::CustomHttp(http_config.get_endpoints().unwrap())
}
MatchValidationType::CustomHttpV2(_) => InternalMatchValidationType::CustomHttpV2,
}
}
pub fn into_match_validator(&self) -> Result<Box<dyn MatchValidator>, String> {
match self {
MatchValidationType::Aws(aws_type) => match aws_type {
AwsType::AwsSecret(aws_config) => {
Ok(Box::new(AwsValidator::new(aws_config.clone())))
}
_ => Err("This aws type shall not be used to create a validator".to_string()),
},
MatchValidationType::CustomHttp(http_config) => Ok(Box::new(
HttpValidator::new_from_config(http_config.clone()),
)),
MatchValidationType::CustomHttpV2(_) => Ok(Box::new(HttpValidatorV2)),
}
}
}
#[derive(PartialEq, Eq, Hash)]
pub enum InternalMatchValidationType {
Aws,
CustomHttp(Vec<String>),
CustomHttpV2,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_serialization_of_aws_config() {
let aws_config = AwsConfig {
aws_sts_endpoint: "https://sts.amazonaws.com".to_string(),
forced_datetime_utc: None,
timeout: Duration::from_secs(3),
};
let serialized = serde_json::to_string(&aws_config).unwrap();
assert_eq!(
serialized,
"{\"aws_sts_endpoint\":\"https://sts.amazonaws.com\",\"timeout\":{\"secs\":3,\"nanos\":0}}"
);
}
}