wakapi 0.3.1

Wakatime API client
Documentation
//! Projects endpoint.
//!
//! - Ref: <https://wakatime.com/developers#projects>
//! - Last checked : 2026-05-15
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};

use crate::{WakapiClient, WakapiError};

use super::commit::RepositoryDetails;

/// Request parameters for the Projects endpoint.
///
/// Ref: <https://wakatime.com/developers#projects>
#[derive(Serialize, Default)]
pub struct ProjectsParams {
    /// optional - Filter projects by a search term
    q: Option<String>,
    /// optional - Page number of projects, not in documentation
    page: Option<usize>,
}

impl ProjectsParams {
    /// Create a new ProjectsParams with default values (no search term).
    pub fn new() -> ProjectsParams {
        ProjectsParams {
            q: None,
            page: None,
        }
    }

    /// Set the search term parameter for the request.
    pub fn q(mut self, q: &str) -> ProjectsParams {
        self.q = Some(q.to_string());
        self
    }

    /// Set the page parameter for the request.
    /// This will return the projects for the given page number.
    pub fn page(mut self, page: usize) -> ProjectsParams {
        self.page = Some(page);
        self
    }
}

/// Projects endpoint.
///
/// Ref: <https://wakatime.com/developers#projects>
#[derive(Deserialize, Debug)]
pub struct Projects {
    /// list of projects
    pub data: Vec<ProjectsData>,
    /// total number of projects
    pub total: usize,
    /// total number of pages
    pub total_pages: usize,
    /// current page number
    pub page: usize,
    /// previous page number
    pub prev_page: Option<usize>,
    /// next page number
    pub next_page: Option<usize>,
}

#[derive(Deserialize, Debug)]
pub struct ProjectsData {
    /// unique project id
    pub id: String,
    /// project name
    pub name: String,
    /// associated repository if connected
    pub repository: Option<RepositoryDetails>,
    /// associated project badge if enabled
    pub badge: Option<ProjectBadge>,
    /// custom project color as hex string, or null if using default color
    pub color: Option<String>,
    /// clients associated with this project
    pub clients: Vec<ProjectClients>,
    /// whether this project has a shareable url defined
    pub has_public_url: bool,
    /// time when project last received code stats as human readable string
    pub human_readable_last_heartbeat_at: String,
    /// time when project last received code stats in ISO 8601 format
    pub last_heartbeat_at: DateTime<Utc>,
    /// time when project first received code stats as human readable string; currently only set for users who signed up after 2024-02-05T00:00:00Z UTC
    pub human_readable_first_heartbeat_at: Option<String>,
    /// time when project first received code stats in ISO 8601 format; currently only set for users who signed up after 2024-02-05T00:00:00Z UTC
    pub first_heartbeat_at: Option<String>,
    /// url of this project relative to wakatime.com
    pub url: String,
    /// project name url entity encoded
    pub urlencoded_name: String,
    /// time when project was created in ISO 8601 format
    pub created_at: DateTime<Utc>,
}

#[derive(Deserialize, Debug)]
pub struct ProjectBadge {
    /// badge color
    pub color: String,
    /// badge unique id
    pub id: String,
    /// badge left text
    pub left_text: String,
    /// badge link
    pub link: String,
    /// badge project id
    pub project_id: String,
    /// badge snippets
    pub snippets: Vec<BadgeSnippet>,
    /// badge title
    pub title: String,
    /// badge url
    pub url: String,
}

#[derive(Deserialize, Debug)]
pub struct BadgeSnippet {
    /// snippet content
    pub content: String,
    /// snippet name
    pub name: String,
}

#[derive(Deserialize, Debug)]
pub struct ProjectClients {
    /// unique client id
    pub id: String,
    /// client name
    pub name: String,
    /// client rate
    pub rate: f64,
    /// client timeout
    pub timeout: usize,
}

impl Projects {
    #[cfg(feature = "blocking")]
    /// Fetch the projects for the current user.
    pub fn fetch(client: &WakapiClient, params: ProjectsParams) -> Result<Self, WakapiError> {
        let url = client.build_url(
            "/api/v1/users/current/projects",
            Some(serde_url_params::to_string(&params)?),
        );

        let response = reqwest::blocking::Client::new()
            .get(&url)
            .header("Authorization", client.get_auth_header())
            .send()?;
        if response.status().is_success() {
            let body = response.json::<Projects>()?;
            Ok(body)
        } else {
            let error = response.json::<crate::error::ErrorMessage>()?;
            Err(WakapiError::ResponseError(error))
        }
    }

    #[cfg(not(feature = "blocking"))]
    /// Fetch the projects for the current user.
    pub async fn fetch(client: &WakapiClient, params: ProjectsParams) -> Result<Self, WakapiError> {
        let url = client.build_url(
            "/api/v1/users/current/projects",
            Some(serde_url_params::to_string(&params)?),
        );

        let response = reqwest::Client::new()
            .get(&url)
            .header("Authorization", client.get_auth_header())
            .send()
            .await?;
        if response.status().is_success() {
            let body = response.json::<Projects>().await?;
            Ok(body)
        } else {
            let error = response.json::<crate::error::ErrorMessage>().await?;
            Err(WakapiError::ResponseError(error))
        }
    }
}