pdk-contracts-lib 1.9.1-alpha.2

PDK Contracts Library
Documentation
// Copyright (c) 2026, Salesforce, Inc.,
// All rights reserved.
// For full license text, see the LICENSE.txt file

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(),
        })
    }
}