use const_format::concatcp;
use pdk_core::classy::client::{HttpClient, HttpClientError, HttpClientResponse, Service, Uri};
use pdk_core::classy::extract::context::ConfigureContext;
use pdk_core::classy::extract::{extractability, Extract as _, FromContext};
use pdk_core::host::property::PropertyAccessor;
use pdk_core::logger;
use std::{fmt::Display, str::FromStr as _};
use pdk_core::policy_context::metadata::PolicyMetadata;
use crate::{
api::validator::ExtractionError, implementation::constants::PLATFORM_TIMEOUT_DURATION,
};
use super::schemas::LoginPayload;
const CONTRACTS_COLLECTOR_VERSION: &str = env!("CARGO_PKG_VERSION");
const PLATFORM_OAUTH_PATH: &str = "/accounts/oauth2/token";
const AUTHORIZATION_PREFIX: &str = "Bearer ";
const ACCEPT_HASH_ALGORITHM_HEADER: &str = "X-Accept-Hash-Algorithm";
const FLEX_VERSION: &str = concatcp!("Flex ", CONTRACTS_COLLECTOR_VERSION);
fn contracts_base_path(org_id: &str, env_id: &str, api_id: &str) -> String {
format!(
"/apigateway/ccs/v3/organizations/{org_id}/environments/{env_id}/apis/{api_id}/contracts"
)
}
pub struct BasePath(String);
impl BasePath {
pub fn new(mut base_path: String) -> Self {
if base_path == "/" {
base_path = "".to_string();
}
Self(base_path)
}
}
impl Display for BasePath {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
pub struct HttpPlatformClient {
http_client: HttpClient,
client_id: String,
client_secret: String,
service: Service,
base_path: BasePath,
org_id: String,
env_id: String,
}
impl HttpPlatformClient {
pub async fn login(&self) -> Result<HttpClientResponse, HttpClientError> {
logger::info!(
"trying to log in with service {}",
self.service.cluster_name()
);
let login_data = LoginPayload::new(self.client_id.as_str(), self.client_secret.as_str());
let json = serde_json::to_vec(&login_data).unwrap();
let path = format!("{}{}", self.base_path, PLATFORM_OAUTH_PATH);
let headers = vec![("content-type", "application/json")];
self.send_request("POST", path.as_str(), headers, Some(&json))
.await
}
pub async fn contracts(
&self,
token: &str,
api_id: &str,
algorithm: &str,
next_url: Option<String>,
) -> Result<HttpClientResponse, HttpClientError> {
let resolved_path =
next_url.unwrap_or_else(|| contracts_base_path(&self.org_id, &self.env_id, api_id));
let path = format!("{}{}", self.base_path, resolved_path);
let auth_value = format!("{AUTHORIZATION_PREFIX}{token}");
let additional_headers: Vec<(&str, &str)> = vec![
("Authorization", auth_value.as_str()),
(ACCEPT_HASH_ALGORITHM_HEADER, algorithm),
];
self.send_request("GET", path.as_str(), additional_headers, None)
.await
}
async fn send_request(
&self,
method: &str,
path: &str,
additional_headers: Vec<(&str, &str)>,
body: Option<&[u8]>,
) -> Result<HttpClientResponse, HttpClientError> {
let mut headers = vec![("User-Agent", FLEX_VERSION)];
headers.extend(additional_headers);
logger::debug!(
"PlatformClient: Sending request to {} {}",
self.service.cluster_name(),
path,
);
self.http_client
.request(&self.service)
.headers(headers)
.body(body.unwrap_or_default())
.path(path)
.timeout(PLATFORM_TIMEOUT_DURATION)
.send(method)
.await
}
}
impl FromContext<ConfigureContext, extractability::Transitive> for HttpPlatformClient {
type Error = ExtractionError;
fn from_context(context: &ConfigureContext) -> Result<Self, Self::Error> {
let property_accessor: &dyn PropertyAccessor = context.extract_always();
let metadata = PolicyMetadata::from(property_accessor);
let environment = metadata
.anypoint_environment()
.ok_or(ExtractionError::EnvironmentContext)?;
let anypoint = environment
.anypoint()
.ok_or(ExtractionError::AnypointContext)?;
let http_client = context.extract_always();
Ok(Self {
http_client,
client_id: anypoint.client_id().to_string(),
client_secret: anypoint.client_secret().to_string(),
service: Service::new(
anypoint.service_name(),
Uri::from_str(anypoint.url()).expect("invalid authority"),
),
base_path: BasePath::new(anypoint.base_path()),
org_id: environment.organization_id().to_string(),
env_id: environment.environment_id().to_string(),
})
}
}