netbox 0.3.3

ergonomic rust client for NetBox 4.x REST API
Documentation
//! plugin endpoints, including netbox-branching resources.
//!
//! basic usage:
//! ```no_run
//! # use netbox::{Client, ClientConfig};
//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
//! # let client = Client::new(ClientConfig::new("https://netbox.example.com", "token"))?;
//! let branches = client.plugins().branches().list(None).await?;
//! println!("{}", branches.count);
//! # Ok(())
//! # }
//! ```

use crate::Client;
use crate::error::Result;
use crate::resource::Resource;

// Branch types come from the netbox-branching plugin and are not present in
// the core NetBox OpenAPI schema, so they are defined locally here rather
// than imported from generated code.

/// branch model (netbox-branching plugin).
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)]
pub struct Branch {
    /// branch id.
    pub id: Option<u64>,
    /// additional fields returned by the api.
    #[serde(flatten)]
    pub extra: std::collections::HashMap<String, serde_json::Value>,
}

/// branch event model (netbox-branching plugin).
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)]
pub struct BranchEvent {
    /// event id.
    pub id: Option<u64>,
    /// additional fields returned by the api.
    #[serde(flatten)]
    pub extra: std::collections::HashMap<String, serde_json::Value>,
}

/// change diff model (netbox-branching plugin).
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)]
pub struct ChangeDiff {
    /// diff id.
    pub id: Option<u64>,
    /// additional fields returned by the api.
    #[serde(flatten)]
    pub extra: std::collections::HashMap<String, serde_json::Value>,
}

/// commit request body (netbox-branching plugin).
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)]
pub struct CommitRequest {
    /// whether to commit the branch.
    pub commit: Option<bool>,
}

/// job model.
pub type Job = crate::models::Job;

/// writable branch request (netbox-branching plugin).
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)]
pub struct WritableBranchRequest {
    /// branch fields to write.
    #[serde(flatten)]
    pub extra: std::collections::HashMap<String, serde_json::Value>,
}

/// patched writable branch request (netbox-branching plugin).
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)]
pub struct PatchedWritableBranchRequest {
    /// branch fields to patch.
    #[serde(flatten)]
    pub extra: std::collections::HashMap<String, serde_json::Value>,
}

/// resource for branch events.
pub type BranchEventsApi = Resource<BranchEvent>;
/// resource for branches.
pub type BranchesApi = Resource<Branch>;
/// resource for changes.
pub type ChangesApi = Resource<ChangeDiff>;

/// api for plugin endpoints.
#[derive(Clone)]
pub struct PluginsApi {
    client: Client,
}

impl PluginsApi {
    pub(crate) fn new(client: Client) -> Self {
        Self { client }
    }

    /// returns the branch events resource.
    pub fn branch_events(&self) -> BranchEventsApi {
        Resource::new(self.client.clone(), "plugins/branching/branch-events/")
    }

    /// returns the branches resource.
    pub fn branches(&self) -> BranchesApi {
        Resource::new(self.client.clone(), "plugins/branching/branches/")
    }

    /// returns the changes resource.
    pub fn changes(&self) -> ChangesApi {
        Resource::new(self.client.clone(), "plugins/branching/changes/")
    }

    /// merge a branch (enqueue job).
    pub async fn merge_branch(&self, id: u64, body: &CommitRequest) -> Result<Job> {
        self.client
            .post(&format!("plugins/branching/branches/{}/merge/", id), body)
            .await
    }

    /// revert a branch (enqueue job).
    pub async fn revert_branch(&self, id: u64, body: &CommitRequest) -> Result<Job> {
        self.client
            .post(&format!("plugins/branching/branches/{}/revert/", id), body)
            .await
    }

    /// sync a branch (enqueue job).
    pub async fn sync_branch(&self, id: u64, body: &CommitRequest) -> Result<Job> {
        self.client
            .post(&format!("plugins/branching/branches/{}/sync/", id), body)
            .await
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::ClientConfig;
    use httpmock::{Method::POST, MockServer};

    fn test_client(base_url: &str) -> Client {
        let config = ClientConfig::new(base_url, "token").with_max_retries(0);
        Client::new(config).unwrap()
    }

    fn assert_path<T>(resource: Resource<T>, expected: &str)
    where
        T: serde::de::DeserializeOwned,
    {
        let paginator = resource.paginate(None).unwrap();
        assert_eq!(paginator.next_url(), Some(expected));
    }

    #[test]
    fn plugins_accessors_return_expected_paths() {
        let api = PluginsApi::new(test_client("https://netbox.example.com"));

        assert_path(api.branch_events(), "plugins/branching/branch-events/");
        assert_path(api.branches(), "plugins/branching/branches/");
        assert_path(api.changes(), "plugins/branching/changes/");
    }

    #[cfg_attr(miri, ignore)]
    #[tokio::test]
    async fn branch_actions_use_expected_paths() {
        let server = MockServer::start();
        let api = PluginsApi::new(test_client(&server.base_url()));

        let job_response = serde_json::json!({
            "id": 1,
            "url": "http://example.com/api/extras/jobs/1/",
            "display_url": "http://example.com/extras/jobs/1/",
            "display": "job",
            "object_type": "plugins.branch",
            "object_id": null,
            "name": "job",
            "status": {"value": "pending", "label": "Pending"},
            "created": "2024-01-01T00:00:00Z",
            "scheduled": null,
            "interval": null,
            "started": null,
            "completed": null,
            "user": {
                "id": 1,
                "url": "http://example.com/api/users/users/1/",
                "display": "admin",
                "username": "admin"
            },
            "data": null,
            "error": "",
            "job_id": "00000000-0000-0000-0000-000000000000",
            "log_entries": []
        });

        server.mock(|when, then| {
            when.method(POST)
                .path("/api/plugins/branching/branches/1/merge/");
            then.status(200).json_body(job_response.clone());
        });

        server.mock(|when, then| {
            when.method(POST)
                .path("/api/plugins/branching/branches/1/revert/");
            then.status(200).json_body(job_response.clone());
        });

        server.mock(|when, then| {
            when.method(POST)
                .path("/api/plugins/branching/branches/1/sync/");
            then.status(200).json_body(job_response.clone());
        });

        let commit = CommitRequest { commit: Some(true) };

        api.merge_branch(1u64, &commit).await.unwrap();
        api.revert_branch(1u64, &commit).await.unwrap();
        api.sync_branch(1u64, &commit).await.unwrap();
    }
}