File Coverage

blib/lib/Stancer/Core/Iterator.pm
Criterion Covered Total %
statement 142 142 100.0
branch 48 48 100.0
condition 29 30 96.6
subroutine 26 26 100.0
pod 4 4 100.0
total 249 250 99.6


line stmt bran cond sub pod time code
1             package Stancer::Core::Iterator;
2              
3 15     15   398525 use 5.020;
  15         60  
4 15     15   92 use strict;
  15         36  
  15         389  
5 15     15   124 use warnings;
  15         52  
  15         1287  
6              
7             # ABSTRACT: Abstract API object iterator
8             our $VERSION = '1.0.3'; # VERSION
9              
10 15     15   122 use Carp qw(croak);
  15         45  
  15         1143  
11 15     15   5091 use Stancer::Core::Request;
  15         52  
  15         637  
12 15     15   8954 use Stancer::Exceptions::InvalidSearchCreation;
  15         71  
  15         709  
13 15     15   152 use Stancer::Exceptions::InvalidSearchFilter;
  15         41  
  15         459  
14 15     15   8577 use Stancer::Exceptions::InvalidSearchLimit;
  15         67  
  15         768  
15 15     15   8990 use Stancer::Exceptions::InvalidSearchStart;
  15         88  
  15         688  
16 15     15   8727 use Stancer::Exceptions::InvalidSearchUntilCreation;
  15         71  
  15         970  
17 15     15   301 use JSON;
  15         61  
  15         152  
18 15     15   2751 use Scalar::Util qw(blessed);
  15         36  
  15         2590  
19 15     15   110 use Try::Tiny;
  15         110  
  15         1130  
20              
21 15     15   98 use Moo;
  15         33  
  15         121  
22 15     15   8064 use namespace::clean;
  15         33  
  15         103  
