Skip to main content

blz_cli/args/
pagination.rs

1//! Pagination argument groups for CLI commands.
2//!
3//! This module provides reusable pagination arguments that can be composed
4//! across multiple commands using clap's `#[command(flatten)]` attribute.
5//!
6//! # Examples
7//!
8//! ```
9//! use blz_cli::args::PaginationArgs;
10//! use clap::Parser;
11//!
12//! #[derive(Parser)]
13//! struct MyCommand {
14//!     #[command(flatten)]
15//!     pagination: PaginationArgs,
16//! }
17//! ```
18
19use clap::Args;
20use serde::{Deserialize, Serialize};
21
22/// Validates that a limit value is at least 1.
23fn validate_limit(s: &str) -> Result<usize, String> {
24    let value: usize = s
25        .parse()
26        .map_err(|_| format!("'{s}' is not a valid number"))?;
27    if value == 0 {
28        Err("limit must be at least 1".to_string())
29    } else {
30        Ok(value)
31    }
32}
33
34/// Shared pagination arguments for commands that support limiting results.
35///
36/// This group provides consistent limit/offset behavior across commands
37/// that return multiple results (search, list, history, etc.).
38///
39/// # Usage
40///
41/// Flatten into command structs:
42///
43/// ```ignore
44/// #[derive(Args)]
45/// struct SearchArgs {
46///     #[command(flatten)]
47///     pagination: PaginationArgs,
48///     // ... other args
49/// }
50/// ```
51///
52/// # Examples
53///
54/// ```bash
55/// blz search "async" --limit 10
56/// blz search "async" --limit 10 --offset 20
57/// blz list --limit 5
58/// ```
59#[derive(Args, Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
60pub struct PaginationArgs {
61    /// Maximum number of results to return
62    ///
63    /// Limits the output to the specified count. Must be at least 1.
64    #[arg(
65        short = 'l',
66        long,
67        value_name = "COUNT",
68        value_parser = validate_limit,
69        display_order = 50
70    )]
71    pub limit: Option<usize>,
72
73    /// Number of results to skip before returning
74    ///
75    /// Used for paginating through large result sets.
76    #[arg(long, value_name = "COUNT", display_order = 51)]
77    pub offset: Option<usize>,
78}
79
80impl PaginationArgs {
81    /// Create pagination args with a specific limit.
82    #[must_use]
83    pub const fn with_limit(limit: usize) -> Self {
84        Self {
85            limit: Some(limit),
86            offset: None,
87        }
88    }
89
90    /// Create pagination args with limit and offset.
91    #[must_use]
92    pub const fn with_limit_and_offset(limit: usize, offset: usize) -> Self {
93        Self {
94            limit: Some(limit),
95            offset: Some(offset),
96        }
97    }
98
99    /// Get the effective limit, falling back to a default if not specified.
100    #[must_use]
101    pub const fn limit_or(&self, default: usize) -> usize {
102        match self.limit {
103            Some(limit) => limit,
104            None => default,
105        }
106    }
107
108    /// Get the effective offset, defaulting to 0 if not specified.
109    #[must_use]
110    pub const fn offset_or_default(&self) -> usize {
111        match self.offset {
112            Some(offset) => offset,
113            None => 0,
114        }
115    }
116}
117
118#[cfg(test)]
119#[allow(clippy::unwrap_used)]
120mod tests {
121    use super::*;
122
123    #[test]
124    fn test_validate_limit_valid() {
125        assert_eq!(validate_limit("1"), Ok(1));
126        assert_eq!(validate_limit("100"), Ok(100));
127        assert_eq!(validate_limit("999999"), Ok(999_999));
128    }
129
130    #[test]
131    fn test_validate_limit_zero() {
132        assert!(validate_limit("0").is_err());
133        assert!(validate_limit("0").unwrap_err().contains("at least 1"));
134    }
135
136    #[test]
137    fn test_validate_limit_invalid() {
138        assert!(validate_limit("abc").is_err());
139        assert!(validate_limit("-1").is_err());
140        assert!(validate_limit("1.5").is_err());
141    }
142
143    #[test]
144    fn test_default() {
145        let args = PaginationArgs::default();
146        assert_eq!(args.limit, None);
147        assert_eq!(args.offset, None);
148    }
149
150    #[test]
151    fn test_with_limit() {
152        let args = PaginationArgs::with_limit(10);
153        assert_eq!(args.limit, Some(10));
154        assert_eq!(args.offset, None);
155    }
156
157    #[test]
158    fn test_with_limit_and_offset() {
159        let args = PaginationArgs::with_limit_and_offset(10, 20);
160        assert_eq!(args.limit, Some(10));
161        assert_eq!(args.offset, Some(20));
162    }
163
164    #[test]
165    fn test_limit_or() {
166        let args = PaginationArgs::default();
167        assert_eq!(args.limit_or(50), 50);
168
169        let args = PaginationArgs::with_limit(10);
170        assert_eq!(args.limit_or(50), 10);
171    }
172
173    #[test]
174    fn test_offset_or_default() {
175        let args = PaginationArgs::default();
176        assert_eq!(args.offset_or_default(), 0);
177
178        let args = PaginationArgs::with_limit_and_offset(10, 20);
179        assert_eq!(args.offset_or_default(), 20);
180    }
181}