File Coverage

lib/Finance/Robinhood/Utility/Iterator.pm
Criterion Covered Total %
statement 111 112 99.1
branch 8 10 80.0
condition 9 12 75.0
subroutine 16 16 100.0
pod 7 7 100.0
total 151 157 96.1


line stmt bran cond sub pod time code
1             package Finance::Robinhood::Utility::Iterator;
2              
3             =encoding utf-8
4              
5             =for stopwords th
6              
7             =head1 NAME
8              
9             Finance::Robinhood::Utility::Iterator - Sugary Access to Paginated Data
10              
11             =head1 SYNOPSIS
12              
13             use Finance::Robinhood;
14             my $rh = t::Utility::rh_instance(1);
15             my $instruments = $rh->instruments();
16              
17             for my $instrument ($instruments->all) {
18             CORE::say $instrument->symbol;
19             }
20              
21             =cut
22              
23             our $VERSION = '0.92_001';
24 1     1   7 use Mojo::Base-base, -signatures;
  1         2  
  1         9  
25 1     1   182 use Mojo::URL;
  1         3  
  1         7  
26              
27             =head1 METHODS
28              
29             =cut
30              
31             # Destructive iterator because memory isn't free
32             # inspired by Array::Iterator and rust's Vec and IntoIter
33             has _rh => undef => weak => 1;
34             has [
35             '_current', '_next_page', '_class', '_first_page',
36              
37             # midlands returns a count with news and feed which is nice
38             'count'
39             ];
40             has _results => sub { [] };
41              
42             =head2 C
43              
44             Reset the iterator. All elements will be removed and you'll basically start
45             from scratch.
46              
47             =cut
48              
49 1     1 1 3 sub reset($s) {
  1         3  
  1         2  
50 1   33     6 $s->_next_page( $s->_first_page // $s->_next_page );
51 1         843 $s->_current( () );
52 1         6 $s->_results( [] );
53             }
54              
55             sub _test_reset {
56 1     1   31369 my $rh = t::Utility::rh_instance(0);
57 1         15 my $instruments = $rh->equity_instruments;
58 1         250 isa_ok( $instruments, __PACKAGE__ );
59 1         282 my $next = $instruments->next;
60 1         6 $instruments->take(300);
61 1         11 $instruments->reset;
62 1         760 is( $instruments->next, $next );
63             }
64              
65             =head2 C
66              
67             Get the current element without removing it from the stack. This is
68             non-destructive but will move the cursor if there is no current element.
69              
70             =cut
71              
72 21     21 1 2327 sub current($s) {
  21         42  
  21         33  
73 21   100     73 $s->_current // $s->next;
74 21         82 $s->_current;
75             }
76              
77             sub _test_current {
78 1     1   3096 my $rh = t::Utility::rh_instance(0);
79 1         15 my $instruments = $rh->equity_instruments;
80 1         228 isa_ok( $instruments, __PACKAGE__ );
81 1         288 my $next = $instruments->next;
82 1         8 isa_ok( $instruments->current, 'Finance::Robinhood::Equity::Instrument' );
83 1         476 is( $instruments->current, $next );
84             }
85              
86             =head2 C
87              
88             Gets the next element and removes it from the stack. If there are no elements
89             and all pages have been exhausted, this will return an undefined value.
90              
91             =cut
92              
93 13292     13292 1 15968 sub next($s) {
  13292         16479  
  13292         14320  
94 13292         25202 $s->_check_next_page;
95 13292         17500 my ( $retval, @values ) = @{ $s->_results };
  13292         23623  
96 13292         227644 $s->_results( \@values );
97 13292         172379 $s->_current($retval);
98 13292         69500 $retval;
99             }
100              
101             =head2 C
102              
103             my $ten_more = $list->peek(10);
104              
105             Returns the Ith element without removing it from the stack. The index is
106             optional and, by default, the first element is returned.
107              
108             =cut
109              
110 1     1 1 4 sub peek ( $s, $pos = 1 ) {
  1         3  
  1         3  
  1         3  
111 1         7 $s->_check_next_page($pos);
112 1         14 $s->_results->[ $pos - 1 ];
113             }
114              
115             sub _test_peek {
116 1     1   31252 my $rh = t::Utility::rh_instance(0);
117 1         18 my $instruments = $rh->equity_instruments;
118 1         241 isa_ok( $instruments, __PACKAGE__ );
119 1         282 my $peek = $instruments->peek;
120 1         15 is( $instruments->next, $peek );
121             }
122              
123             =head2 C
124              
125             Returns a boolean indicating whether or not we have another I elements. The
126             length is optional and checks the results for a a single element by default.
127              
128             =cut
129              
130 13286     13286 1 16787 sub has_next ( $s, $pos = 1 ) {
  13286         16815  
  13286         16226  
  13286         14461  
131 13286         28837 $s->_check_next_page($pos);
132 13286         28143 !!defined $s->_results->[ $pos - 1 ];
133             }
134              
135             =head2 C
136              
137             Removes a number of elements from the stack and returns them. By default, this
138             returns a single element.
139              
140             =cut
141              
142             # Grab a certain number of elements
143 18     18 1 326 sub take ( $s, $count = 1 ) {
  18         39  
  18         40  
  18         39  
144 18         87 $s->_check_next_page($count);
145 18         358 my @retval;
146 18 100       99 for ( 1 .. $count ) { push @retval, $s->next; last if !$s->has_next }
  13255         79759  
  13255         22379  
147 18         250 $s->_current( $retval[-1] );
148 18         6483 @retval;
149             }
150              
151             sub _test_take {
152 1     1   31740 my $rh = t::Utility::rh_instance(0);
153 1         16 my $instruments = $rh->equity_instruments;
154 1         248 isa_ok( $instruments, __PACKAGE__ );
155             {
156 1         7 my @take = $instruments->take(3);
157 1         10 is( 3, scalar @take, '...take(3) returns 3 items' );
158             }
159             {
160 1         289 my @take = $instruments->take(300);
  1         879  
  1         5  
161 1         15 is( 300, scalar @take, '...take(300) returns 300 items' );
162             }
163             }
164              
165             =head2 C
166              
167             Grabs every page and returns every element we see.
168              
169             =cut
170              
171 4     4 1 885 sub all($s) {
  4         11  
  4         9  
172 4         10 my @retval;
173 4   50     17 push @retval, $s->take( $s->count // 1000 ) until !$s->has_next;
174 4         58 $s->_current( $retval[-1] );
175 4         2821 @retval;
176             }
177              
178             sub _test_all_and_has_next {
179 1     1   11775 my $rh = t::Utility::rh_instance(0); # Do not log in!
180 1         15 my $instruments = $rh->equity_instruments;
181 1         276 isa_ok( $instruments, __PACKAGE__ );
182 1         327 diag('Grabbing all instruments... please hold...');
183 1         545 my @take = $instruments->all;
184 1         81 cmp_ok( 11000, '<=', scalar(@take), sprintf '...all() returns %d items', scalar @take );
185 1         485 isnt( $instruments->has_next, !!1, '...has_next() works at the end of the list' );
186             }
187              
188             # Check if we need to slurp the next page of elements to fill a position
189 26597     26597   31472 sub _check_next_page ( $s, $count = 1 ) {
  26597         27479  
  26597         30790  
  26597         28230  
190 26597         29713 my @push = @{ $s->_results };
  26597         40151  
191 26597         399592 my $pre = scalar @push;
192 26597   100     48629 $s->_first_page // $s->_first_page( $s->_next_page );
193 26597   100     177200 while ( ( $count > scalar @push ) && defined $s->_next_page ) {
194 167         9691 my $res = $s->_rh->_get( $s->_next_page );
195              
196             #use Data::Dump;
197             #ddx $res;
198             #ddx $res->json;
199             #die;
200 167 50       5523 if ( $res->is_success ) {
201 167         3354 my $json = $res->json;
202 167         13389594 $s->_next_page( $json->{next} );
203             push @push, map {
204             defined $_
205             ? defined $s->_class
206 16348 50       863105 ? do { eval 'require ' . $s->_class; $s->_class->new( _rh => $s->_rh, %$_ ) }
  16347 100       69068  
  16347         85019  
207             : $_
208             : ()
209 167         2082 } @{ $json->{results} };
  167         1068  
210             }
211             else { # Trouble! Let's not try another page
212 0         0 $s->_next_page(undef);
213             }
214             }
215 26597 100       299271 $s->_results( \@push ) if scalar @push > $pre;
216             }
217              
218             sub _test_check_next_page {
219 1     1   78815 my $rh = t::Utility::rh_instance(0); # Do not log in!
220 1         45 is( $rh->equity_instruments_by_id('c7d4323d-9512-4b15-977a-7cb2d1381d00'), () ); # Fake id
221             }
222              
223             =head1 LEGAL
224              
225             This is a simple wrapper around the API used in the official apps. The author
226             provides no investment, legal, or tax advice and is not responsible for any
227             damages incurred while using this software. This software is not affiliated
228             with Robinhood Financial LLC in any way.
229              
230             For Robinhood's terms and disclosures, please see their website at
231             https://robinhood.com/legal/
232              
233             =head1 LICENSE
234              
235             Copyright (C) Sanko Robinson.
236              
237             This library is free software; you can redistribute it and/or modify it under
238             the terms found in the Artistic License 2. Other copyrights, terms, and
239             conditions may apply to data transmitted through this module. Please refer to
240             the L section.
241              
242             =head1 AUTHOR
243              
244             Sanko Robinson Esanko@cpan.orgE
245              
246             =cut
247              
248             1;