force 0.2.0

Production-ready Salesforce Platform API client with REST and Bulk API 2.0 support
Documentation
//! REST API handler for Salesforce.
//!
//! This module provides the `RestHandler` which serves as the foundation for all
//! REST API operations including CRUD, queries, and metadata operations.

pub(crate) mod crud;
pub(crate) mod describe;

pub(crate) mod limits;
pub(crate) mod query;
pub(crate) mod query_plan_analyzer;
pub(crate) mod search;

pub use crate::api::soql::{SoqlQueryBuilder, escape_soql};
pub use limits::{LimitInfo, OrgLimits};
pub use query_plan_analyzer::{InsightSeverity, QueryInsight, QueryInsights, analyze_query_plan};
pub use search::{SearchAttributes, SearchQueryBuilder, SearchRecords, SearchResult};

use crate::api::rest_operation::RestOperation;
use crate::error::Result;
use serde::de::DeserializeOwned;
use std::sync::Arc;

/// REST API handler for performing Salesforce REST operations.
///
/// The handler provides access to all REST API functionality including:
/// - CRUD operations (Create, Read, Update, Delete, Upsert)
/// - SOQL queries with pagination
/// - SOSL searches
/// - Metadata operations (Describe API)
/// - Org limits
///
/// The handler is obtained from a `ForceClient` and shares its authentication
/// and configuration.
#[derive(Debug)]
pub struct RestHandler<A: crate::auth::Authenticator> {
    /// Reference to the client's inner state.
    inner: Arc<crate::session::Session<A>>,
}

impl<A: crate::auth::Authenticator> Clone for RestHandler<A> {
    fn clone(&self) -> Self {
        Self {
            inner: Arc::clone(&self.inner),
        }
    }
}

impl<A: crate::auth::Authenticator> RestOperation<A> for RestHandler<A> {
    fn session(&self) -> &Arc<crate::session::Session<A>> {
        &self.inner
    }

    fn path_prefix(&self) -> &'static str {
        ""
    }
}

impl<A: crate::auth::Authenticator> RestHandler<A> {
    /// Creates a new REST handler for the given client inner state.
    ///
    /// # Arguments
    ///
    /// * `inner` - The client's inner state
    ///
    /// # Examples
    ///
    /// ```ignore
    /// let handler = RestHandler::new(inner);
    /// ```
    #[must_use]
    pub(crate) fn new(inner: Arc<crate::session::Session<A>>) -> Self {
        Self { inner }
    }

    /// Constructs the base URL for REST API operations.
    ///
    /// The base URL is constructed as: `{instance_url}/services/data/{api_version}`
    ///
    /// This method requires token access to get the instance URL from authentication.
    ///
    /// # Errors
    ///
    /// Returns an error if token retrieval fails.
    ///
    /// # Examples
    ///
    /// ```ignore
    /// let base = handler.base_url().await?;
    /// // Returns: "https://na1.salesforce.com/services/data/v60.0"
    /// ```
    pub async fn base_url(&self) -> Result<String> {
        self.inner.resolve_url("").await
    }

    /// Helper method to execute a GET request and deserialize the response.
    pub(crate) async fn execute_get<T: DeserializeOwned>(
        &self,
        path: &str,
        query: Option<&[(&str, &str)]>,
        error_msg: &str,
    ) -> Result<T> {
        let url = self.inner.resolve_url(path).await?;
        let mut request = self.inner.get(&url);

        if let Some(params) = query {
            request = request.query(params);
        }

        let request = request.build().map_err(crate::error::HttpError::from)?;
        self.inner.send_request_and_decode(request, error_msg).await
    }

    /// Helper method to execute a POST request and deserialize the response.
    pub(crate) async fn execute_post<T: DeserializeOwned>(
        &self,
        path: &str,
        body: &serde_json::Value,
        error_msg: &str,
    ) -> Result<T> {
        let url = self.inner.resolve_url(path).await?;
        let request = self
            .inner
            .post(&url)
            .json(body)
            .build()
            .map_err(crate::error::HttpError::from)?;
        self.inner.send_request_and_decode(request, error_msg).await
    }

    /// Retrieves organization limits.
    ///
    /// Returns information about the organization's usage and limits for various
    /// resources including API requests, storage, workflow emails, and more.
    ///
    /// # Errors
    ///
    /// Returns an error if:
    /// - Authentication fails
    /// - The HTTP request fails
    /// - The response cannot be deserialized
    ///
    /// # Examples
    ///
    /// ```ignore
    /// let limits = client.rest().limits().await?;
    /// let api_limit = &limits.daily_api_requests;
    /// println!("API calls: {}/{}", api_limit.used.unwrap_or(0), api_limit.max);
    /// ```
    pub async fn limits(&self) -> Result<limits::OrgLimits> {
        self.execute_get("limits", None, "Limits API request failed")
            .await
    }