23              
24              
25       3     sub _create_element {
26             # Must be implemented in other iterator classes
27             }
28              
29       1     sub _element_key {
30             # Must be implemented in other iterator classes
31             }
32              
33              
34             sub new {
35 36     36 1 1239365 my ($class, $sub) = @_;
36 36         259 my $self = {
37             callback => $sub,
38             stop => 0,
39             };
40              
41 36         544 return bless $self, $class;
42             }
43              
44              
45             sub end {
46 1     1 1 1156 my $this = shift;
47              
48 1         4 $this->{stop} = 1;
49              
50 1         6 return $this;
51             }
52              
53              
54             sub next { ## no critic (Subroutines::ProhibitBuiltinHomonyms)
55 52     52 1 138721 my $this = shift;
56              
57 52 100       364 if ($this->{stop}) {
58 1         4 $this->{stop} = 0;
59              
60 1         6 return undef;
61             }
62              
63 51         228 return $this->{callback}();
64             }
65              
66              
67             sub search {
68 35     35 1 3047 my ($class, @args) = @_;
69 35         235 my $obj = $class->_create_element(); # Mandatory for requests
70 35         459 my $request = Stancer::Core::Request->new();
71 35         921 my $more = 1;
72 35         132 my @elements = ();
73 35         130 my $data;
74              
75 35 100       206 if (scalar @args == 1) {
76 34         107 $data = $args[0];
77             } else {
78 1         4 $data = {@args};
79             }
80              
81 35 100       88 if (not keys %{$data}) {
  35         177  
82 4         34 my $message = 'Invalid search filters.';
83              
84 4         57 Stancer::Exceptions::InvalidSearchFilter->throw(message => $message);
85             }
86              
87 31 100       159 if (not defined $data->{start}) {
88 27         145 $data->{start} = 0;
89             }
90              
91 31         133 my $created_until = $data->{created_until};
92              
93 31         185 delete $data->{created_until};
94              
95             my $sub = sub {
96 45 100 100 45   373 if (scalar @elements == 0 && $more) {
97 21         63 my $response;
98              
99             try {
100 21         2361 $response = $request->get($obj, $data);
101             }
102             catch {
103 5 100 100     474 return if blessed($_) && $_->isa('Stancer::Exceptions::Http::NotFound');
104              
105 4 100 100     141 $_->throw() if blessed($_) && $_->can('does') && $_->does('Throwable');
      100        
106              
107 3         183 croak $_;
108 21         503 };
109              
110 17 100       2220 if ($response) {
111 12         1380 my $json = decode_json $response;
112              
113 12         182 $more = $json->{range}->{has_more} == JSON::true;
114 12         428 @elements = @{$json->{$class->_element_key}};
  12         83  
115              
116 12         86 $data->{start} += $json->{range}->{limit};
117             }
118             }
119              
120 41         124 my $value = shift @elements;
121              
122 41 100       144 if (defined $value) {
123 34         191 my $item = $class->_create_element($value);
124              
125 34 100 66     1130 return if defined $created_until && defined $item->created && $item->created->epoch > $created_until;
      100        
126 26         1631 return $item;
127             }
128              
129 7         99 return;
130 31         563 };
131              
132 31         289 return $class->new($sub);
133             }
134              
135             sub _search_filter_created {
136 36     36   148 my ($class, $data) = @_;
137 36         129 my $created = $data->{created};
138 36         112 my $blessed = blessed $data->{created};
139              
140 36 100 100     320 if (defined $blessed && $blessed eq 'DateTime') {
141 2         39 $created = $data->{created}->epoch();
142             }
143              
144 36 100 100     298 if (defined $blessed && $blessed eq 'DateTime::Span') {
145 12         112 $created = $data->{created}->start->epoch();
146 12         1197 $data->{created_until} = $data->{created}->end->epoch();
147              
148 12 100       804 if ($data->{created}->start_is_open) {
149 4         68 $created += 1;
150             }
151              
152 12 100       173 if ($data->{created}->end_is_open) {
153 4         65 $data->{created_until} -= 1;
154             }
155             }
156              
157 36 100       655 if ($created !~ m/^\d+$/sm) {
158 4         26 my $message = 'Created must be a position integer or a DateTime object.';
159              
160 4         30 Stancer::Exceptions::InvalidSearchCreation->throw(message => $message);
161             }
162              
163 32 100       231 if ($created > time) {
164 4         28 my $message = 'Created must be in the past.';
165              
166 4         195 Stancer::Exceptions::InvalidSearchCreation->throw(message => $message);
167             }
168              
169 28         217 return $created;
170             }
171              
172             sub _search_filter_created_until {
173 22     22   88 my ($class, $data) = @_;
174 22         70 my $created_until = $data->{created_until};
175 22         108 my $blessed = blessed $data->{created_until};
176              
177 22 100 100     131 if (defined $blessed && $blessed eq 'DateTime') {
178 2         44 $created_until = $data->{created_until}->epoch();
179             }
180              
181 22 100       297 if ($created_until !~ m/^\d+$/sm) {
182 4         27 my $message = 'Created until must be a position integer or a DateTime object.';
183              
184 4         27 Stancer::Exceptions::InvalidSearchUntilCreation->throw(message => $message);
185             }
186              
187 18 100       124 if ($created_until > time) {
188 4         15 my $message = 'Created until must be in the past.';
189              
190 4         113 Stancer::Exceptions::InvalidSearchUntilCreation->throw(message => $message);
191             }
192              
193 14 100       92 if (defined $data->{created}) {
194 12         72 my $created = $class->_search_filter_created($data);
195              
196 12 100       72 if ($created_until < $created) {
197 2         6 my $message = 'Created until can not be before created.';
198              
199 2         16 Stancer::Exceptions::InvalidSearchUntilCreation->throw(message => $message);
200             }
201             }
202              
203 12         730 return $created_until;
204             }
205              
206             sub _search_filter_limit {
207 10     10   28 my ($class, $data) = @_;
208              
209 10 100       97 if ($data->{limit} !~ m/^\d+$/sm) {
210 2         17 my $message = 'Limit must be an integer.';
211              
212 2         17 Stancer::Exceptions::InvalidSearchLimit->throw(message => $message);
213             }
214              
215 8 100 100     110 if ($data->{limit} < 1 || $data->{limit} > 100) {
216 4         15 my $message = 'Limit must be between 1 and 100.';
217              
218 4         71 Stancer::Exceptions::InvalidSearchLimit->throw(message => $message);
219             }
220              
221 4         20 return $data->{limit};
222             }
223              
224             sub _search_filter_start {
225 8     8   26 my ($class, $data) = @_;
226              
227 8 100       142 if ($data->{start} !~ m/^\d+$/sm) {
228 4         23 my $message = 'Start must be a positive integer.';
229              
230 4         79 Stancer::Exceptions::InvalidSearchStart->throw(message => $message);
231             }
232              
233 4         22 return $data->{start};
234             }
235              
236             1;
237              
238             __END__
239              
240             =pod
241              
242             =encoding UTF-8
243              
244             =head1 NAME
245              
246             Stancer::Core::Iterator - Abstract API object iterator
247              
248             =head1 VERSION
249              
250             version 1.0.3
251              
252             =head1 DESCRIPTION
253              
254             You should not use this class directly.
255              
256             This module is an internal class, regrouping method for every API object list.
257              
258             =head1 METHODS
259              
260             =head2 C<< Stancer::Core::Iterator->new(I<$sub>) : I<self> >>
261              
262             Create a new iterator.
263              
264             A subroutine, C<$sub> is mandatory, it will be used on every iteration.
265              
266             =head2 C<< $iterator->end() : I<self> >>
267              
268             Stop the current iterator.
269              
270             =head2 C<< $iterator->next() : I<mixed> >>
271              
272             Return the next element or C<undef> if no more element to iterate.
273              
274             =head2 C<< Stancer::Core::Iterator->search(I<%terms>) : I<self> >>
275              
276             =head2 C<< Stancer::Core::Iterator->search(I<\%terms>) : I<self> >>
277              
278             Search element on the API with some terms.
279              
280             I<%terms> (or I<\%terms>) are mandatory but accepted values will not be listed here, this method is internaly used
281             by L<Stancer::Payment/list> and L<Stancer::Dispute/list>.
282              
283             =head1 USAGE
284              
285             =head2 Logging
286              
287              
288              
289             We use the L<Log::Any> framework for logging events.
290             You may tell where it should log using any available L<Log::Any::Adapter> module.
291              
292             For example, to log everything to a file you just have to add a line to your script, like this:
293             #! /usr/bin/env perl
294             use Log::Any::Adapter (File => '/var/log/payment.log');
295             use Stancer::Core::Iterator;
296              
297             You must import C<Log::Any::Adapter> before our libraries, to initialize the logger instance before use.
298              
299             You can choose your log level on import directly:
300             use Log::Any::Adapter (File => '/var/log/payment.log', log_level => 'info');
301              
302             Read the L<Log::Any> documentation to know what other options you have.
303              
304             =cut
305              
306             =head1 SECURITY
307              
308             =over
309              
310             =item *
311              
312             Never, never, NEVER register a card or a bank account number in your database.
313              
314             =item *
315              
316             Always uses HTTPS in card/SEPA in communication.
317              
318             =item *
319              
320             Our API will never give you a complete card/SEPA number, only the last four digits.
321             If you need to keep track, use these last four digit.
322              
323             =back
324              
325             =cut
326              
327             =head1 BUGS
328              
329             Please report any bugs or feature requests on the bugtracker website
330             L<https://gitlab.com/wearestancer/library/lib-perl/-/issues> or by email to
331             L<bug-stancer@rt.cpan.org|mailto:bug-stancer@rt.cpan.org>.
332              
333             When submitting a bug or request, please include a test-file or a
334             patch to an existing test-file that illustrates the bug or desired
335             feature.
336              
337             =head1 AUTHOR
338              
339             Joel Da Silva <jdasilva@cpan.org>
340              
341             =head1 COPYRIGHT AND LICENSE
342              
343             This software is Copyright (c) 2018-2024 by Stancer / Iliad78.
344              
345             This is free software, licensed under:
346              
347             The Artistic License 2.0 (GPL Compatible)
348              
349             =cut