File Coverage

lib/Finance/Robinhood/Utilities/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::Utilities::Iterator;
2              
3             =encoding utf-8
4              
5             =for stopwords th
6              
7             =head1 NAME
8              
9             Finance::Robinhood::Utilities::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_003';
24 1     1   7 use Mojo::Base-base, -signatures;
  1         1  
  1         5  
25 1     1   150 use Mojo::URL;
  1         2  
  1         4  
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         2  
  1         2  
50 1   33     4 $s->_next_page($s->_first_page // $s->_next_page);
51 1         15 $s->_current(());
52 1         6 $s->_results([]);
53             }
54              
55             sub _test_reset {
56 1     1   31680 my $rh = t::Utility::rh_instance(0);
57 1         17 my $instruments = $rh->equity_instruments;
58 1         230 isa_ok($instruments, __PACKAGE__);
59 1         292 my $next = $instruments->next;
60 1         7 $instruments->take(300);
61 1         9 $instruments->reset;
62 1         590 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 2453 sub current($s) {
  21         50  
  21         35  
73 21   100     89 $s->_current // $s->next;
74 21         75 $s->_current;
75             }
76              
77             sub _test_current {
78 1     1   3397 my $rh = t::Utility::rh_instance(0);
79 1         17 my $instruments = $rh->equity_instruments;
80 1         247 isa_ok($instruments, __PACKAGE__);
81 1         310 my $next = $instruments->next;
82 1         7 isa_ok($instruments->current, 'Finance::Robinhood::Equity::Instrument');
83 1         471 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 13373     13373 1 16331 sub next($s) {
  13373         16429  
  13373         14986  
94 13373         24129 $s->_check_next_page;
95 13373         17873 my ($retval, @values) = @{$s->_results};
  13373         26615  
96 13373         238242 $s->_results(\@values);
97 13373         178670 $s->_current($retval);
98 13373         70802 $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 2 sub peek ($s, $pos = 1) {
  1         3  
  1         2  
  1         3  
111 1         6 $s->_check_next_page($pos);
112 1         14 $s->_results->[$pos - 1];
113             }
114              
115             sub _test_peek {
116 1     1   32125 my $rh = t::Utility::rh_instance(0);
117 1         17 my $instruments = $rh->equity_instruments;
118 1         237 isa_ok($instruments, __PACKAGE__);
119 1         293 my $peek = $instruments->peek;
120 1         14 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 13367     13367 1 17300 sub has_next ($s, $pos = 1) {
  13367         16208  
  13367         16377  
  13367         14320  
131 13367         28860 $s->_check_next_page($pos);
132 13367         30070 !!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 335 sub take ($s, $count = 1) {
  18         42  
  18         48  
  18         40  
144 18         57 $s->_check_next_page($count);
145 18         432 my @retval;
146 18 100       146 for (1 .. $count) { push @retval, $s->next; last if !$s->has_next }
  13343         86918  
  13343         23285  
147 18         251 $s->_current($retval[-1]);
148 18         5799 @retval;
149             }
150              
151             sub _test_take {
152 1     1   25500 my $rh = t::Utility::rh_instance(0);
153 1         13 my $instruments = $rh->equity_instruments;
154 1         191 isa_ok($instruments, __PACKAGE__);
155             {
156 1         5 my @take = $instruments->take(3);
157 1         8 is(3, scalar @take, '...take(3) returns 3 items');
158             }
159             {
160 1         233 my @take = $instruments->take(300);
  1         673  
  1         4  
161 1         11 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 875 sub all($s) {
  4         11  
  4         10  
172 4         9 my @retval;
173 4   50     20 push @retval, $s->take($s->count // 1000) until !$s->has_next;
174 4         43 $s->_current($retval[-1]);
175 4         2639 @retval;
176             }
177              
178             sub _test_all_and_has_next {
179 1     1   11887 my $rh = t::Utility::rh_instance(0); # Do not log in!
180 1         14 my $instruments = $rh->equity_instruments;
181 1         246 isa_ok($instruments, __PACKAGE__);
182 1         296 diag('Grabbing all instruments... please hold...');
183 1         545 my @take = $instruments->all;
184 1         75 cmp_ok(11000, '<=', scalar(@take), sprintf '...all() returns %d items',
185             scalar @take);
186 1         481 isnt($instruments->has_next, !!1,
187             '...has_next() works at the end of the list');
188             }
189              
190             # Check if we need to slurp the next page of elements to fill a position
191 26759     26759   30032 sub _check_next_page ($s, $count = 1) {
  26759         29775  
  26759         30965  
  26759         30386  
192 26759         29686 my @push = @{$s->_results};
  26759         42775  
193 26759         423654 my $pre = scalar @push;
194 26759   100     54348 $s->_first_page // $s->_first_page($s->_next_page);
195 26759   100     182464 while (($count > scalar @push) && defined $s->_next_page) {
196 168         9800 my $res = $s->_rh->_get($s->_next_page);
197              
198             #use Data::Dump;
199             #ddx $res;
200             #ddx $res->json;
201             #die;
202 168 50       6378 if ($res->is_success) {
203 168         3959 my $json = $res->json;
204 168         13812899 $s->_next_page($json->{next});
205             push @push, map {
206             defined $_
207             ? defined $s->_class
208 16436 50       880642 ? do {
    100          
209 16435         68355 eval 'require ' . $s->_class;
210 16435         91660 $s->_class->new(_rh => $s->_rh, %$_);
211             }
212             : $_
213             : ()
214 168         2231 } @{$json->{results}};
  168         901  
215             }
216             else { # Trouble! Let's not try another page
217 0         0 $s->_next_page(undef);
218             }
219             }
220 26759 100       313116 $s->_results(\@push) if scalar @push > $pre;
221             }
222              
223             sub _test_check_next_page {
224 1     1   54292 my $rh = t::Utility::rh_instance(0); # Do not log in!
225 1         17 is($rh->equity_instruments_by_id('c7d4323d-9512-4b15-977a-7cb2d1381d00'),
226             ()); # Fake id
227             }
228              
229             =head1 LEGAL
230              
231             This is a simple wrapper around the API used in the official apps. The author
232             provides no investment, legal, or tax advice and is not responsible for any
233             damages incurred while using this software. This software is not affiliated
234             with Robinhood Financial LLC in any way.
235              
236             For Robinhood's terms and disclosures, please see their website at
237             https://robinhood.com/legal/
238              
239             =head1 LICENSE
240              
241             Copyright (C) Sanko Robinson.
242              
243             This library is free software; you can redistribute it and/or modify it under
244             the terms found in the Artistic License 2. Other copyrights, terms, and
245             conditions may apply to data transmitted through this module. Please refer to
246             the L section.
247              
248             =head1 AUTHOR
249              
250             Sanko Robinson Esanko@cpan.orgE
251              
252             =cut
253              
254             1;