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 crate::implementation::platform::shared::Contract;
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Debug)]
pub struct LoginResponse {
    #[serde(skip_serializing_if = "Option::is_none")]
    access_token: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    token_type: Option<String>,
}

impl LoginResponse {
    pub fn get_token(&self) -> &str {
        self.access_token.as_deref().unwrap_or_default()
    }

    pub fn get_type(&self) -> &str {
        self.token_type.as_deref().unwrap_or_default()
    }
}

#[derive(Serialize, Deserialize, Debug)]
pub struct ContractsLinksResponse {
    #[serde(rename = "self")]
    self_link: String,
    next: String,
}

#[allow(unused)]
#[derive(Deserialize, Debug)]
pub struct ContractsDataResponse {
    #[serde(rename = "organizationId")]
    organization_id: Option<String>,
    #[serde(rename = "contractId")]
    pub contract_id: String,
    #[serde(rename = "apiId")]
    pub api_id: String,
    #[serde(rename = "versionId")]
    pub version_id: String,
    #[serde(rename = "slaTierId")]
    pub sla_tier_id: Option<String>,
    #[serde(rename = "clientId")]
    pub client_id: String,
    #[serde(rename = "clientSecret")]
    pub client_secret: Option<String>,
    #[serde(rename = "clientSecretSalt")]
    pub client_secret_salt: Option<String>,
    #[serde(rename = "contractUpdatedDate")]
    contract_updated_date: Option<String>,
    #[serde(rename = "redirectUris")]
    redirect_uris: Option<Vec<String>>,
    #[serde(rename = "clientName")]
    pub client_name: Option<String>,
    #[serde(rename = "clientDescription")]
    client_description: Option<String>,
    #[serde(rename = "clientUpdatedDate")]
    client_updated_date: Option<String>,
    #[serde(rename = "updatedDate")]
    updated_date: Option<String>,
    pub removed: Option<bool>,
}

#[derive(Deserialize, Debug)]
pub struct ContractsResponse {
    links: ContractsLinksResponse,
    data: Vec<ContractsDataResponse>,
}

impl ContractsResponse {
    pub fn verify_contracts(&self) -> Result<(), Vec<String>> {
        let errors: Vec<String> = self
            .data
            .iter()
            .filter_map(|contract| {
                if contract.removed.unwrap_or_default() {
                    None
                } else {
                    let mut missing_fields = vec![];

                    if contract.client_name.is_none() {
                        missing_fields.push("client_name");
                    }
                    if contract.client_secret.is_some() && contract.client_secret_salt.is_none() {
                        missing_fields.push("client_secret_salt");
                    }
                    if contract.client_secret.is_none() && contract.client_secret_salt.is_some() {
                        missing_fields.push("client_secret");
                    }

                    if missing_fields.is_empty() {
                        None
                    } else {
                        let fields = missing_fields.join(", ");
                        Some(format!(
                            "Non removed contract {} for api {} is missing: {fields}",
                            contract.contract_id, contract.api_id
                        ))
                    }
                }
            })
            .collect();
        if errors.is_empty() {
            Ok(())
        } else {
            Err(errors)
        }
    }
}

impl ContractsResponse {
    pub fn get_links(&self) -> &ContractsLinksResponse {
        &self.links
    }

    pub fn get_data(&self) -> &Vec<ContractsDataResponse> {
        &self.data
    }
}

impl ContractsLinksResponse {
    pub fn self_link(&self) -> &str {
        &self.self_link
    }

    pub fn next_link(&self) -> &str {
        &self.next
    }
}

pub fn contract_from_event(data: &ContractsDataResponse) -> Contract {
    Contract {
        contract_id: data.contract_id.clone(),
        api_id: data.api_id.clone(),
        version_id: data.version_id.clone(),
        sla_tier_id: data.sla_tier_id.clone(),
        client_id: data.client_id.clone(),
        client_secret: data
            .client_secret
            .clone()
            .unwrap_or_else(default_field_value),
        client_secret_salt: data
            .client_secret_salt
            .clone()
            .unwrap_or_else(default_field_value),
        client_name: data.client_name.clone().unwrap_or_else(default_field_value),
        removed: data.removed.unwrap_or(false),
    }
}

fn default_field_value() -> String {
    "None".to_string()
}

#[cfg(test)]
mod tests {
    use crate::implementation::platform::responses::{
        ContractsDataResponse, ContractsLinksResponse, ContractsResponse,
    };

    #[test]
    fn with_no_contracts_then_no_errors_are_generated() {
        let contracts_response = ContractsResponse {
            links: ContractsLinksResponse {
                self_link: "".to_string(),
                next: "".to_string(),
            },
            data: vec![],
        };

        assert_eq!(Ok(()), contracts_response.verify_contracts())
    }

    #[test]
    fn with_valid_contracts_then_no_errors_are_generated() {
        let contracts_response = ContractsResponse {
            links: ContractsLinksResponse {
                self_link: "".to_string(),
                next: "".to_string(),
            },
            data: vec![invalid_removed_contract(), valid_active_contract()],
        };

        assert_eq!(Ok(()), contracts_response.verify_contracts())
    }

