File Coverage

blib/lib/Crypto/API.pm
Criterion Covered Total %
statement 150 218 68.8
branch 83 148 56.0
condition 14 47 29.7
subroutine 14 18 77.7
pod 0 3 0.0
total 261 434 60.1


line stmt bran cond sub pod time code
1             package Crypto::API;
2             $Crypto::API::VERSION = '0.06';
3             =head1 NAME
4              
5             Crypto::API - Universal Plug & Play API
6              
7             =head1 USAGE
8              
9             This module mainly used by Inheritance
10              
11             package Exchange {
12             use Moo;
13             extends 'Crypto::API';
14             }
15              
16             You can use this module as parent and the child class
17             can simply define the api spec.
18              
19             package foo {
20             use Moo;
21             extends 'Crypto::API';
22              
23             sub _build_base_url {
24             URI->new('https://api.kucoin.com');
25             }
26              
27             sub set_prices {{
28             request => {
29             method => 'get',
30             path => '/api/v1/market/stats',
31             data => {
32             pair => 'symbol',
33             },
34             },
35             response => {
36             key => 'data',
37             row => {
38             pair => 'symbol',
39             last_price => 'last',
40             },
41             },
42             }}
43             }
44              
45             The main purpose of this is to normalise the request and response
46             for different exchanges that using this API as a standard.
47              
48             So if you call price data from Binance and Kucoin or etc ...
49              
50             There will be no different.
51              
52             $binance->prices(pair => 'XRP-USDC') -> getting { pair => 'XRP-USDC', last_price => 1234 };
53              
54             OR
55              
56             $kucoin->prices(pair => 'XRP-USDC') -> getting { pair => 'XRP-USDC', last_price => 1234 };
57              
58             =cut
59              
60 9     9   503066 use Moo;
  9         101490  
  9         47  
61 9     9   17683 use URI::Escape qw( uri_escape );
  9         13820  
  9         617  
62 9     9   5765 use Digest::SHA qw( hmac_sha256_hex hmac_sha256 );
  9         29957  
  9         847  
63 9     9   4731 use MIME::Base64 qw( encode_base64 );
  9         7405  
  9         627  
64 9     9   5044 use HTTP::API::Client;
  9         1105501  
  9         27640  
