Skip to main content

futu_core/
qot_page_bounds.rs

1//! Pure QOT pagination and max-count validation rules.
2//!
3//! Historical context: v1.4.106 found inconsistent pagination validation across
4//! QOT analysis/reference endpoints. Keep the shared contract in `futu-core` so
5//! REST/MCP/gateway callers do not need the legacy `futu-qot` SDK facade for a
6//! pure input rule.
7//!
8//! C++ reference points:
9//! - `NNBiz/Src/Qot/StockScreener/NNBiz_Qot_StockScreener.cpp` validates
10//!   CMD9010 `begin` / `num`.
11//! - `NNBiz_Qot_Warrant.cpp` validates CMD6513 `data_from` /
12//!   `data_max_count`.
13
14use std::fmt;
15
16/// Validated pagination parameters.
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub struct PageBounds {
19    pub begin: i32,
20    pub num: i32,
21}
22
23/// Pagination validation failure.
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub struct PageBoundsError {
26    /// Endpoint label used in user-facing diagnostics, for example `warrant`.
27    pub endpoint: String,
28    pub reason: PageBoundsErrorReason,
29}
30
31#[derive(Debug, Clone, PartialEq, Eq)]
32pub enum PageBoundsErrorReason {
33    BeginNegative {
34        begin: i32,
35    },
36    NumOutOfRange {
37        num: i32,
38        min: i32,
39        max: i32,
40    },
41    /// Negative max_count is invalid; None / 0 means unbounded.
42    MaxCountNegative {
43        max_count: i32,
44    },
45    /// Positive max_count exceeds the endpoint cap.
46    MaxCountTooLarge {
47        max_count: i32,
48        max_allowed: i32,
49    },
50}
51
52impl fmt::Display for PageBoundsError {
53    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54        match &self.reason {
55            PageBoundsErrorReason::BeginNegative { begin } => write!(
56                f,
57                "{}: begin={} 非法 (必须 >= 0)。begin 是分页起始 index, 0 表示第一页",
58                self.endpoint, begin
59            ),
60            PageBoundsErrorReason::NumOutOfRange { num, min, max } => write!(
61                f,
62                "{}: num={} 非法 (合法范围 [{}, {}])。num 必须显式在范围内, \
63                 daemon 不再静默 clamp。如需更大 batch 请分页发多次。",
64                self.endpoint, num, min, max
65            ),
66            PageBoundsErrorReason::MaxCountNegative { max_count } => write!(
67                f,
68                "{}: max_count={} 负数非法。省略 (None) 或 0 = 不限制; \
69                 否则必须正整数。",
70                self.endpoint, max_count
71            ),
72            PageBoundsErrorReason::MaxCountTooLarge {
73                max_count,
74                max_allowed,
75            } => write!(
76                f,
77                "{}: max_count={} 超上限 (合法上限 {})。\
78                 更大 range 请分段查询或省略以使用默认值。",
79                self.endpoint, max_count, max_allowed
80            ),
81        }
82    }
83}
84
85impl std::error::Error for PageBoundsError {}
86
87/// Validate `(begin, num)` pagination parameters.
88///
89/// Rules:
90/// - `begin >= 0`
91/// - `0 <= num <= max_num`
92///
93/// `num = 0` is intentionally valid and represents an empty page request, which
94/// matches the audited C++/backend behavior for the affected endpoints.
95pub fn validate_begin_num(
96    begin: i32,
97    num: i32,
98    max_num: i32,
99    endpoint: &str,
100) -> Result<PageBounds, PageBoundsError> {
101    if begin < 0 {
102        return Err(PageBoundsError {
103            endpoint: endpoint.to_string(),
104            reason: PageBoundsErrorReason::BeginNegative { begin },
105        });
106    }
107    if num < 0 || num > max_num {
108        return Err(PageBoundsError {
109            endpoint: endpoint.to_string(),
110            reason: PageBoundsErrorReason::NumOutOfRange {
111                num,
112                min: 0,
113                max: max_num,
114            },
115        });
116    }
117    Ok(PageBounds { begin, num })
118}
119
120/// Validate optional `max_count` parameters used by history-kline-style calls.
121///
122/// Semantics:
123/// - `None` and `Some(0)` mean unbounded/default and normalize to `None`;
124/// - positive values must be within `max_allowed`;
125/// - negative values are rejected loudly.
126pub fn validate_optional_max_count(
127    max_count: Option<i32>,
128    max_allowed: i32,
129    endpoint: &str,
130) -> Result<Option<i32>, PageBoundsError> {
131    match max_count {
132        None | Some(0) => Ok(None),
133        Some(n) if n < 0 => Err(PageBoundsError {
134            endpoint: endpoint.to_string(),
135            reason: PageBoundsErrorReason::MaxCountNegative { max_count: n },
136        }),
137        Some(n) if n > max_allowed => Err(PageBoundsError {
138            endpoint: endpoint.to_string(),
139            reason: PageBoundsErrorReason::MaxCountTooLarge {
140                max_count: n,
141                max_allowed,
142            },
143        }),
144        Some(n) => Ok(Some(n)),
145    }
146}