wakapi 0.3.1

Wakatime API client
Documentation
//! Durations endpoint
//!
//! - Ref: <https://wakatime.com/developers#durations>
//! - Last checked : 2026-05-15

use std::collections::HashMap;

use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};

use crate::{ErrorMessage, WakapiClient, WakapiError};

/// Request parameters for the Durations endpoint.
#[derive(Serialize)]
pub struct DurationsParams {
    /// required - Requested day; Durations will be returned from 12am until 11:59pm in user's timezone for this day.
    date: String,
    /// optional - Only show durations for this project.
    project: Option<String>,
    /// optional - Only show durations for these branches; comma separated list of branch names.
    branches: Option<Vec<String>>,
    /// The keystroke timeout preference used when joining heartbeats into durations. Defaults the the user's keystroke timeout value.
    timeout: Option<usize>,
    /// optional - The writes_only preference. Defaults to the user's writes_only setting.
    writes_only: Option<bool>,
    /// optional - The timezone for given date. Defaults to the user's timezone.
    timezone: Option<String>,
    /// optional - Optional primary key to use when slicing durations. Defaults to “entity”. Can be “entity”, “language”, “dependencies”, “os”, “editor”, “category”, or “machine”.
    slice_by: Option<String>,
}

impl Default for DurationsParams {
    /// Create a new DurationsParams with default values (today's date).
    fn default() -> Self {
        Self::new(None)
    }
}

impl DurationsParams {
    /// Create a new DurationsParams.
    /// If date is None, the default date is today.
    pub fn new(date: Option<String>) -> DurationsParams {
        DurationsParams {
            date: date.unwrap_or_else(get_default_date),
            project: None,
            branches: None,
            timeout: None,
            writes_only: None,
            timezone: None,
            slice_by: None,
        }
    }

    /// Set the project parameter for the request.
    /// Optional : Only show durations for this project.
    pub fn project(mut self, project: &str) -> DurationsParams {
        self.project = Some(project.to_string());
        self
    }

    /// Set the branches parameter for the request.
    /// Optional : Only show durations for these branches; comma separated list of branch names.
    pub fn branches(mut self, branches: Vec<String>) -> DurationsParams {
        self.branches = Some(branches);
        self
    }

    /// Set the timeout parameter for the request.
    /// Optional : The keystroke timeout preference used when joining heartbeats into durations. Defaults the the user's keystroke timeout value.
    pub fn timeout(mut self, timeout: usize) -> DurationsParams {
        self.timeout = Some(timeout);
        self
    }

    /// Set the writes_only parameter for the request.
    /// Optional : The writes_only preference. Defaults to the user's writes_only setting.
    pub fn writes_only(mut self, writes_only: bool) -> DurationsParams {
        self.writes_only = Some(writes_only);
        self
    }

    /// Set the timezone parameter for the request.
    /// Optional : The timezone for given date. Defaults to the user's timezone.
    pub fn timezone(mut self, timezone: &str) -> DurationsParams {
        self.timezone = Some(timezone.to_string());
        self
    }

    /// Set the slice_by parameter for the request.
    /// Optional : Optional primary key to use when slicing durations. Defaults to “entity”. Can be “entity”, “language”, “dependencies”, “os”, “editor”, “category”, or “machine”.
    pub fn slice_by(mut self, slice_by: &str) -> DurationsParams {
        self.slice_by = Some(slice_by.to_string());
        self
    }
}

/// Return today's date as a string in the format "YYYY-MM-DD".
fn get_default_date() -> String {
    chrono::Local::now().format("%Y-%m-%d").to_string()
}

/// Durations endpoint
///
/// Ref : <https://wakatime.com/developers#durations>

#[derive(Deserialize, Debug)]
pub struct Durations {
    /// Durations data
    pub data: Vec<DurationsData>,
    /// Start of time range as ISO 8601 UTC date string
    pub start: DateTime<Utc>,
    /// End of time range as ISO 8601 UTC date string
    pub end: DateTime<Utc>,
    /// Timezone used for this request in Olson Country/Region format
    pub timezone: String,
}

#[derive(Deserialize, Debug)]
pub struct DurationsData {
    /// Project name
    pub project: String,
    /// start of this duration as UNIX epoch; numbers after decimal point are fractions of a second
    pub time: f64,
    /// length of time of this duration in seconds
    pub duration: f64,
    /// number of lines added by GenAI since last duration
    pub ai_additions: Option<i64>,
    /// number of lines removed by GenAI since last duration
    pub ai_deletions: Option<i64>,
    /// number of lines added by old-school typing since last duration
    pub human_additions: Option<i64>,
    /// number of lines removed by old-school typing since last duration
    pub human_deletions: Option<i64>,
    /// estimated USD cost per GenAI agent during this duration
    pub ai_agent_costs: Option<HashMap<String, f64>>,
    /// number of AI user input tokens used since last duration
    pub ai_input_tokens: Option<i64>,
    /// number of AI output tokens used since last duration
    pub ai_output_tokens: Option<i64>,
    /// number of user prompt characters typed to AI since last duration
    pub ai_prompt_length: Option<i64>,
}

impl Durations {
    #[cfg(feature = "blocking")]
    pub fn fetch(client: &WakapiClient, params: DurationsParams) -> Result<Durations, WakapiError> {
        let url = client.build_url(
            "/api/v1/users/:user/durations",
            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 data = response.json::<Durations>()?;
            Ok(data)
        } else {
            Err(WakapiError::ResponseError(
                response.json().unwrap_or(ErrorMessage::unknown()),
            ))
        }
    }

    #[cfg(not(feature = "blocking"))]
    pub async fn fetch(
        client: &WakapiClient,
        params: DurationsParams,
    ) -> Result<Durations, WakapiError> {
        let url = client.build_url(
            "/api/v1/users/:user/durations",
            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 data = response.json::<Durations>().await?;
            Ok(data)
        } else {
            Err(WakapiError::ResponseError(
                response.json().await.unwrap_or(ErrorMessage::unknown()),
            ))
        }
    }
}