File Coverage

lib/Net/API/CPAN/List.pm
Criterion Covered Total %
statement 187 373 50.1
branch 55 208 26.4
condition 51 256 19.9
subroutine 37 48 77.0
pod 32 32 100.0
total 362 917 39.4


line stmt bran cond sub pod time code
1             ##----------------------------------------------------------------------------
2             ## Meta CPAN API - ~/lib/Net/API/CPAN/List.pm
3             ## Version v0.1.0
4             ## Copyright(c) 2023 DEGUEST Pte. Ltd.
5             ## Author: Jacques Deguest <jack@deguest.jp>
6             ## Created 2023/08/02
7             ## Modified 2023/08/02
8             ## All rights reserved
9             ##
10             ##
11             ## This program is free software; you can redistribute it and/or modify it
12             ## under the same terms as Perl itself.
13             ##----------------------------------------------------------------------------
14             package Net::API::CPAN::List;
15             BEGIN
16             {
17 3     3   108563 use strict;
  3         17  
  3         89  
18 3     3   15 use warnings;
  3         5  
  3         77  
19 3     3   505 use parent qw( Net::API::CPAN::Generic );
  3         296  
  3         15  
20 3     3   171 use vars qw( $VERSION );
  3         7  
  3         110  
21 3     3   18 use Clone ();
  3         4  
  3         48  
22 3     3   1999 use HTTP::Promise;
  3         21741462  
  3         37  
23 3     3   1076 use Want;
  3         9  
  3         202  
24             use constant {
25             # Elastic Search default value.
26             # See <https://www.elastic.co/guide/en/elasticsearch/reference/2.4/search-request-from-size.html>
27 3         206 DEFAULT_PAGE_SIZE => 10,
28 3     3   19 };
  3         7  
29 3     3   61 our $VERSION = 'v0.1.0';
30             };
31              
32 3     3   14 use strict;
  3         14  
  3         95  
33 3     3   16 use warnings;
  3         6  
  3         10817  
