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_002';
24 1     1   6 use Mojo::Base-base, -signatures;
  1         3  
  1         6  
25 1     1   183 use Mojo::URL;
  1         3  
  1         8  
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 4 sub reset($s) {
  1         2  
  1         3  
50 1   33     5 $s->_next_page( $s->_first_page // $s->_next_page );
51 1         847 $s->_current( () );
52 1         8 $s->_results( [] );
53             }
54              
55             sub _test_reset {
56 1     1   31719 my $rh = t::Utility::rh_instance(0);
57 1         17 my $instruments = $rh->equity_instruments;
58 1         253 isa_ok( $instruments, __PACKAGE__ );
59 1         286 my $next = $instruments->next;
60 1         8 $instruments->take(300);
61 1         11 $instruments->reset;
62 1         640 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 2472 sub current($s) {
  21         53  
  21         40  
73 21   100     99 $s->_current // $s->next;
74 21         66 $s->_current;
75             }
76              
77             sub _test_current {
78 1     1   3148 my $rh = t::Utility::rh_instance(0);
79 1         13 my $instruments = $rh->equity_instruments;
80 1         195 isa_ok( $instruments, __PACKAGE__ );
81 1         289 my $next = $instruments->next;
82 1         7 isa_ok( $instruments->current, 'Finance::Robinhood::Equity::Instrument' );
83 1         460 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 16097 sub next($s) {
  13292         15380  
  13292         14331  
94 13292         26807 $s->_check_next_page;
95 13292         18361 my ( $retval, @values ) = @{ $s->_results };
  13292         24290  
96 13292         234959 $s->_results( \@values );
97 13292         174700 $s->_current($retval);
98 13292         70781 $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 5 sub peek ( $s, $pos = 1 ) {
  1         3  
  1         4  
  1         3  
111 1         7 $s->_check_next_page($pos);
112 1         13 $s->_results->[ $pos - 1 ];
113             }
114              
115             sub _test_peek {
116 1     1   31638 my $rh = t::Utility::rh_instance(0);
117 1         17 my $instruments = $rh->equity_instruments;
118 1         236 isa_ok( $instruments, __PACKAGE__ );
119 1         277 my $peek = $instruments->peek;
120 1         13 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         16545  
  13286         17256  
  13286         14505  
131 13286         26059 $s->_check_next_page($pos);
132 13286         29235 !!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 315 sub take ( $s, $count = 1 ) {
  18         37  
  18         41  
  18         33  
144 18         69 $s->_check_next_page($count);
145 18         351 my @retval;
146 18 100       95 for ( 1 .. $count ) { push @retval, $s->next; last if !$s->has_next }
  13255         80327  
  13255         27109  
147 18         268 $s->_current( $retval[-1] );
148 18         6503 @retval;
149             }
150              
151             sub _test_take {
152 1     1   30412 my $rh = t::Utility::rh_instance(0);
153 1         15 my $instruments = $rh->equity_instruments;
154 1         225 isa_ok( $instruments, __PACKAGE__ );
155             {
156 1         7 my @take = $instruments->take(3);
157 1         11 is( 3, scalar @take, '...take(3) returns 3 items' );
158             }
159             {
160 1         276 my @take = $instruments->take(300);
  1         887  
  1         5  
161 1         36 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 789 sub all($s) {
  4         14  
  4         8  
172 4         9 my @retval;
173 4   50     18 push @retval, $s->take( $s->count // 1000 ) until !$s->has_next;
174 4         61 $s->_current( $retval[-1] );
175 4         2349 @retval;
176             }
177              
178             sub _test_all_and_has_next {
179 1     1   11543 my $rh = t::Utility::rh_instance(0); # Do not log in!
180 1         15 my $instruments = $rh->equity_instruments;
181 1         270 isa_ok( $instruments, __PACKAGE__ );
182 1         338 diag('Grabbing all instruments... please hold...');
183 1         566 my @take = $instruments->all;
184 1         64 cmp_ok( 11000, '<=', scalar(@take), sprintf '...all() returns %d items', scalar @take );
185 1         410 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   30562 sub _check_next_page ( $s, $count = 1 ) {
  26597         28570  
  26597         29727  
  26597         28042  
190 26597         30865 my @push = @{ $s->_results };
  26597         42256  
191 26597         414789 my $pre = scalar @push;
192 26597   100     50498 $s->_first_page // $s->_first_page( $s->_next_page );
193 26597   100     184426 while ( ( $count > scalar @push ) && defined $s->_next_page ) {
194 167         9675 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       5734 if ( $res->is_success ) {
201 167         3318 my $json = $res->json;
202 167         13534625 $s->_next_page( $json->{next} );
203             push @push, map {
204             defined $_
205             ? defined $s->_class
206 16348 50       872661 ? do { eval 'require ' . $s->_class; $s->_class->new( _rh => $s->_rh, %$_ ) }
  16347 100       70252  
  16347         95471  
207             : $_
208             : ()
209 167         2538 } @{ $json->{results} };
  167         903  
210             }
211             else { # Trouble! Let's not try another page
212 0         0 $s->_next_page(undef);
213             }
214             }
215 26597 100       309860 $s->_results( \@push ) if scalar @push > $pre;
216             }
217              
218             sub _test_check_next_page {
219 1     1   60090 my $rh = t::Utility::rh_instance(0); # Do not log in!
220 1         14 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;