    #[test]
    fn with_invalid_contract_errors_are_generated() {
        let contracts_response = ContractsResponse {
            links: ContractsLinksResponse {
                self_link: "".to_string(),
                next: "".to_string(),
            },
            data: vec![
                invalid_removed_contract(),
                valid_active_contract(),
                invalid_active_contract(),
            ],
        };
        let errors = contracts_response.verify_contracts().err().unwrap();
        assert_eq!(1, errors.len());
        assert_eq!(
            "Non removed contract contract id for api api id is missing: client_name",
            errors.first().unwrap()
        );
    }

    #[test]
    fn with_non_removed_contract_without_client_secret_then_no_errors() {
        let contracts_response = ContractsResponse {
            links: ContractsLinksResponse {
                self_link: "".to_string(),
                next: "".to_string(),
            },
            data: vec![active_contract_without_secret()],
        };
        assert_eq!(Ok(()), contracts_response.verify_contracts());
    }

    #[test]
    fn with_client_secret_but_no_salt_then_error() {
        let contracts_response = ContractsResponse {
            links: ContractsLinksResponse {
                self_link: "".to_string(),
                next: "".to_string(),
            },
            data: vec![contract_with_secret_no_salt()],
        };
        let errors = contracts_response.verify_contracts().err().unwrap();
        assert_eq!(1, errors.len());
        assert!(errors.first().unwrap().contains("client_secret_salt"));
    }

    #[test]
    fn with_client_secret_salt_but_no_secret_then_error() {
        let contracts_response = ContractsResponse {
            links: ContractsLinksResponse {
                self_link: "".to_string(),
                next: "".to_string(),
            },
            data: vec![contract_with_salt_no_secret()],
        };
        let errors = contracts_response.verify_contracts().err().unwrap();
        assert_eq!(1, errors.len());
        assert!(errors.first().unwrap().contains("client_secret"));
    }

    fn invalid_active_contract() -> ContractsDataResponse {
        ContractsDataResponse {
            organization_id: None,
            contract_id: "contract id".to_string(),
            api_id: "api id".to_string(),
            version_id: "".to_string(),
            sla_tier_id: None,
            client_id: "".to_string(),
            client_secret: None,
            client_secret_salt: None,
            contract_updated_date: None,
            redirect_uris: None,
            client_name: None,
            client_description: None,
            client_updated_date: None,
            updated_date: None,
            removed: Some(false),
        }
    }

    fn valid_active_contract() -> ContractsDataResponse {
        ContractsDataResponse {
            organization_id: Some("none".to_string()),
            contract_id: "".to_string(),
            api_id: "".to_string(),
            version_id: "".to_string(),
            sla_tier_id: Some("none".to_string()),
            client_id: "".to_string(),
            client_secret: Some("none".to_string()),
            client_secret_salt: Some("none".to_string()),
            contract_updated_date: Some("none".to_string()),
            redirect_uris: Some(vec![]),
            client_name: Some("none".to_string()),
            client_description: Some("none".to_string()),
            client_updated_date: Some("none".to_string()),
            updated_date: Some("none".to_string()),
            removed: None,
        }
    }
    fn invalid_removed_contract() -> ContractsDataResponse {
        ContractsDataResponse {
            organization_id: None,
            contract_id: "".to_string(),
            api_id: "".to_string(),
            version_id: "".to_string(),
            sla_tier_id: None,
            client_id: "".to_string(),
            client_secret: None,
            client_secret_salt: None,
            contract_updated_date: None,
            redirect_uris: None,
            client_name: None,
            client_description: None,
            client_updated_date: None,
            updated_date: None,
            removed: Some(true),
        }
    }

    fn active_contract_without_secret() -> ContractsDataResponse {
        ContractsDataResponse {
            organization_id: None,
            contract_id: "pub-contract".to_string(),
            api_id: "api-1".to_string(),
            version_id: "".to_string(),
            sla_tier_id: None,
            client_id: "public_client".to_string(),
            client_secret: None,
            client_secret_salt: None,
            contract_updated_date: None,
            redirect_uris: None,
            client_name: Some("Public Client".to_string()),
            client_description: None,
            client_updated_date: None,
            updated_date: None,
            removed: Some(false),
        }
    }

    fn contract_with_secret_no_salt() -> ContractsDataResponse {
        ContractsDataResponse {
            organization_id: None,
            contract_id: "c1".to_string(),
            api_id: "api-1".to_string(),
            version_id: "".to_string(),
            sla_tier_id: None,
            client_id: "client1".to_string(),
            client_secret: Some("hashed".to_string()),
            client_secret_salt: None,
            contract_updated_date: None,
            redirect_uris: None,
            client_name: Some("Client 1".to_string()),
            client_description: None,
            client_updated_date: None,
            updated_date: None,
            removed: Some(false),
        }
    }

    fn contract_with_salt_no_secret() -> ContractsDataResponse {
        ContractsDataResponse {
            organization_id: None,
            contract_id: "c2".to_string(),
            api_id: "api-1".to_string(),
            version_id: "".to_string(),
            sla_tier_id: None,
            client_id: "client2".to_string(),
            client_secret: None,
            client_secret_salt: Some("salt".to_string()),
            contract_updated_date: None,
            redirect_uris: None,
            client_name: Some("Client 2".to_string()),
            client_description: None,
            client_updated_date: None,
            updated_date: None,
            removed: Some(false),
        }
    }
}