    /// Executes a SOSL (Salesforce Object Search Language) search.
    ///
    /// SOSL allows you to search across multiple objects and fields simultaneously.
    ///
    /// # Arguments
    ///
    /// * `sosl` - The SOSL search query (e.g., "FIND {Acme} IN ALL FIELDS RETURNING Account(Name)")
    ///
    /// # Errors
    ///
    /// Returns an error if:
    /// - Authentication fails
    /// - The HTTP request fails
    /// - The SOSL query is malformed
    /// - The response cannot be deserialized
    ///
    /// # Examples
    ///
    /// ```ignore
    /// use force::api::rest::search::SearchQueryBuilder;
    ///
    /// // Using builder
    /// let query = SearchQueryBuilder::new()
    ///     .find("Acme")
    ///     .in_all_fields()
    ///     .returning("Account", &["Id", "Name"])
    ///     .returning("Contact", &["Id", "Name"])
    ///     .limit(10)
    ///     .build();
    ///
    /// let results = client.rest().search(&query).await?;
    ///
    /// for record_set in &results.search_records {
    ///     println!("Found {} {} records",
    ///         record_set.records.len(),
    ///         record_set.attributes.type_);
    /// }
    /// ```
    pub async fn search(&self, sosl: &str) -> Result<search::SearchResult> {
        self.execute_get("search", Some(&[("q", sosl)]), "SOSL search request failed")
            .await
    }

    /// Retrieves the query execution plan for a SOQL query.
    ///
    /// The Query Plan API (`/query/?explain=`) allows developers to check the
    /// performance cost of a query before executing it. This is useful for
    /// identifying table scans and inefficient filters.
    ///
    /// # Arguments
    ///
    /// * `soql` - The SOQL query string to analyze.
    ///
    /// # Errors
    ///
    /// Returns an error if:
    /// - Authentication fails
    /// - The HTTP request fails
    /// - The response cannot be deserialized
    pub async fn explain(&self, soql: &str) -> Result<crate::types::explain::ExplainResponse> {
        self.execute_get(
            "query",
            Some(&[("explain", soql)]),
            "Query Plan API request failed",
        )
        .await
    }
}
#[cfg(test)]
mod tests {
    use crate::client::{ForceClient, builder};
    use crate::config::ClientConfig;
    use crate::test_support::{MockAuthenticator, Must, MustMsg};

    async fn create_test_client() -> ForceClient<MockAuthenticator> {
        let auth = MockAuthenticator::new("test_token", "https://test.salesforce.com");
        builder()
            .authenticate(auth)
            .build()
            .await
            .must_msg("failed to create test client")
    }

    #[tokio::test]
    async fn test_rest_handler_construction() {
        let client: ForceClient<MockAuthenticator> = create_test_client().await;
        let _handler = client.rest();
        // If we get here, handler was created successfully
    }

    #[tokio::test]
    async fn test_rest_handler_is_cloneable() {
        let client: ForceClient<MockAuthenticator> = create_test_client().await;
        let handler1 = client.rest();
        let handler2 = handler1.clone();

        // Both should produce the same base URL
        let url1: String = handler1.base_url().await.must();
        let url2: String = handler2.base_url().await.must();
        assert_eq!(url1, url2);
    }

    #[tokio::test]
    async fn test_base_url_construction() {
        let client: ForceClient<MockAuthenticator> = create_test_client().await;
        let handler = client.rest();

        let base_url: String = handler.base_url().await.must();
        assert!(base_url.starts_with("https://test.salesforce.com"));
        assert!(base_url.contains("/services/data/"));
        assert!(base_url.ends_with("v60.0")); // Default API version
    }

    #[tokio::test]
    async fn test_base_url_with_custom_api_version() {
        let auth = MockAuthenticator::new("test_token", "https://custom.salesforce.com");
        let config = ClientConfig {
            api_version: "v59.0".into(),
            ..Default::default()
        };
        let client = builder()
            .authenticate(auth)
            .config(config)
            .build()
            .await
            .must();

        let handler = client.rest();
        let base_url = handler.base_url().await.must();

        assert_eq!(
            base_url,
            "https://custom.salesforce.com/services/data/v59.0"
        );
    }

    #[tokio::test]
    async fn test_base_url_with_different_instance() {
        let auth = MockAuthenticator::new("token", "https://na139.salesforce.com");
        let client = builder().authenticate(auth).build().await.must();

        let handler = client.rest();
        let base_url = handler.base_url().await.must();

        assert!(base_url.starts_with("https://na139.salesforce.com"));
    }

    #[tokio::test]
    async fn test_rest_handler_shares_client_config() {
        let auth = MockAuthenticator::new("token", "https://shared.salesforce.com");
        let config = ClientConfig {
            api_version: "v58.0".into(),
            ..Default::default()
        };
        let client = builder()
            .authenticate(auth)
            .config(config)
            .build()
            .await
            .must();

        let handler = client.rest();
        let base_url = handler.base_url().await.must();

        // Verify the handler uses the same config as the client
        assert!(base_url.contains("shared.salesforce.com"));
        assert!(base_url.contains("v58.0"));
    }

    #[tokio::test]
    async fn test_multiple_handlers_from_same_client() {
        let client: ForceClient<MockAuthenticator> = create_test_client().await;

        let handler1 = client.rest();
        let handler2 = client.rest();

        // Both should have the same base URL
        let url1: String = handler1.base_url().await.must();
        let url2: String = handler2.base_url().await.must();
        assert_eq!(url1, url2);
    }

    #[tokio::test]
    async fn test_handler_debug_impl() {
        let client: ForceClient<MockAuthenticator> = create_test_client().await;
        let handler = client.rest();

        let debug_str = format!("{:?}", handler);
        assert!(!debug_str.is_empty());
    }
}