faucet_stream/pagination/
mod.rs1pub mod cursor;
4pub mod link_header;
5pub mod next_link_body;
6pub mod offset;
7pub mod page;
8
9use crate::error::FaucetError;
10use reqwest::header::HeaderMap;
11use serde_json::Value;
12use std::collections::HashMap;
13
14#[derive(Debug, Clone)]
16pub enum PaginationStyle {
17 None,
18 Cursor {
19 next_token_path: String,
20 param_name: String,
21 },
22 LinkHeader,
23 NextLinkInBody {
28 next_link_path: String,
29 },
30 PageNumber {
31 param_name: String,
32 start_page: usize,
33 page_size: Option<usize>,
34 page_size_param: Option<String>,
35 },
36 Offset {
37 offset_param: String,
38 limit_param: String,
39 limit: usize,
40 total_path: Option<String>,
41 },
42}
43
44#[derive(Debug, Default)]
46pub struct PaginationState {
47 pub page: usize,
48 pub next_token: Option<String>,
49 pub offset: usize,
50 pub next_link: Option<String>,
51 #[doc(hidden)]
55 pub previous_token: Option<String>,
56}
57
58impl PaginationStyle {
59 pub fn apply_params(&self, params: &mut HashMap<String, String>, state: &PaginationState) {
60 match self {
61 PaginationStyle::None => {}
62 PaginationStyle::Cursor { param_name, .. } => {
63 cursor::apply_params(params, param_name, &state.next_token);
64 }
65 PaginationStyle::LinkHeader => {}
66 PaginationStyle::NextLinkInBody { .. } => {}
67 PaginationStyle::PageNumber {
68 param_name,
69 start_page,
70 page_size,
71 page_size_param,
72 } => {
73 page::apply_params(
74 params,
75 param_name,
76 *start_page,
77 state.page,
78 *page_size,
79 page_size_param.as_deref(),
80 );
81 }
82 PaginationStyle::Offset {
83 offset_param,
84 limit_param,
85 limit,
86 ..
87 } => {
88 offset::apply_params(params, offset_param, limit_param, state.offset, *limit);
89 }
90 }
91 }
92
93 pub fn advance(
100 &self,
101 body: &Value,
102 headers: &HeaderMap,
103 state: &mut PaginationState,
104 record_count: usize,
105 ) -> Result<bool, FaucetError> {
106 match self {
107 PaginationStyle::None => Ok(false),
108 PaginationStyle::Cursor {
109 next_token_path, ..
110 } => {
111 let has_next = cursor::advance(body, next_token_path, &mut state.next_token)?;
112 if has_next {
113 if state.next_token == state.previous_token {
114 tracing::warn!(
115 "pagination loop detected: cursor {:?} repeated — stopping",
116 state.next_token
117 );
118 return Ok(false);
119 }
120 state.previous_token = state.next_token.clone();
121 }
122 Ok(has_next)
123 }
124 PaginationStyle::LinkHeader => match link_header::extract_next_link(headers) {
125 Some(link) => {
126 if Some(&link) == state.previous_token.as_ref() {
127 tracing::warn!(
128 "pagination loop detected: link {link:?} repeated — stopping"
129 );
130 state.next_link = None;
131 return Ok(false);
132 }
133 state.previous_token = Some(link.clone());
134 state.next_link = Some(link);
135 Ok(true)
136 }
137 None => {
138 state.next_link = None;
139 Ok(false)
140 }
141 },
142 PaginationStyle::NextLinkInBody { next_link_path } => {
143 let has_next = next_link_body::advance(body, next_link_path, &mut state.next_link)?;
144 if has_next {
145 if state.next_link == state.previous_token {
146 tracing::warn!(
147 "pagination loop detected: next_link {:?} repeated — stopping",
148 state.next_link
149 );
150 return Ok(false);
151 }
152 state.previous_token = state.next_link.clone();
153 }
154 Ok(has_next)
155 }
156 PaginationStyle::PageNumber { .. } => {
157 state.page += 1;
158 Ok(record_count > 0)
159 }
160 PaginationStyle::Offset {
161 limit, total_path, ..
162 } => offset::advance(
163 body,
164 &mut state.offset,
165 record_count,
166 *limit,
167 total_path.as_deref(),
168 ),
169 }
170 }
171}