65              
66             extends 'HTTP::API::Client';
67              
68             sub do_hmac_sha256_hex {
69 0     0 0 0 my ($self, $str, $secret) = @_;
70 0         0 return hmac_sha256_hex($str, $secret);
71             }
72              
73             sub do_hmac_sha256_base64 {
74 0     0 0 0 my ($self, $str, $secret) = @_;
75 0         0 return encode_base64( hmac_sha256($str, $secret), '' );
76             }
77              
78             our $AUTOLOAD = '';
79              
80             sub AUTOLOAD {
81 12     12   37894 my ($self, @args) = @_;
82              
83 12         59 my ($function) = reverse split /::/, $AUTOLOAD;
84              
85 12 100       94 if (!$self->can("set_$function")) {
86 1         25 die "Can't call method '$function'";
87             }
88              
89 11         56 return $self->_call_function(func => $function, @args);
90             }
91              
92             sub _call_function {
93 11     11   47 my ($self, %o) = @_;
94              
95             my $function = delete $o{func}
96 11 50       42 or die "What is the function name??";
97              
98 11         27 my $route_spec_func = "set_$function";
99              
100 11   66     59 my $route = delete($o{spec}) // $self->$route_spec_func;
101              
102 11         160 my ($req_spec, $resp_spec) = @$route{qw(request response)};
103              
104 11 100       31 if (!$req_spec) {
105 1         13 die 'Missing request';
106             }
107              
108 10 100       38 if (!$resp_spec) {
109 1         14 die 'Missing response';
110             }
111              
112 9         41 my ($method, $path, $data, $headers, $events) = @$req_spec{qw{
113             method path data headers events
114             }};
115              
116 9 100       34 if (!$method) {
117 1         13 die 'Missing method';
118             }
119              
120 8 100       73 if (!$path) {
121 1         13 die 'Missing path or URL';
122             }
123              
124 7         19 my %mapped_data = ();
125              
126 7   100     44 $data ||= {};
127              
128 7         18 $events->{not_include} = {};
129              
130 7         38 while (my ($my_alias, $setting) = each %$data) {
131 5         13 my ($to_exchange, $type, $required, $default, $include, $checker);
132              
133 5 100       19 if (ref $setting eq 'HASH') {
134             $to_exchange = $setting->{field_name}
135 4 50       12 or die "Missing setting: field_name";
136             ($type, $required, $default, $include, $checker) =
137 4         13 @$setting{qw(type required default include checker)};
138             }
139             else {
140 1         2 $to_exchange = $setting;
141             }
142              
143 5   50     38 $include ||= '';
144              
145 5         12 my $value = $o{$my_alias};
146              
147 5 100       15 if (!defined $value) {
148 2 100       5 if ($default) {
149 1 50       5 if (ref $default eq 'CODE') {
150 0         0 $value = $self->$default($my_alias, $setting);
151             }
152             else {
153 1         3 $value = $default;
154             }
155             }
156 2 100 66     8 if ($required && !defined $value) {
157 1         19 die "Missing argument: $my_alias";
158             }
159             }
160              
161 4         11 my $format = "request_attr_$my_alias";
162              
163 4 100       27 if ($self->can($format)) {
164 3         11 $value = $self->$format($value);
165             }
166              
167 4 50       52 if ($type) {
168 0 0       0 if (ref $type eq 'ARRAY') {
    0          
    0          
    0          
169 0 0 0     0 if (defined($value) && !grep {ref ? $value =~ /$_/ : $value eq $_} @$type) {
  0 0       0  
170 0         0 die "the value is not in $my_alias enum list";
171             }
172             }
173             elsif ($type =~ m/int/i) {
174 0 0 0     0 if (defined($value) && $value !~ /^[\d\.]+$/) {
175 0         0 die "$my_alias should be integer";
176             }
177             }
178             elsif ($type =~ m/bool/i) {
179 0 0 0     0 if (defined($value) && $value !~ /^[01]$/) {
180 0         0 die "$my_alias should be boolean";
181             }
182             }
183             elsif ($type =~ m/hex/i) {
184 0 0 0     0 if (defined($value) && $value !~ /^[0-9a0-f]$/i) {
185 0         0 die "$my_alias should be hexadecimal";
186             }
187             }
188             }
189              
190 4 100       14 if ($checker) {
191 1         2 foreach my $c(@$checker) {
192             my $code = $c->{ok}
193 1 50       3 or die "$my_alias checker missing ok";
194              
195             my $err = $c->{err}
196 1 50       4 or die "$my_alias is missing err";
197              
198 1         1 local $_ = $value;
199              
200 1 50       3 if (!$code->()) {
201 1         27 die "$my_alias $err";
202             }
203             }
204             }
205              
206 3 50 33     10 if (defined($value) || $include eq 'always') {
207 3 50       10 if (ref $value eq 'HASH') {
208 0         0 my @keys = split /,/, $to_exchange;
209 0         0 @mapped_data{@keys} = @$value{@keys};
210             }
211             else {
212 3         16 $mapped_data{$to_exchange} = $value;
213             }
214             }
215             else {
216 0         0 $events->{not_include}{$to_exchange} = 1;
217             }
218             }
219              
220 5 50       17 if (my $code = $events->{keys}) {
221 0         0 my @events_keys;
222              
223 0 0       0 if (ref $code eq 'CODE') {
    0          
224 0         0 @events_keys = $code->();
225             }
226             elsif (ref $code eq 'ARRAY') {
227 0         0 @events_keys = @$code;
228             }
229             else {
230 0         0 die "Expected keys is either CODE REF|ARRAY REF";
231             }
232              
233 0         0 my @mapped_keys = ();
234              
235 0         0 foreach my $my_alias(@events_keys) {
236 0   0     0 my $setting = $data->{$my_alias} || $my_alias;
237              
238 0 0       0 if (ref $setting eq 'HASH') {
239 0         0 push @mapped_keys, split /,/, $setting->{field_name};
240             }
241             else {
242 0         0 push @mapped_keys, $setting;
243             }
244             }
245              
246 0     0   0 $events->{keys} = sub { @mapped_keys };
  0         0  
247             }
248              
249 5   50     47 my $debug = $self->$method($path, \%mapped_data, $headers ||= {}, $events ||= {});
      50        
250              
251 5 100       15885 if ($events->{test_request_object}) {
252 2         23 return $debug;
253             }
254              
255 3 50       9 if ($events->{test_response_object}) {
256 0         0 return $debug;
257             }
258              
259 3         20 return $self->_process_response(
260             $self->json_response,
261             $resp_spec,
262             request => {
263             method => $method,
264             path => $path,
265             data => $data,
266             headers => $headers,
267             events => $events,
268             }
269             );
270             }
271              
272             sub _process_response {
273 3     3   49 my ($self, $response, $resp_specs, %options) = @_;
274              
275 3         8 my ($method, $path, $data, $headers, $events) = @{$options{request}}{qw(
  3         11  
276             method path data headers events)};
277              
278 3 100       12 $resp_specs = [$resp_specs] if ref $resp_specs ne 'ARRAY';
279              
280 3         6 my @result = ();
281              
282 3         9 SPEC: foreach my $resp_spec(@$resp_specs) {
283 5         14 my $resp = _get($response, $resp_spec->{key});
284              
285 5 50       18 if (my $code = $resp_spec->{raw_process}) {
286 0         0 push @result, $self->$code(
287             request => {
288             method => $method,
289             path => $path,
290             data => $data,
291             headers => $headers,
292             event => $events
293             },
294             response => $resp,
295             );
296 0         0 next SPEC;
297             }
298              
299 5 50       22 if (!ref $resp) {
    100          
300 0         0 push @result, $resp;
301 0         0 next SPEC;
302             }
303             elsif (ref $resp eq 'HASH') {
304 3         14 my %mapped_row = $self->_map_response_attr($resp, row_spec => $resp_spec->{row});
305              
306 3 50       12 if (my $code = $resp_spec->{post_row}) {
307 0         0 $self->$code(\%mapped_row, $resp);
308             }
309              
310 3         9 push @result, \%mapped_row;
311 3         9 next SPEC;
312             }
313              
314 2         3 my @mapped_rows;
315             my %mapped_rows;
316              
317 2         5 ROW: foreach my $row(@$resp) {
318 4         12 my %mapped_row = $self->_map_response_attr($row, row_spec => $resp_spec->{row});
319              
320 4 50       11 if (my $code = $resp_spec->{post_row}) {
321 0         0 $self->$code(\%mapped_row);
322             }
323              
324 4 50       9 if (my $filter = $resp_spec->{row_filter}) {
325 0   0     0 my $action = $self->$filter(\%mapped_row, $row) || '';
326 0 0 0     0 if ($action && $action !~ m/^(next|last)$/) {
327 0         0 die "Row Filter returns expected either 'next' or 'last' or '' or undef";
328             }
329 0 0       0 if ($action eq 'next') {
    0          
330 0         0 next ROW;
331             }
332             elsif ($action eq 'last') {
333 0         0 last ROW;
334             }
335             }
336              
337 4 50       11 if (my $primary_key = $resp_spec->{array2hash}) {
    50          
338 0         0 eval _hash_key(
339             head => '$mapped_rows',
340             path => $primary_key,
341             tail => ' = \\%mapped_row',
342             source => \%mapped_row,
343             );
344             }
345             elsif (my $pri_key = $resp_spec->{'array2[hash]'}) {
346 0         0 eval _hash_key(
347             head => 'push @{$mapped_rows',
348             path => $pri_key,
349             tail => ' ||= []}, \\%mapped_row',
350             source => \%mapped_row,
351             );
352             }
353              
354 4 50       7 die $@ if $@;
355              
356 4 50       13 push @mapped_rows, \%mapped_row
357             if !%mapped_rows;
358             }
359              
360 2 50       4 if (%mapped_rows) {
361 0         0 push @result, \%mapped_rows;
362 0         0 next SPEC;
363             }
364              
365 2 50       15 if (my $csort = $resp_spec->{custom_sort}) {
    50          
366 0         0 @mapped_rows = sort { $self->$csort($a, $b) } @mapped_rows;
  0         0  
367             }
368             elsif (my $sort = $resp_spec->{sort_by}) {
369 2         7 @mapped_rows = _sort_rows($sort, @mapped_rows);
370             }
371              
372 2         9 push @result, \@mapped_rows;
373             }
374              
375 3 100       39 return wantarray ? @result : $result[0];
376             }
377              
378             sub _hash_key {
379 0     0   0 my %options = @_;
380              
381 0   0     0 my $head = $options{head} // '$_';
382 0 0       0 my $path = $options{path} or die 'Missing path';
383 0   0     0 my $tail = $options{tail} // '';
384 0         0 my $source = $options{source};
385              
386 0 0       0 if (ref $path eq 'ARRAY') {
387 0         0 my @path = @$path; ## clone
388             return sprintf '%s%s%s',
389             $head,
390 0 0       0 join('', map { $_ = _defor(_get($source, $_), '') if $source;
  0         0  
391 0         0 s/'/\\'/g; "{'$_'}" } @path),
  0         0  
392             $tail;
393             }
394              
395 0         0 $path =~ s/'/\\'/g;
396 0         0 return sprintf "%s{'%s'}%s", $head, $path, $tail;
397             }
398              
399             sub _sort_rows {
400 5     5   98 my ($sorts, @rows) = @_;
401              
402 5         10 my @sort = ();
403              
404 5         12 foreach my $sort(@$sorts) {
405 6         16 my ($way, $field) = each %$sort;
406              
407 6         18 $field =~ s/'/\\'/g;
408              
409 6 100       23 if ($way =~ m/desc/) {
    50          
410 3 100       11 if ($way =~ m/^n/) {
411 2         15 push @sort, "_defor(_get(\$b, '$field'), 0) <=> _defor(_get(\$a, '$field'), 0)";
412             }
413             else {
414 1         6 push @sort, "_defor(_get(\$b, '$field'), '') cmp _defor(_get(\$a, '$field'), '')";
415             }
416             }
417             elsif ($way =~ m/asc/) {
418 3 100       9 if ($way =~ m/^n/) {
419 1         4 push @sort, "_defor(_get(\$a, '$field'), 0) <=> _defor(_get(\$b, '$field'), 0)";
420             }
421             else {
422 2         9 push @sort, "_defor(_get(\$a, '$field'), '') cmp _defor(_get(\$b, '$field'), '')";
423             }
424             }
425             else {
426 0         0 die "Invalid sorting $sort. Only accept asc, desc, nasc and ndesc";
427             }
428             }
429              
430 5         29 my $sort = sprintf 'sort {%s} @rows', join '||', @sort;
431              
432 5 50       16 if ($ENV{DEBUG}) {
433 0         0 print "SORT: $sort\n";
434             }
435              
436 5         546 return eval $sort;
437             }
438              
439             sub _defor {
440 28     28   135 my ($default, $or) = @_;
441 28 100 66     365 return (defined($default) && length($default)) ? $default : $or;
442             }
443              
444             sub _get {
445 38     38   747 my ($data, $path) = @_;
446              
447 38 100       138 return $data->{$path}
448             if $path !~ m/\./;
449              
450 8         16 my $xpath = '';
451              
452 8         29 foreach my $item(split /\./, $path) {
453 25 100       44 if (!$item) {
454 1         12 die "Invalid path: $path";
455             }
456              
457 24         39 $xpath .= ".$item";
458              
459 24 100       58 if (ref $data eq 'HASH') {
    100          
460 21 100       44 if (!exists $data->{$item}) {
461 1         14 warn "$xpath is not exists";
462             }
463 21         671 $data = $data->{$item};
464             }
465             elsif (ref $data eq 'ARRAY') {
466 2 100       19 if (!defined $data->[$item]) {
467 1         8 warn "$xpath is not exists";
468             }
469 2         572 $data = $data->[$item];
470             }
471             else {
472 1         9 die "Path deadend $xpath";
473             }
474             }
475              
476 6         30 return $data;
477             }
478              
479             sub _map_response_attr {
480 7     7   19 my ($self, $row, %options) = @_;
481              
482 7         12 my $row_spec = $options{row_spec};
483              
484 7         10 my %mapped_row;
485              
486 7         34 while (my ($my_alias, $from_exchange) = each %$row_spec) {
487 21 100 66     79 next if $my_alias =~ m/^_/ || $from_exchange eq '[X]';
488              
489 20         25 my $attr;
490              
491 20 50       38 if ( ref $from_exchange eq 'HASH' ) {
492             my @attr = $self->_process_response( $row, $from_exchange,
493 0         0 request => $options{request} );
494 0 0       0 $attr = ref $from_exchange eq 'ARRAY' ? \@attr : $attr[0];
495             }
496             else {
497 20         31 $attr = $row->{$from_exchange};
498             }
499              
500 20 100       114 if (my $code = $self->can("response_attr_$my_alias")) {
501 1         5 $attr = $self->$code($attr, $row);
502             }
503              
504 20         82 $mapped_row{$my_alias} = $attr;
505             }
506              
507 7 100       10 foreach my $key(@{$row_spec->{_others} || []}) {
  7         28  
508 2         3 my $attr = $row->{$key};
509 2 50       19 if (my $code = $self->can("response_attr_$key")) {
510 0         0 $attr = $self->$code($attr, $row);
511             }
512 2         7 $mapped_row{_others}{$key} = $attr;
513             }
514              
515 7         46 return %mapped_row;
516             }
517              
518       5 0   sub DEMOLISH {}
519              
520 9     9   131 no Moo;
  9         39  
  9         103  
521              
522             1;