34              
35             sub init
36             {
37 1     1 1 715 my $self = CORE::shift( @_ );
38 1 50       61 $self->{api} = undef unless( CORE::exists( $self->{api} ) );
39             # The name of the JSON property containing the array reference of data
40             # This is used for data in format other than search results such as hits->hits
41 1 50       5 $self->{container} = undef unless( CORE::exists( $self->{container} ) );
42             # The full URI to the endpoint used for loading data
43             # $self->{endpoint} = undef unless( CORE::exists( $self->{endpoint} ) );
44 1 50       4 $self->{filter} = undef unless( CORE::exists( $self->{filter} ) );
45             # Is this list a list that can load more data or is this set of data the only one available?
46 1 50       4 $self->{pageable} = 1 unless( CORE::exists( $self->{pageable} ) );
47 1 50       5 $self->{page} = 1 unless( CORE::exists( $self->{page} ) );
48             # Either 'from' or 'page' is used to navigate through pages of data
49 1 50       5 $self->{page_type} = 'from' unless( CORE::exists( $self->{page_type} ) );
50 1 50   1   7 $self->{postprocess} = sub{$_[0]} unless( CORE::exists( $self->{postprocess} ) );
  1         6  
51             # $self->{preprocess} = sub{$_[0]} unless( CORE::exists( $self->{preprocess} ) );
52 1 50       8 $self->{preprocess} = undef unless( CORE::exists( $self->{preprocess} ) );
53 1 50       5 $self->{request} = undef unless( CORE::exists( $self->{request} ) );
54             # We use this if we are not using filters
55 1 50       5 $self->{size} = undef unless( CORE::exists( $self->{size} ) );
56             # Usually this is 'size', but sometimes it is 'page_size'
57 1 50       5 $self->{size_prop} = 'size' unless( CORE::exists( $self->{size_prop} ) );
58 1 50       4 $self->{timed_out} = 0 unless( CORE::exists( $self->{timed_out} ) );
59 1 50       4 $self->{took} = undef unless( CORE::exists( $self->{took} ) );
60 1 50       6 $self->{total} = undef unless( CORE::exists( $self->{total} ) );
61 1 50       3 $self->{type} = undef unless( CORE::exists( $self->{type} ) );
62 1         4 $self->{_init_strict_use_sub} = 1;
63 1         5 $self->{_init_params_order} = [qw( debug type api container preprocess postprocess total data )];
64 1 50       8 $self->SUPER::init( @_ ) || return( $self->pass_error );
65 1 50 33     13 unless( CORE::exists( $self->{api} ) && $self->_is_a( $self->{api} => 'Net::API::CPAN' ) )
66             {
67 1 50       21 $self->_load_class( 'Net::API::CPAN' ) || return( $self->pass_error );
68 1   50     291 $self->{api} = Net::API::CPAN->new( debug => $self->debug ) ||
69             return( $self->pass_error( Net::API::CPAN->error ) );
70             }
71             # Simple initialisation
72 1         26 $self->{_raw_items} = $self->new_array;
73 1         30 return( $self );
74             }
75              
76 3     3 1 1001 sub api { return( CORE::shift->_set_get_object( 'api', 'Net::API::CPAN', @_ ) ); }
77              
78 3     3 1 436 sub container { return( CORE::shift->_set_get_scalar( 'container', @_ ) ); }
79              
80             sub data
81             {
82 0     0 1 0 my $self = CORE::shift( @_ );
83 0 0       0 if( @_ )
84             {
85 0 0       0 $self->load_data( @_ ) || return( $self->pass_error );
86             }
87 0         0 return( $self->items );
88             }
89              
90             # sub endpoint { return( CORE::shift->_set_get_uri( 'endpoint', @_ ) ); }
91              
92 7     7 1 1801 sub filter { return( CORE::shift->_set_get_object_without_init( 'filter', 'Net::API::CPAN::Filter', @_ ) ); }
93              
94             sub get
95             {
96 1     1 1 350 my $self = CORE::shift( @_ );
97 1 50 0     7 my $pos = @_ ? int( CORE::shift( @_ ) ) : ( $self->{_pos} || 0 );
98 1         6 my $data = $self->items;
99 1         6 my $what = lc( Want::wantref );
100 1 50       34 if( !defined( $data->[ $pos ] ) )
101             {
102 0 0 0     0 if( $what eq 'object' || $what eq 'hash' )
103             {
104 0         0 return( $self->new_null( type => $what ) );
105             }
106 0         0 return;
107             }
108 1         5 return( $data->[ $pos ] );
109             }
110              
111             sub has_more
112             {
113 1     1 1 733 my $self = CORE::shift( @_ );
114 1   50     5 my $total = $self->total // 0;
115 1         828 my( $filter, $size );
116 1 50       5 if( $filter = $self->filter )
117             {
118 1         38 $size = $filter->size;
119             }
120             # This is the default Elastic Search value
121 1   50     771 $size //= $self->size // DEFAULT_PAGE_SIZE;
      33        
122 1   50     770 my $offset = $self->offset // 0;
123 1         37177 $self->message( 4, "\$total = $total, \$offset = $offset, \$size = $size" );
124 1 50 33     356 $self->message( 4, "Returning true. There is more." ) if( $total && $total > $size && ( $total - ( $offset + 1 ) > 0 ) );
      33        
125             # Do we have data and is it bigger than the page size and we are not on the last page
126 1 50 33     1008 return(1) if( $total && $total > $size && ( $total - ( $offset + 1 ) > 0 ) );
      33        
127 0         0 $self->message( 4, "Returning false. There is no more data to fetch." );
128 0         0 return(0);
129             }
130              
131             sub items
132             {
133 8     8 1 1702 my $self = CORE::shift( @_ );
134 8 50 66     37 @_ = () if( @_ == 1 && !defined( $_[0] ) );
135 8 100       26 if( @_ )
136             {
137 1         3 my $ref = CORE::shift( @_ );
138 1         11 my $opts = $self->_get_args_as_hash( @_ );
139 1 50       18 return( $self->error( "I was expecting an array reference, but instead got '", overload::StrVal( $ref ), "'." ) ) if( !$self->_is_array( $ref ) );
140 1         16 my $cache = {};
141 1         8 my $api = $self->api;
142 1         39 my $arr = $self->new_array;
143 1         20 my $def_type = $self->type->scalar;
144 1         578 $self->message( 4, "Processing ", scalar( @$ref ), " elements with type '$def_type'" );
145 1         27 for( my $i = 0; $i < scalar( @$ref ); $i++ )
146             {
147 10 50       127 return( $self->error( "I was expecting an array of hash reference, but instead of an hash I found '", overload::StrVal( $ref->[$i] ), "'" ) ) if( ref( $ref->[$i] ) ne 'HASH' );
148 10         22 my $hash = $ref->[$i];
149 10   50     90 my $type = $opts->{type} || $hash->{_type} || $def_type ||
150             return( $self->error( "No object type was specified nor any could be found in the hash reference at offset $i" ) );
151 10         19 my $class;
152 10 100       29 unless( $class = $cache->{ $type } )
153             {
154 1   50     10 $class = $self->_object_type_to_class( $type ) || return( $self->error( "Could not find corresponding class for ojbect type \"$type\"." ) );
155 1         10 $self->message( 4, "Loading class $class (", overload::StrVal( $class ), ") for object type $type" );
156 1 50       30 $self->_load_class( $class ) || return( $self->pass_error );
157 1         379 $cache->{ $type } = $class;
158             # XXX
159             # if( $class eq 'Net::API::CPAN::List::Web::Element' )
160             # {
161             # my @symbols = $self->_list_symbols( 'Net::API::CPAN::List::Web::Element' );
162             # $self->message( 4, "Symbols found for class Net::API::CPAN::List::Web::Element are -> ", sub{ $self->Module::Generic::dump( \@symbols ) } );
163             # }
164             }
165 10 50 33     68 $hash = $hash->{_source} if( exists( $hash->{_source} ) && ref( $hash->{_source} ) eq 'HASH' );
166             # $self->message( 4, "Instantiating a $class object at offset $i for object type $type with data: ", sub{ $self->Module::Generic::dump( $hash ) } );
167 10         36 $hash->{debug} = $self->debug;
168 10   50     384 my $o = $class->new( %$hash, api => $api ) || return( $self->pass_error( $class->error ) );
169 10         132 $self->message( 4, "Adding new $class object $o (", overload::StrVal( $o ), ") to the stack." );
170 10         295 $arr->push( $o );
171             }
172 1         15 $self->{items} = $arr;
173             }
174 8 50 33     77 if( !$self->{items} || !$self->_is_a( $self->{items} => 'Module::Generic::Array' ) )
175             {
176 0 0       0 $self->{items} = $self->new_array( defined( $self->{items} ) ? $self->{items} : [] );
177             }
178 8         339 return( $self->{items} );
179             }
180              
181 1     1 1 37387 sub length { return( CORE::shift->items->length ); }
182              
183             sub load
184             {
185 0     0 1 0 my $self = CORE::shift( @_ );
186 0         0 my $opts = $self->_get_args_as_hash( @_ );
187             my $req = $opts->{request} ||
188 0   0     0 return( $self->error( "No HTTP request was provided to load data." ) );
189 0   0     0 my $api = $self->api ||
190             return( $self->error( "No Net::API::CPAN obejct is currently set. This should not happen." ) );
191 0   0     0 my $type = $self->type ||
192             return( $self->error( "No object type set for this list." ) );
193 0         0 my $filter = $self->filter;
194 0 0       0 if( $filter )
195             {
196 0 0       0 return( $self->error( "No search query set." ) ) if( !$filter->query );
197 0   0     0 my $json = $filter->as_json ||
198             return( $self->pass_error( $filter->error ) );
199 0         0 $req->method( 'POST' );
200             }
201             else
202             {
203 0         0 $req->method( 'GET' );
204             }
205 0         0 $req->headers->header( Accept => 'application/json' );
206             my $data = $api->fetch( $type => {
207             request => $req,
208             # We simply want the raw data back
209 0     0   0 class => sub{ $_[0] },
210 0   0     0 }) || return( $self->pass_error( $api->error ) );
211            
212 0 0       0 $self->load_data( $data ) || return( $self->pass_error );
213 0         0 return( $self );
214             }
215              
216             sub load_data
217             {
218 1     1 1 10576 my $self = CORE::shift( @_ );
219 1   50     7 my $data = CORE::shift( @_ ) ||
220             return( $self->error( "No data was provided to load." ) );
221 1 50       7 return( $self->error( "Data provided is not an hash reference." ) ) if( ref( $data ) ne 'HASH' );
222 1         22 $self->message( 4, "Loading data received with ", scalar( keys( %$data ) ), " properties: ", join( ', ', sort( keys( %$data ) ) ) );
223 1         35 my $filter = $self->filter;
224 1         27 my $container = $self->container;
225 1   50     838 $self->message( 4, "Container to use is '", ( $container // 'undef', ), "'" );
226              
227 1 50       27 if( my $code = $self->preprocess )
228             {
229 0         0 $self->message( 4, "Executing preprocess." );
230             # try-catch
231 0         0 local $@;
232             $data = eval
233 0         0 {
234 0         0 $code->( $data );
235             };
236 0 0       0 if( $@ )
237             {
238 0         0 return( $self->error( $@ ) );
239             }
240             # $self->message( 5, "After preprocess, data now is: ", sub{ $self->Module::Generic::dump( $data ) } );
241             }
242              
243             # $ref is the variable containing the array reference of data
244 1         773 my( $ref, $total );
245 1 50 33     31 if( ( $container &&
      33        
      0        
      0        
      0        
      33        
      33        
      33        
      33        
      33        
246             $container eq 'hits' &&
247             exists( $data->{ $container } ) &&
248             ref( $data->{ $container } ) eq 'HASH' &&
249             exists( $data->{ $container }->{hits} ) &&
250             ref( $data->{ $container }->{hits} ) eq 'ARRAY'
251             ) ||
252             ( !defined( $container ) &&
253             exists( $data->{hits} ) &&
254             ref( $data->{hits} ) eq 'HASH' &&
255             exists( $data->{hits}->{hits} ) &&
256             ref( $data->{hits}->{hits} ) eq 'ARRAY'
257             ) )
258             {
259 1         5 $self->message( 4, "Guessed container to be 'hits->hits'" );
260 1 50       26 unless( defined( $container ) )
261             {
262 1         6 $self->container( $container = 'hits' );
263             }
264 1 50 33     829 if( !exists( $data->{hits} ) ||
    50 33        
      33        
      33        
265             !defined( $data->{hits} ) ||
266             ref( $data->{hits} ) ne 'HASH' )
267             {
268             # return( $self->error( "Malformed data received. I was expecting a top property 'hits' to be an hash reference." ) );
269             # Actually not necessarily an error, but simply no more data
270 0         0 $self->items->reset;
271             }
272             elsif( !exists( $data->{hits}->{hits} ) ||
273             !defined( $data->{hits}->{hits} ) ||
274             ref( $data->{hits}->{hits} ) ne 'ARRAY' )
275             {
276             # return( $self->error( "Malformed data received. I was expecting the property 'hits' within the top property 'hits' to be an array reference." ) );
277             # Actually not necessarily an error, but simply no more data
278 0         0 $self->items->reset;
279             }
280             else
281             {
282 1         4 $ref = $data->{hits}->{hits};
283             # The overall number of hits
284 1 50 33     18 if( exists( $data->{hits}->{total} ) &&
      33        
285             defined( $data->{hits}->{total} ) &&
286             CORE::length( $data->{hits}->{total} ) )
287             {
288 1         7 $self->message( 4, "Setting total value using hits->total (", $data->{hits}->{total}, ")" );
289             # $self->total( $data->{hits}->{total} );
290 1         22 $total = $data->{hits}->{total};
291             }
292             # If the information is not available somehow, use the size of the array
293             else
294             {
295 0         0 $self->message( 4, "Setting total value using items->length (", $self->items->length, ")" );
296             # $self->total( $self->items->length );
297 0         0 $total = $self->items->length;
298             }
299             }
300             }
301             else
302             {
303 0         0 $self->message( 4, "Container is not hits->hits, trying to find the first property with an array." );
304 0 0       0 unless( $container = $self->container )
305             {
306 0         0 foreach my $prop ( keys( %$data ) )
307             {
308 0 0 0     0 if( defined( $data->{ $prop } ) &&
309             ref( $data->{ $prop } ) eq 'ARRAY' )
310             {
311 0         0 $container = $prop;
312 0         0 $self->message( 4, "Found the container being the property '$prop'" );
313             # Save it for next time
314 0         0 $self->container( $container );
315 0         0 last;
316             }
317             }
318             }
319            
320 0 0       0 if( !defined( $container ) )
    0          
    0          
    0          
321             {
322 0         0 return( $self->error( "No data container name was specified and none could be found in the data provided." ) );
323             }
324             elsif( !exists( $data->{ $container } ) )
325             {
326 0         0 return( $self->error( "Data container specified '$container' does not exist in the data provided." ) );
327             }
328             # There is simply no data. Admittedly it would be better if it was defined and an empty array
329             elsif( !defined( $data->{ $container } ) )
330             {
331 0         0 $ref = [];
332             }
333             elsif( ref( $data->{ $container } ) ne 'ARRAY' )
334             {
335 0   0     0 return( $self->error( "Data container specified '$container' does not point to an array reference, but to a '", ( ref( $data->{ $container } ) // 'string' ), "'." ) );
336             }
337             else
338             {
339 0         0 $ref = $data->{ $container };
340             }
341            
342             # Like for some release data, such as /all_by_author
343 0 0 0     0 $total = $data->{total} if( exists( $data->{total} ) && CORE::length( $data->{total} // '' ) );
      0        
344             }
345              
346 1 50       8 if( defined( $ref ) )
347             {
348 1         5 $self->message( 4, "Container contains ", scalar( @$ref ), " ", $self->type, " elements." );
349             # We take steps to ensure the data we received is not the same as the data we already have
350 1         469 my $new_items = $self->new_array( $ref );
351 1   33     78 my $old_items = $self->{_raw_items} // $self->new_array;
352 1     0   10 $self->message( 5, "Old items are -> ", sub{ $self->Module::Generic::dump( $old_items ) } );
  0         0  
353             # Set the size of the number of elements per page, so we can rely on it, even if the array is modified afterwards
354             # unless( defined( $filter->size ) )
355             # {
356             # $filter->size( $new_items->length );
357             # }
358             # Set the value for 'type' for our object if it was not set already by the user.
359             # It should already have been set if there was an API call, but not necessarily if the data were simply and directly loaded here.
360 1 50 50     24 if( !$self->type &&
      33        
      33        
      50        
      33        
361             scalar( @$new_items ) &&
362             ref( $new_items->[0] ) eq 'HASH' &&
363             exists( $new_items->[0]->{_type} ) &&
364             CORE::length( $new_items->[0]->{_type} // '' ) )
365             {
366 1         770 $self->type( $new_items->[0]->{_type} );
367             }
368            
369 1 50 33     957 if( !$new_items->is_empty &&
      33        
      33        
370             !$old_items->is_empty &&
371             ref( $new_items->first ) eq 'HASH' &&
372             ref( $old_items->first ) eq 'HASH' )
373             {
374 0         0 my $json = $self->new_json->canonical;
375 0         0 my $new_first = $new_items->first;
376 0         0 my $old_first = $old_items->first;
377 0         0 my( $new_serial, $old_serial );
378 0         0 local $@;
379             # try-catch
380             eval
381 0         0 {
382 0         0 $new_serial = $json->encode( $new_first );
383 0         0 $old_serial = $json->encode( $old_first );
384             };
385 0 0       0 if( $@ )
386             {
387 0         0 return( $self->error( "Error serialising hash reference into JSON data: $@" ) );
388             }
389              
390             # old and new data are the same. We cannot have that,
391             # so we set an empty data pool
392 0 0 0     0 if( defined( $new_serial ) &&
      0        
393             defined( $old_serial ) &&
394             $new_serial eq $old_serial )
395             {
396 0         0 $self->items->reset;
397             }
398             else
399             {
400 0         0 $self->items( $ref );
401 0         0 $self->{_raw_items} = $self->new_array( Clone::clone( $ref ) );
402             }
403             }
404             # Ok, old and new data are not the same
405             else
406             {
407 1         49 $self->items( $ref );
408 1         346 $self->{_raw_items} = $self->new_array( Clone::clone( $ref ) );
409             }
410            
411             # If the information is not available somehow, use the size of the array
412 1   33     55 $self->total( $total // $self->items->length );
413             # If it was already set the first time, we do not overwrite it.
414             # This is used so we can compute next page offset
415 1 50       37900 unless( $self->page_size )
416             {
417 1         654 $self->page_size( $self->items->length );
418             }
419             }
420             else
421             {
422 0         0 return( $self->error( "No data found for container '$container'" ) );
423             }
424            
425 1 50 33     36773 if( exists( $data->{timed_out} ) &&
      33        
426             defined( $data->{timed_out} ) &&
427             CORE::length( $data->{timed_out} ) )
428             {
429 1         173 $self->timed_out( $data->{timed_out} );
430             }
431             else
432             {
433 0         0 $self->timed_out(0);
434             }
435            
436 1 50 33     888 if( exists( $data->{took} ) &&
      33        
437             defined( $data->{took} ) &&
438             CORE::length( $data->{took} ) )
439             {
440 1         10 $self->took( $data->{took} );
441             }
442             else
443             {
444 0         0 $self->took(undef);
445             }
446             # Reset the array position
447 1         36579 delete( $self->{_pos} );
448            
449 1 50       8 if( my $code = $self->postprocess )
450             {
451 1         1155 $self->message( 4, "Executing postprocess." );
452             # try-catch
453 1         27 local $@;
454             eval
455 1         3 {
456 1         5 $code->( $data );
457             };
458 1 50       4 if( $@ )
459             {
460 0         0 return( $self->error( $@ ) );
461             }
462             }
463 1         9 return( $self );
464             }
465              
466             sub next
467             {
468 2     2 1 37964 my $self = CORE::shift( @_ );
469 2 100       12 $self->{_pos} = -1 if( !exists( $self->{_pos} ) );
470 2         6 my $data = $self->items;
471 2         10 my $what = lc( Want::wantref );
472 2         54 my $val;
473             # if( $self->{_pos} + 1 < $data->length )
474             # {
475 2         9 $val = $data->[ ++$self->{_pos} ];
476             # }
477            
478 2 50       7 if( !defined( $val ) )
479             {
480             # Our offset exceeds the size of our data pool and we have more data, so let's fetch some more
481 0 0 0     0 if( $self->{_pos} > $data->size && $self->has_more )
482             {
483 0   0     0 my $req = $self->request ||
484             return( $self->error( "No initial HTTP request was provided to load data." ) );
485 0         0 $req = $req->clone;
486 0         0 my $filter = $self->filter;
487             # Starting from 1
488 0   0     0 my $page = $self->page // 1;
489 0         0 my $size;
490 0 0       0 if( $filter )
491             {
492 0 0       0 return( $self->error( "No search query set." ) ) if( !$filter->query );
493             # 10 is Elastic Search default size
494 0         0 $size = $filter->size;
495 0 0 0     0 if( defined( $size ) && !$size )
496             {
497 0   0     0 $size = $self->size // DEFAULT_PAGE_SIZE;
498 0         0 $filter->size( $size );
499             }
500 0         0 $filter->from( int( ( $page - 1 ) * $size ) );
501             }
502             else
503             {
504 0         0 $self->message( 5, "Original query URL is ", $req->uri );
505 0         0 my $query = $req->uri->query_form_hash;
506 0 0 0     0 if( !scalar( keys( %$query ) ) ||
      0        
507             ( !exists( $query->{page} ) && !exists( $query->{from} ) && !exists( $query->{size} ) ) )
508             {
509 0         0 return( $self->error( "No search query set." ) );
510             }
511 0         0 $size = $self->size;
512             # If size option is set, otherwise we leave it out
513 0 0       0 if( defined( $size ) )
514             {
515 0   0     0 $size //= DEFAULT_PAGE_SIZE;
516 0   0     0 my $size_prop = $self->size_prop // 'size';
517 0         0 $query->{ $size_prop } = $size;
518             }
519 0   0     0 my $page_type = $self->page_type || 'from';
520 0   0     0 $self->message( 4, "Page is '", ( $page // 'undef' ), "' size is '", ( $size // 'undef' ), "', page_type is '", ( $page_type // 'undef' ), "'" );
      0        
      0        
521 0 0       0 if( $page_type eq 'from' )
    0          
522             {
523 0   0     0 my $page_size = $size // $self->page_size;
524             # 0..9 or 10..19
525 0         0 my $offset = int( ( $page - 1 ) * $page_size );
526 0         0 $self->message( 4, "Offset is '$offset'" );
527 0         0 $query->{from} = $offset;
528             }
529             elsif( $page_type eq 'page' )
530             {
531 0         0 $query->{page} = ( $page + 1 );
532             }
533             else
534             {
535 0         0 return( $self->error( "Unknown page type '$page_type'" ) );
536             }
537 0     0   0 $self->message( 4, "Setting new query with hash -> ", sub{ $self->dump( $query ) } );
  0         0  
538 0         0 $req->uri->query_form( $query );
539 0         0 $self->message( 4, "Using URI for next load of data -> ", $req->uri );
540             }
541            
542             # User will need to check if there is an error if the user gets an undefined value in return,
543             # to distinguish between no more value vs a returned error
544 0 0       0 $self->load( request => $req ) || return( $self->pass_error );
545             # We set our current page to +1 if indeed we have data.
546 0 0       0 $self->page( $page + 1 ) if( !$self->items->is_empty );
547 0         0 return( $self->next );
548             }
549            
550 0 0 0     0 if( $what eq 'object' || $what eq 'hash' )
551             {
552 0         0 return( $self->new_null( type => $what ) );
553             }
554 0         0 return;
555             }
556 2         6 return( $val );
557             }
558              
559             sub offset
560             {
561 3     3 1 2617 my $self = CORE::shift( @_ );
562 3   50     12 my $page = $self->page // 1;
563 3         38357 my( $size, $filter );
564 3 50       11 if( $filter = $self->filter )
565             {
566 3         95 $size = $filter->size;
567             }
568             # $size //= $self->page_size // DEFAULT_PAGE_SIZE;
569 3   50     2318 $size ||= $self->page_size || DEFAULT_PAGE_SIZE;
      33        
570 3   100     2363 my $pos = $self->pos // 0;
571 3         11 $self->message( 4, "Calculating offset using page '$page', size '$size' and pos '$pos' -> ", ( $pos + ( $size * ( $page - 1 ) ) ) );
572 3         3478 return( $self->new_number( $pos + ( $size * ( $page - 1 ) ) ) );
573             }
574              
575 4     4 1 38736 sub page { return( CORE::shift->_set_get_number( 'page', @_ ) ); }
576              
577 0     0 1 0 sub pageable { return( CORE::shift->_set_get_number( 'pageable', @_ ) ); }
578              
579 5     5 1 35891 sub page_size { return( CORE::shift->_set_get_number( 'page_size', @_ ) ); }
580              
581 1     1 1 1988 sub page_type { return( shift->_set_get_scalar_as_object( 'page_type', @_ ) ); }
582              
583             sub pop
584             {
585 0     0 1 0 my $self = CORE::shift( @_ );
586 0         0 my $data = $self->items;
587 0         0 my $what = lc( Want::wantref );
588 0         0 my $val = $self->items->pop;
589              
590 0 0       0 if( !defined( $val ) )
591             {
592 0 0 0     0 if( $what eq 'object' || $what eq 'hash' )
593             {
594 0         0 return( $self->new_null( type => $what ) );
595             }
596 0         0 return;
597             }
598 0         0 return( $val );
599             }
600              
601 4     4 1 1243 sub pos { return( CORE::shift->{_pos} ); }
602              
603 1     1 1 9 sub preprocess { return( CORE::shift->_set_get_code( 'preprocess', @_ ) ); }
604              
605 2     2 1 1728 sub postprocess { return( CORE::shift->_set_get_code( 'postprocess', @_ ) ); }
606              
607             sub prev
608             {
609 1     1 1 39910 my $self = CORE::shift( @_ );
610 1 50       6 $self->{_pos} = -1 if( !exists( $self->{_pos} ) );
611 1         12 my $data = $self->items;
612 1         6 my $what = lc( Want::wantref );
613 1         30 my $val;
614 1 50       11 if( $self->{_pos} - 1 >= 0 )
615             {
616 1         5 $val = $data->[ --$self->{_pos} ];
617             }
618              
619 1 50       5 if( !defined( $val ) )
620             {
621             # Starting from 1
622 0   0     0 my $page = $self->page // 1;
623 0 0 0     0 if( $self->{_pos} <= 0 && $page > 1 )
624             {
625 0   0     0 my $req = $self->request ||
626             return( $self->error( "No initial HTTP request was provided to load data." ) );
627 0         0 $req = $req->clone;
628 0         0 my $filter = $self->filter;
629             # Next page
630 0         0 $page--;
631 0         0 my $size;
632 0 0       0 if( $filter )
633             {
634 0 0       0 return( $self->error( "No search query set." ) ) if( !$filter->query );
635             # 10 is Elastic Search default size
636 0         0 $size = $filter->size;
637 0 0 0     0 if( defined( $size ) && !$size )
638             {
639 0   0     0 $size = $self->size // DEFAULT_PAGE_SIZE;
640 0         0 $filter->size( $size );
641             }
642 0         0 $filter->from( int( ( $page - 1 ) * $size ) );
643             }
644             else
645             {
646 0         0 my $query = $req->uri->query_form_hash;
647 0 0 0     0 return( $self->error( "No search query set." ) ) if( !scalar( keys( %$query ) ) || !CORE::length( $query->{'q'} // '' ) );
      0        
648 0         0 $size = $self->size;
649             # If size option is set, otherwise we leave it out
650 0 0       0 if( defined( $size ) )
651             {
652 0   0     0 $size //= DEFAULT_PAGE_SIZE;
653 0   0     0 my $size_prop = $self->size_prop // 'size';
654 0         0 $query->{ $size_prop } = $size;
655             }
656 0   0     0 my $page_type = $self->page_type || 'from';
657 0 0       0 if( $page_type eq 'from' )
    0          
658             {
659             # 0..9 or 10..19
660 0         0 my $offset = int( ( $page - 1 ) * $size );
661 0         0 $query->{from} = $offset;
662             }
663             elsif( $page_type eq 'page' )
664             {
665 0         0 $query->{page} = ( $page + 1 );
666             }
667             else
668             {
669 0         0 return( $self->error( "Unknown page type '$page_type'" ) );
670             }
671 0         0 $req->uri->query_form_hash( $query );
672             }
673            
674             # User will need to check if there is an error if the user gets an undefined value in return,
675             # to distinguish between no more value vs a returned error
676 0 0       0 $self->load( request => $req ) || return( $self->pass_error );
677             # We set our current page to +1 if indeed we have data.
678 0 0       0 $self->page( $page ) if( !$self->items->is_empty );
679             }
680            
681 0 0 0     0 if( $what eq 'object' || $what eq 'hash' )
682             {
683 0         0 return( $self->new_null( type => $what ) );
684             }
685 0         0 return;
686             }
687 1         3 return( $val );
688             }
689              
690             sub push
691             {
692 0     0 1 0 my $self = CORE::shift( @_ );
693 0   0     0 my $this = CORE::shift( @_ ) || return( $self->error( "Nothing was provided to add to the list of object." ) );
694 0 0       0 $self->_check( $this ) || return( $self->pass_error );
695 0         0 $self->items->push( $this );
696 0         0 return( $self );
697             }
698              
699 1     1 1 1203 sub request { CORE::return( shift->_set_get_object_without_init( 'request', 'HTTP::Promise::Request', @_ ) ); }
700              
701             sub shift
702             {
703 0     0 1 0 my $self = CORE::shift( @_ );
704 0         0 my $data = $self->items;
705 0         0 my $what = lc( Want::wantref );
706 0         0 my $val = $self->items->shift;
707              
708              
709 0 0       0 if( !defined( $val ) )
710             {
711 0 0 0     0 if( $what eq 'object' || $what eq 'hash' )
712             {
713 0         0 return( $self->new_null( type => $what ) );
714             }
715 0         0 return;
716             }
717 0         0 return( $val );
718             }
719              
720 1     1 1 5 sub size { return( CORE::shift->_set_get_number( 'size', @_ ) ); }
721              
722 1     1 1 432 sub size_prop { return( CORE::shift->_set_get_scalar_as_object( 'size_prop', @_ ) ); }
723              
724             # NOTE: timed_out is returned by MetaCPAN API (Elastic Search)
725 2     2 1 1852 sub timed_out { return( CORE::shift->_set_get_boolean( 'timed_out', @_ ) ); }
726              
727             # NOTE: took is returned by MetaCPAN API (Elastic Search)
728 2     2 1 1112 sub took { return( CORE::shift->_set_get_number( { field => 'took', undef_ok => 1 }, @_ ) ); }
729              
730             # NOTE: total returns the number returned by MetaCPAN API, which represents the overal total number of hits across all pages
731 3     3 1 1387 sub total { return( CORE::shift->_set_get_number( 'total', @_ ) ); }
732              
733 5     5 1 1327 sub type { return( CORE::shift->_set_get_scalar_as_object( 'type', @_ ) ); }
734              
735             sub unshift
736             {
737 0     0 1   my $self = CORE::shift( @_ );
738 0   0       my $this = CORE::shift( @_ ) || return( $self->error( "Nothing was provided to add to the list of object." ) );
739 0 0         $self->_check( $this ) || return( $self->pass_error );
740 0           $self->items->unshift( $this );
741 0           return( $self );
742             }
743              
744             sub _check
745             {
746 0     0     my $self = CORE::shift( @_ );
747 0   0       my $this = CORE::shift( @_ ) || return( $self->error( "No data was provided to check." ) );
748 0 0         return( $self->error( "Data provided is not an object." ) ) if( !$self->_is_object( $this ) );
749             # Check if there is any data and if there is find out what kind of object we are holding so we can maintain consistency
750 0           my $data = $self->items;
751 0           my $obj_name;
752 0 0 0       if( !$data->is_empty && $self->_is_object( $data->[0] ) )
753             {
754 0 0         $obj_name = $data->[0]->object if( $data->[0]->can( 'object' ) );
755             }
756 0 0         if( $this->can( 'object' ) )
757             {
758 0           my $this_object = $this->object;
759 0 0 0       $this_object = '' if( !defined( $this_object ) || !$this_object->defined );
760 0 0 0       $obj_name = '' if( !defined( $obj_name ) || !$obj_name->defined );
761 0 0         return( $self->error( "Object provided (", overload::StrVal( $this ), ") has an object type (${this_object}) different from the ones currently in our stack (${obj_name})." ) ) if( $this_object ne $obj_name );
762             }
763 0           return( $this );
764             }
765              
766             1;
767             # NOTE: POD
768             __END__
769              
770             =encoding utf-8
771              
772             =head1 NAME
773              
774             Net::API::CPAN::List - Meta CPAN API List
775              
776             =head1 SYNOPSIS
777              
778             use Net::API::CPAN::List;
779             my $list = Net::API::CPAN::List->new(
780             items => $array_ref,
781             ) || die( Net::API::CPAN::List->error, "\n" );
782             # or
783             my $list = Net::API::CPAN::List->new;
784             $list->load_data( $hash_ref ) || die( $list->error );
785              
786             =head1 VERSION
787              
788             v0.1.0
789              
790             =head1 DESCRIPTION
791              
792             This class is used to retrieve and manipulate list of data such as the ones resulting from a search query.
793              
794             It inherits from L<Net::API::CPAN::Generic>
795              
796             =head1 CONSTRUCTOR
797              
798             =head2 new
799              
800             Provided with an hash or an hash reference of parameters and this will instantiate a new list object.
801              
802             The valid parmeters that can be used are as below and can also be accessed with their corresponding method:
803              
804             =over 4
805              
806             =item * C<api>
807              
808             An L<Net::API::CPAN> object.
809              
810             =item * C<items>
811              
812             An array reference of data.
813              
814             =back
815              
816             =head1 METHODS
817              
818             =head2 api_uri
819              
820             Sets or gets the MetaCPAN API base URI, which defaults to L<https://fastapi.metacpan.org>
821              
822             This returns an L<URI> object.
823              
824             =head2 api_version
825              
826             Sets or gets the MetaCPAN API version, which defaults to C<1>. This is used to form the base of the endpoints, such as C</v1/author/search>
827              
828             It returns a L<scalar object|Module::Generic::Scalar>
829              
830             =head2 container
831              
832             Sets or gets a string representing the property containing the array of all the data.
833              
834             If this is not set, then L</load_data> will try to guess it.
835              
836             =head2 data
837              
838             $list->data( $hash_ref ) || die( $list->error );
839             my $array_ref = $list->data;
840              
841             This is a convenient shortcut that calls L<load_data|/load_data> when data is provided, and returns the call to L<items|/items> either way.
842              
843             =head2 filter
844              
845             Sets or gets the L<filter object|Net::API::CPAN::Filter>. It returns C<undef> in scalar context if none is set, but it instantiates automatically a new instance in object context.
846              
847             my $filter = $list->filter; # undef
848             my $from = $list->filter->from; # ok, it works, but still undef
849              
850             =head2 get
851              
852             # implicit
853             my $obj = $list->get;
854             # explicit
855             my $obj = $list->get(0);
856             # or
857             my $obj = $list->get(2);
858              
859             This returns the data object at the current offset, or at the provided offset if one was provided.
860              
861             If no data object exists at the offset, this will return C<undef> in scalar context, or an empty list in list context, and it will return a L<null object|Module::Generic::Null> in object context to prevent a perl error of some methods called with an undefined value. The null object virtual method call will return C<undef> eventually.
862              
863             For example, let's assume a list of only 1 element. Calling C<get> with offset C<3> exceeds the size of the data pool, and would return an C<undef> value, but since it is called in object context, it returns a L<null object|Module::Generic::Null> instead.
864              
865             my $undef = $list->get(3)->author;
866              
867             =head2 has_more
868              
869             Read-only. This will return true if there are more data to be fetched from the MetaCPAN API, or false otherwise.
870              
871             =head2 http_request
872              
873             Sets or gets an L<HTTP::Promise::Request> object.
874              
875             =head2 http_response
876              
877             Sets or gets an L<HTTP::Promise::Response> object.
878              
879             =head2 items
880              
881             Provided with an array reference and an hash or hash reference of options and this sets the provided array as the active pool of data.
882              
883             The data contained can be either an array reference of hash reference, or an array reference of objects. If the data provided are an array reference of hash reference, they will be turned into their corresponding object, based on the value of each hash C<_type> property, such as C<< _type => "author" >>
884              
885             It always return an L<array object|Module::Generic::Array>, whether any data were provided or not.
886              
887             =head2 load
888              
889             Using the L<filter object|Net::API::CPAN::Filter> accessible with L</filter>, this will issue a new HTTP C<POST> query to the MetaCPAN API endpoint to retrieve the C<JSON> data.
890              
891             It will then populate the data, notably to C<items>, C<timed_out>, C<total>, C<took> and return the current object for chaining.
892              
893             It also sets the HTTP request object and the HTTP response object that can be retrieved with L</http_request> and L</http_response> respectively.
894              
895             If an error occurred, it will set an L<error object|Net::API::CPAN::Exception>, and return C<undef> in scalar context or an empty list in list context.
896              
897             There is no need to access C<load> directly. This would be called automatically when more data is requested and if there is indeed more data.
898              
899             =head2 load_data
900              
901             Provided with an hash reference of data, and this will load it into the current object, and return the current object for chaining. Upon error, this will set an L<error object|Net::API::CPAN::Exception> and return C<undef> in scalar context, or an empty list in list context.
902              
903             =head2 length
904              
905             Read-only. This returns the size of the data pool as a L<number object|Module::Generic::Number>.
906              
907             A 10-elements data pool would return 10. The value returned is directly related to the sie of the arra reference data pool, so if you use the methods L</pop>, L</unshift>, L</shift>, L</unshift>, it will affect the value returned here.
908              
909             See also L</total>
910              
911             =head2 next
912              
913             my $obj = $list->next;
914              
915             This returns the next data object in the data pool, or if none exists, C<undef> in scalar context and an empty list in list context, and it will return a L<null object|Module::Generic::Null> in object context to prevent a perl error of some methods called with an undefined value. The null object virtual method call will return C<undef> eventually.
916              
917             For example, let's assume the list is empty. Calling C<next> would return an C<undef> value, but since it is called in object context, it returns a L<null object|Module::Generic::Null> instead.
918              
919             my $undef = $list->next->author;
920              
921             The size of the data pool returned from the MetaCPAN REST API is based upon the value of L<size|/size>, which usually defaults to C<10>. Once C<next> has reached the last element in the data pool, it will attempt to load more data, if there are more to load at all. To know that, it calls L</has_more>. Thus, when C<undef> is returned, it really means, there is no more data to retrieve.
922              
923             =head2 offset
924              
925             Read-only. Returns the offset position of the current item across the entire data set.
926              
927             For example, if you are currently checking the 3rd element of the 2 data page, the offset value would be C<12>, because offset starts at C<0>
928              
929             This returns a L<number object|Module::Generic::Number>
930              
931             See also L</pos>
932              
933             =head2 page
934              
935             Integer. Sets or gets an integer representing the current page of data being used.
936              
937             Returns a L<number object|Module::Generic::Number>
938              
939             =head2 page_type
940              
941             Sets or gets a L<scalar object|Module::Generic::Scalar> representing the type of paging used to access next and previous pages. Possible values are C<from> and C<page>, and this defaults to C<from>
942              
943             When it is set to C<from>, L<load|/load> will use a data offset starting from C<0>. For example, on a data set broken into pages of 20 elements each, moving to the 2 pages would set the C<from> value to C<20>.
944              
945             If C<page_type> is set to C<page>, the L<page|/page> number starting from C<1> will be used.
946              
947             =head2 pageable
948              
949             Boolean. Sets or gets a boolean value representing whether more data can be loaded beyond the current set, or if the current set the only set of available data.
950              
951             Returns a L<boolean object|Module::Generic::Boolean>
952              
953             For example, L<autocomplete|Net::API::CPAN/autocomplete> returns a set of 10 elements, but is not pageable.
954              
955             =head2 page_size
956              
957             Integer. This indicates the size of each result page. Contrary to L<size|/size>, which is a preference that can be set to indicate how many result per page one wants, C<page_size> is set upon loading data with L<load_data|/load_data> and reflects the actul page size.
958              
959             If the page contains only a small amount of results, such as 3, then C<page_size> will be 3, but if the overall total exceeds that of the page size, C<page_size> will show how many result per page is provided.
960              
961             This information is then used for new requests to load more data by L<next|/next> and L<prev|/prev>
962              
963             Thus it is more an internal method.
964              
965             =head2 pop
966              
967             my $obj = $list->pop;
968              
969             This removes the last entry from the data pool, thus altering it, and returns it.
970              
971             If the value to be returned is undefined, it will return C<undef> in scalar context and an empty list in list context, and it will return a L<null object|Module::Generic::Null> in object context to prevent a perl error of some methods called with an undefined value. The null object virtual method call will return C<undef> eventually.
972              
973             For example, let's assume the list is empty. Calling C<pop> would return an C<undef> value, but since it is called in object context, it returns a L<null object|Module::Generic::Null> instead.
974              
975             my $undef = $list->pop->author;
976              
977             You might want to use C<< $list->get(-1) >> instead to avoid modifying the array reference of data.
978              
979             =head2 pos
980              
981             Read-ony. Returns the current position in the array reference of data pool. This would be a positive integer, or C<undef> if no data was accessed yet.
982              
983             See also L</offset>
984              
985             =head2 postprocess
986              
987             Sets or gets the caller (anonymous subroutine) that is is called by L</load_data> with the hash reference of data received from the MetaCPAN API, for possible post processing.
988              
989             This method, not the callback, returns the current object for chaining, or upon error, sets an L<error|Net::API::CPAN::Exception> and returns C<undef> in scalar context or an empty list in list context.
990              
991             =head2 preprocess
992              
993             Sets or gets the caller (anonymous subroutine) that is is called by L</load_data> with the hash reference of data received from the MetaCPAN API, for possible pre processing.
994              
995             This method, not the callback, returns the current object for chaining, or upon error, sets an L<error|Net::API::CPAN::Exception> and returns C<undef> in scalar context or an empty list in list context.
996              
997             =head2 prev
998              
999             my $obj = $list->next;
1000              
1001             This returns the previous data object in the data pool, or if none exists, C<undef> in scalar context and an empty list in list context, and it will return a L<null object|Module::Generic::Null> in object context to prevent a perl error of some methods called with an undefined value. The null object virtual method call will return C<undef> eventually.
1002              
1003             For example, let's assume the list is empty. Calling C<prev> would return an C<undef> value, but since it is called in object context, it returns a L<null object|Module::Generic::Null> instead.
1004              
1005             my $undef = $list->prev->author;
1006              
1007             =head2 push
1008              
1009             $list->push( $object );
1010              
1011             Provided with a proper object value, and this will add it to the end of the data pool and returns the current list object for chaining purposes.
1012              
1013             A valid value must be an object of the same type as the ones used in this data pool.
1014              
1015             Upon error, this will set an L<error object|Net::API::CPAN::Exception> and returns C<undef> in scalar context and an empty list in list context.
1016              
1017             =head2 request
1018              
1019             Sets or gets the original L<HTTP request object|HTTP::Promise::Request> that was used for this list object.
1020              
1021             =head2 shift
1022              
1023             my $obj = $list->shift;
1024              
1025             This removes the first entry from the data pool, thus altering it, and returns it.
1026              
1027             If the value to be returned is undefined, it will return C<undef> in scalar context and an empty list in list context, and it will return a L<null object|Module::Generic::Null> in object context to prevent a perl error of some methods called with an undefined value. The null object virtual method call will return C<undef> eventually.
1028              
1029             For example, let's assume the list is empty. Calling C<shift> would return an C<undef> value, but since it is called in object context, it returns a L<null object|Module::Generic::Null> instead.
1030              
1031             my $undef = $list->shift->author;
1032              
1033             =head2 size
1034              
1035             Sets or gets the size of each page. Normally, this is figured out automatically by L<load_data|/load_data>, but sometimes, when large chunk of data are returning at once, and you want to break it down, setting the C<size> makes it possible.
1036              
1037             This returns a L<number object|Module::Generic::Number>
1038              
1039             =head2 size_prop
1040              
1041             Sets or gets the property name in the data containing the size of the data set. Typically this is C<size> and this is the default value.
1042              
1043             This returns a L<scalar object|Module::Generic::Scalar>
1044              
1045             =head2 timed_out
1046              
1047             Sets or gets the boolean value returned from the last API call to MetaCPAN and representing whether the query has timed out.
1048              
1049             =head2 took
1050              
1051             Sets or gets a number returned from the last API call to MetaCPAN. If set, this will return a L<number object|Module::Generic::Number>, otherwise C<undef>
1052              
1053             =head2 total
1054              
1055             Sets or gets the overall size of the data available from MetaCPAN API, possibly across multiple API calls.
1056              
1057             For example, a query might result in data of 120 elements found, and each page with 10 elements. C<total> would then be 120.
1058              
1059             It returns a L<number object|Module::Generic::Number>.
1060              
1061             See also L</length>, which returns the current size of the data at hands stored in C<items>
1062              
1063             =head2 type
1064              
1065             Sets or gets the underlying object type of the data pool. This can be C<author>, C<changes>, C<cover>, C<distribution>, C<favorite>, C<file>, C<mirror>, C<module>, C<package>, C<permission>, C<rating>, C<release>, but is not enforced, so whatever value you set is your responsibility.
1066              
1067             =head2 unshift
1068              
1069             $list->unshift( $object );
1070              
1071             Provided with a proper object value, and this will add it at the beginning of the data pool and returns the current list object for chaining purposes.
1072              
1073             A valid value must be an object of the same type as the ones used in this data pool.
1074              
1075             Upon error, this will set an L<error object|Net::API::CPAN::Exception> and returns C<undef> in scalar context and an empty list in list context.
1076              
1077             =head1 AUTHOR
1078              
1079             Jacques Deguest E<lt>F<jack@deguest.jp>E<gt>
1080              
1081             =head1 SEE ALSO
1082              
1083             L<Net::API::CPAN>, L<Net::API::CPAN::Scroll>, L<Net::API::CPAN::Filter>, L<Net::API::CPAN::Exception>, L<HTTP::Promise>
1084              
1085             =head1 COPYRIGHT & LICENSE
1086              
1087             Copyright(c) 2023 DEGUEST Pte. Ltd.
1088              
1089             All rights reserved
1090              
1091             This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself.
1092              
1093             =cut