@@ -130,35 +130,364 @@ impl QueryResponseBuilder {
130130 }
131131
132132 pub fn build ( mut self ) -> trc:: Result < QueryResponse > {
133- if !self . has_anchor || self . anchor_found {
134- if !self . has_anchor && self . requested_position >= 0 {
135- self . response . position = if self . position == 0 {
136- self . requested_position
137- } else {
138- 0
139- } ;
140- } else if self . position >= 0 {
141- self . response . position = self . position ;
133+ if self . has_anchor && !self . anchor_found {
134+ return Err ( trc:: JmapEvent :: AnchorNotFound . into_err ( ) ) ;
135+ }
136+
137+ if !self . has_anchor && self . requested_position >= 0 {
138+ self . response . position = if self . position == 0 {
139+ self . requested_position
142140 } else {
143- let position = self . position . unsigned_abs ( ) as usize ;
144- let start_offset = if position < self . response . ids . len ( ) {
145- self . response . ids . len ( ) - position
146- } else {
147- 0
148- } ;
149- self . response . position = start_offset as i32 ;
150- let end_offset = if self . limit > 0 {
151- std:: cmp:: min ( start_offset + self . limit , self . response . ids . len ( ) )
152- } else {
153- self . response . ids . len ( )
154- } ;
141+ 0
142+ } ;
143+ } else if self . position >= 0 {
144+ self . response . position = self . position ;
145+ } else {
146+ let position = self . position . unsigned_abs ( ) as usize ;
147+ let start_offset = if position < self . response . ids . len ( ) {
148+ self . response . ids . len ( ) - position
149+ } else {
150+ 0
151+ } ;
152+ self . response . position = start_offset as i32 ;
153+ let end_offset = if self . limit > 0 {
154+ std:: cmp:: min ( start_offset + self . limit , self . response . ids . len ( ) )
155+ } else {
156+ self . response . ids . len ( )
157+ } ;
158+
159+ self . response . ids = self . response . ids [ start_offset..end_offset] . to_vec ( )
160+ }
161+
162+ Ok ( self . response )
163+ }
164+ }
165+
166+ #[ cfg( test) ]
167+ mod tests {
168+ use super :: * ;
169+ use jmap_proto:: types:: state:: State ;
170+ use jmap_tools:: Null ;
171+ use types:: id:: Id ;
172+
173+ #[ derive( Debug , Clone , Copy ) ]
174+ struct TestJMAPObject ;
175+
176+ impl JmapObject for TestJMAPObject {
177+ type Property = Null ;
178+ type Element = Null ;
179+ type Id = Null ;
180+
181+ type Filter = ( ) ;
182+ type Comparator = ( ) ;
183+
184+ type GetArguments = ( ) ;
185+ type SetArguments < ' de > = ( ) ;
186+ type QueryArguments = ( ) ;
187+ type CopyArguments = ( ) ;
188+ type ParseArguments = ( ) ;
155189
156- self . response . ids = self . response . ids [ start_offset..end_offset] . to_vec ( )
190+ const ID_PROPERTY : Self :: Property = Null ;
191+ }
192+
193+ #[ test]
194+ fn test_pagination_with_zero_position ( ) {
195+ let range: std:: ops:: Range < u32 > = 0 ..10 ;
196+ let mut builder = QueryResponseBuilder :: new (
197+ range. len ( ) ,
198+ 100 ,
199+ State :: Initial ,
200+ & QueryRequest :: < TestJMAPObject > {
201+ position : Some ( 0 ) ,
202+ limit : Some ( 3 ) ,
203+ ..Default :: default ( )
204+ } ,
205+ ) ;
206+
207+ for i in range {
208+ if !builder. add_id ( Id :: from_parts ( 0 , i) ) {
209+ break ;
157210 }
211+ }
158212
159- Ok ( self . response )
160- } else {
161- Err ( trc:: JmapEvent :: AnchorNotFound . into_err ( ) )
213+ let result = builder. build ( ) . unwrap ( ) ;
214+
215+ assert_eq ! ( result. ids. len( ) , 3 , "Should collect 3 items" ) ;
216+ assert_eq ! ( result. ids[ 0 ] . document_id( ) , 0 , "First item should be 0" ) ;
217+ assert_eq ! ( result. ids[ 1 ] . document_id( ) , 1 , "Second item should be 1" ) ;
218+ assert_eq ! ( result. ids[ 2 ] . document_id( ) , 2 , "Third item should be 2" ) ;
219+ assert_eq ! ( result. position, 0 , "Position should be 0" ) ;
220+ }
221+
222+ #[ test]
223+ fn test_pagination_with_positive_position ( ) {
224+ let range: std:: ops:: Range < u32 > = 0 ..10 ;
225+ let mut builder = QueryResponseBuilder :: new (
226+ range. len ( ) ,
227+ 100 ,
228+ State :: Initial ,
229+ & QueryRequest :: < TestJMAPObject > {
230+ position : Some ( 5 ) ,
231+ limit : Some ( 3 ) ,
232+ ..Default :: default ( )
233+ } ,
234+ ) ;
235+
236+ for i in range {
237+ if !builder. add_id ( Id :: from_parts ( 0 , i) ) {
238+ break ;
239+ }
240+ }
241+
242+ let result = builder. build ( ) . unwrap ( ) ;
243+
244+ assert_eq ! ( result. ids. len( ) , 3 , "Should collect 3 items" ) ;
245+ assert_eq ! ( result. ids[ 0 ] . document_id( ) , 5 , "First item should be 5" ) ;
246+ assert_eq ! ( result. ids[ 1 ] . document_id( ) , 6 , "Second item should be 6" ) ;
247+ assert_eq ! ( result. ids[ 2 ] . document_id( ) , 7 , "Third item should be 7" ) ;
248+ assert_eq ! ( result. position, 5 , "Position should be 5" ) ;
249+ }
250+
251+ #[ test]
252+ fn test_pagination_negative_position ( ) {
253+ let range: std:: ops:: Range < u32 > = 0 ..30 ;
254+ let mut builder = QueryResponseBuilder :: new (
255+ range. len ( ) ,
256+ 100 ,
257+ State :: Initial ,
258+ & QueryRequest :: < TestJMAPObject > {
259+ position : Some ( -6 ) ,
260+ limit : Some ( 3 ) ,
261+ ..Default :: default ( )
262+ } ,
263+ ) ;
264+
265+ for i in range {
266+ if !builder. add_id ( Id :: from_parts ( 0 , i) ) {
267+ break ;
268+ }
269+ }
270+
271+ let result = builder. build ( ) . unwrap ( ) ;
272+
273+ assert_eq ! ( result. ids. len( ) , 3 , "Should collect 10 items" ) ;
274+ assert_eq ! ( result. ids[ 0 ] . document_id( ) , 24 , "First item should be 24" ) ;
275+ assert_eq ! ( result. ids[ 1 ] . document_id( ) , 25 , "Second item should be 25" ) ;
276+ assert_eq ! ( result. ids[ 2 ] . document_id( ) , 26 , "Third item should be 26" ) ;
277+ assert_eq ! ( result. position, 24 ) ;
278+ }
279+
280+ #[ test]
281+ fn test_pagination_with_position_partial_results ( ) {
282+ let range: std:: ops:: Range < u32 > = 0 ..5 ;
283+ let mut builder = QueryResponseBuilder :: new (
284+ range. len ( ) ,
285+ 100 ,
286+ State :: Initial ,
287+ & QueryRequest :: < TestJMAPObject > {
288+ position : Some ( 3 ) ,
289+ limit : Some ( 5 ) ,
290+ ..Default :: default ( )
291+ } ,
292+ ) ;
293+
294+ for i in range {
295+ if !builder. add_id ( Id :: from_parts ( 0 , i) ) {
296+ break ;
297+ }
298+ }
299+
300+ let result = builder. build ( ) . unwrap ( ) ;
301+
302+ assert_eq ! (
303+ result. ids. len( ) ,
304+ 2 ,
305+ "Should collect 2 items (not full limit)"
306+ ) ;
307+ assert_eq ! ( result. ids[ 0 ] . document_id( ) , 3 , "First item should be 3" ) ;
308+ assert_eq ! ( result. ids[ 1 ] . document_id( ) , 4 , "Second item should be 4" ) ;
309+ assert_eq ! ( result. position, 3 , " be 3" ) ;
310+ }
311+
312+ #[ test]
313+ fn test_pagination_with_zero_anchor_offset ( ) {
314+ let range: std:: ops:: Range < u32 > = 0 ..10 ;
315+ let mut builder = QueryResponseBuilder :: new (
316+ range. len ( ) ,
317+ 100 ,
318+ State :: Initial ,
319+ & QueryRequest :: < TestJMAPObject > {
320+ limit : Some ( 3 ) ,
321+ anchor : Some ( Id :: from_parts ( 0 , 5 ) ) ,
322+ anchor_offset : Some ( 0 ) ,
323+ ..Default :: default ( )
324+ } ,
325+ ) ;
326+
327+ for i in range {
328+ if !builder. add_id ( Id :: from_parts ( 0 , i) ) {
329+ break ;
330+ }
331+ }
332+
333+ let result = builder. build ( ) . unwrap ( ) ;
334+
335+ assert_eq ! ( result. ids. len( ) , 3 , "Should collect 3 items" ) ;
336+ assert_eq ! ( result. ids[ 0 ] . document_id( ) , 5 , "First item should be 5" ) ;
337+ assert_eq ! ( result. ids[ 1 ] . document_id( ) , 6 , "Second item should be 6" ) ;
338+ assert_eq ! ( result. ids[ 2 ] . document_id( ) , 7 , "Third item should be 7" ) ;
339+ assert_eq ! ( result. position, 5 , "Position should be 5" ) ;
340+ }
341+
342+ #[ test]
343+ fn test_pagination_with_negative_anchor_offset ( ) {
344+ let range: std:: ops:: Range < u32 > = 0 ..10 ;
345+ let mut builder = QueryResponseBuilder :: new (
346+ range. len ( ) ,
347+ 100 ,
348+ State :: Initial ,
349+ & QueryRequest :: < TestJMAPObject > {
350+ limit : Some ( 3 ) ,
351+ anchor : Some ( Id :: from_parts ( 0 , 5 ) ) ,
352+ anchor_offset : Some ( -2 ) ,
353+ ..Default :: default ( )
354+ } ,
355+ ) ;
356+
357+ for i in range {
358+ if !builder. add_id ( Id :: from_parts ( 0 , i) ) {
359+ break ;
360+ }
361+ }
362+
363+ let result = builder. build ( ) . unwrap ( ) ;
364+
365+ assert_eq ! ( result. ids. len( ) , 3 , "Should collect 3 items" ) ;
366+ assert_eq ! ( result. ids[ 0 ] . document_id( ) , 3 , "First item should be 3" ) ;
367+ assert_eq ! ( result. ids[ 1 ] . document_id( ) , 4 , "Second item should be 4" ) ;
368+ assert_eq ! ( result. ids[ 2 ] . document_id( ) , 5 , "Third item should be 5" ) ;
369+ assert_eq ! ( result. position, 3 , "Position should be 3" ) ;
370+ }
371+
372+ #[ test]
373+ fn test_pagination_with_negative_anchor_offset_more_than_limit ( ) {
374+ let range: std:: ops:: Range < u32 > = 0 ..10 ;
375+ let mut builder = QueryResponseBuilder :: new (
376+ range. len ( ) ,
377+ 100 ,
378+ State :: Initial ,
379+ & QueryRequest :: < TestJMAPObject > {
380+ limit : Some ( 3 ) ,
381+ anchor : Some ( Id :: from_parts ( 0 , 9 ) ) ,
382+ anchor_offset : Some ( -6 ) ,
383+ ..Default :: default ( )
384+ } ,
385+ ) ;
386+
387+ for i in range {
388+ if !builder. add_id ( Id :: from_parts ( 0 , i) ) {
389+ break ;
390+ }
391+ }
392+
393+ let result = builder. build ( ) . unwrap ( ) ;
394+
395+ assert_eq ! ( result. ids. len( ) , 3 , "Should collect 3 items" ) ;
396+ assert_eq ! ( result. ids[ 0 ] . document_id( ) , 3 , "First item should be 3" ) ;
397+ assert_eq ! ( result. ids[ 1 ] . document_id( ) , 4 , "Second item should be 4" ) ;
398+ assert_eq ! ( result. ids[ 2 ] . document_id( ) , 5 , "Third item should be 5" ) ;
399+ assert_eq ! ( result. position, 3 , "Position should be 3" ) ;
400+ }
401+
402+ #[ test]
403+ fn test_pagination_with_anchor_offset_1 ( ) {
404+ let range: std:: ops:: Range < u32 > = 0 ..10 ;
405+ let mut builder = QueryResponseBuilder :: new (
406+ range. len ( ) ,
407+ 100 ,
408+ State :: Initial ,
409+ & QueryRequest :: < TestJMAPObject > {
410+ limit : Some ( 3 ) ,
411+ anchor : Some ( Id :: from_parts ( 0 , 3 ) ) ,
412+ anchor_offset : Some ( 1 ) ,
413+ ..Default :: default ( )
414+ } ,
415+ ) ;
416+
417+ for i in range {
418+ if !builder. add_id ( Id :: from_parts ( 0 , i) ) {
419+ break ;
420+ }
421+ }
422+
423+ let result = builder. build ( ) . unwrap ( ) ;
424+
425+ assert_eq ! ( result. ids. len( ) , 3 , "Should collect 3 items" ) ;
426+ assert_eq ! ( result. ids[ 0 ] . document_id( ) , 4 , "First item should be 4" ) ;
427+ assert_eq ! ( result. ids[ 1 ] . document_id( ) , 5 , "Second item should be 5" ) ;
428+ assert_eq ! ( result. ids[ 2 ] . document_id( ) , 6 , "Third item should be 6" ) ;
429+ assert_eq ! ( result. position, 4 , "Position should be 4" ) ;
430+ }
431+
432+ #[ test]
433+ fn test_pagination_with_anchor_offset_2 ( ) {
434+ let range: std:: ops:: Range < u32 > = 0 ..10 ;
435+ let mut builder = QueryResponseBuilder :: new (
436+ range. len ( ) ,
437+ 100 ,
438+ State :: Initial ,
439+ & QueryRequest :: < TestJMAPObject > {
440+ limit : Some ( 3 ) ,
441+ anchor : Some ( Id :: from_parts ( 0 , 3 ) ) ,
442+ anchor_offset : Some ( 2 ) ,
443+ ..Default :: default ( )
444+ } ,
445+ ) ;
446+
447+ for i in range {
448+ if !builder. add_id ( Id :: from_parts ( 0 , i) ) {
449+ break ;
450+ }
451+ }
452+
453+ let result = builder. build ( ) . unwrap ( ) ;
454+
455+ assert_eq ! ( result. ids. len( ) , 3 , "Should collect 3 items" ) ;
456+ assert_eq ! ( result. ids[ 0 ] . document_id( ) , 5 , "First item should be 5" ) ;
457+ assert_eq ! ( result. ids[ 1 ] . document_id( ) , 6 , "Second item should be 6" ) ;
458+ assert_eq ! ( result. ids[ 2 ] . document_id( ) , 7 , "Third item should be 7" ) ;
459+ assert_eq ! ( result. position, 5 , "Position should be 5" ) ;
460+ }
461+
462+ #[ test]
463+ fn test_pagination_with_anchor_offset_10 ( ) {
464+ let range: std:: ops:: Range < u32 > = 0 ..30 ;
465+ let mut builder = QueryResponseBuilder :: new (
466+ range. len ( ) ,
467+ 100 ,
468+ State :: Initial ,
469+ & QueryRequest :: < TestJMAPObject > {
470+ limit : Some ( 5 ) ,
471+ anchor : Some ( Id :: from_parts ( 0 , 3 ) ) ,
472+ anchor_offset : Some ( 10 ) ,
473+ ..Default :: default ( )
474+ } ,
475+ ) ;
476+
477+ for i in range {
478+ if !builder. add_id ( Id :: from_parts ( 0 , i) ) {
479+ break ;
480+ }
162481 }
482+
483+ let result = builder. build ( ) . unwrap ( ) ;
484+
485+ assert_eq ! ( result. ids. len( ) , 5 , "Should collect 5 items" ) ;
486+ assert_eq ! ( result. ids[ 0 ] . document_id( ) , 13 , "First item should be 13" ) ;
487+ assert_eq ! ( result. ids[ 1 ] . document_id( ) , 14 , "Second item should be 14" ) ;
488+ assert_eq ! ( result. ids[ 2 ] . document_id( ) , 15 , "Third item should be 15" ) ;
489+ assert_eq ! ( result. ids[ 3 ] . document_id( ) , 16 , "Fourth item should be 16" ) ;
490+ assert_eq ! ( result. ids[ 4 ] . document_id( ) , 17 , "Fifth item should be 17" ) ;
491+ assert_eq ! ( result. position, 13 , "Position should be 13" ) ;
163492 }
164493}
0 commit comments