File Coverage

blib/lib/Crypto/API.pm
Criterion Covered Total %
statement 143 199 71.8
branch 76 116 65.5
condition 10 27 37.0
subroutine 13 16 81.2
pod 0 3 0.0
total 242 361 67.0


line stmt bran cond sub pod time code
1             package Crypto::API;
2             $Crypto::API::VERSION = '0.05';
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   480894 use Moo;
  9         91076  
  9         47  
61 9     9   15628 use URI::Escape qw( uri_escape );
  9         12889  
  9         585  
62 9     9   5109 use Digest::SHA qw( hmac_sha256_hex hmac_sha256 );
  9         28279  
  9         828  
63 9     9   4207 use MIME::Base64 qw( encode_base64 );
  9         6870  
  9         648  
64 9     9   4471 use HTTP::API::Client;
  9         1017236  
  9         21408  
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   34565 my ($self, @args) = @_;
82              
83 12         64 my ($function) = reverse split /::/, $AUTOLOAD;
84              
85 12 100       89 if (!$self->can("set_$function")) {
86 1         13 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   40 my ($self, %o) = @_;
94              
95             my $function = delete $o{func}
96 11 50       43 or die "What is the function name??";
97              
98 11         86 my $route_spec_func = "set_$function";
99              
100 11   66     64 my $route = delete($o{spec}) // $self->$route_spec_func;
101              
102 11         166 my ($req_spec, $resp_spec) = @$route{qw(request response)};
103              
104 11 100       32 if (!$req_spec) {
105 1         9 die 'Missing request';
106             }
107              
108 10 100       36 if (!$resp_spec) {
109 1         11 die 'Missing response';
110             }
111              
112 9         35 my ($method, $path, $data, $headers, $events) = @$req_spec{qw{
113             method path data headers events
114             }};
115              
116 9 100       28 if (!$method) {
117 1         11 die 'Missing method';
118             }
119              
120 8 100       75 if (!$path) {
121 1         10 die 'Missing path or URL';
122             }
123              
124 7         15 my %mapped_data = ();
125              
126 7   100     42 $data ||= {};
127              
128 7         21 $events->{not_include} = {};
129              
130 7         38 while (my ($my_alias, $setting) = each %$data) {
131 5         11 my ($to_exchange, $required, $default, $include, $checker);
132              
133 5 100       31 if (ref $setting eq 'HASH') {
134             $to_exchange = $setting->{field_name}
135 4 50       15 or die "Missing setting: field_name";
136             ($required, $default, $include, $checker) =
137 4         13 @$setting{qw(required default include checker)};
138             }
139             else {
140 1         1 $to_exchange = $setting;
141             }
142              
143 5   50     27 $include ||= '';
144              
145 5         10 my $value = $o{$my_alias};
146              
147 5 100       17 if (!defined $value) {
148 2 100       8 if ($required) {
    50          
149 1         17 die "Missing argument: $my_alias";
150             }
151             elsif ($default) {
152 1 50       5 if (ref $default eq 'CODE') {
153 0         0 $value = $self->$default($my_alias, $setting);
154             }
155             else {
156 1         3 $value = $default;
157             }
158             }
159             }
160              
161 4         14 my $format = "request_attr_$my_alias";
162              
163 4 100       25 if ($self->can($format)) {
164 3         12 $value = $self->$format($value);
165             }
166              
167 4 100       52 if ($checker) {
168 1         4 foreach my $c(@$checker) {
169             my $code = $c->{code}
170 1 50       4 or die "$my_alias checker missing code";
171              
172             my $err = $c->{err}
173 1 50       3 or die "$my_alias is message error message";
174              
175 1         3 local $_ = $value;
176              
177 1 50       3 if (!$code->()) {
178 1         22 die "$my_alias $err";
179             }
180             }
181             }
182              
183 3 50 33     13 if (defined($value) || $include eq 'always') {
184 3 50       11 if (ref $value eq 'HASH') {
185 0         0 my @keys = split /,/, $to_exchange;
186 0         0 @mapped_data{@keys} = @$value{@keys};
187             }
188             else {
189 3         18 $mapped_data{$to_exchange} = $value;
190             }
191             }
192             else {
193 0         0 $events->{not_include}{$to_exchange} = 1;
194             }
195             }
196              
197 5 50       18 if (my $code = $events->{keys}) {
198 0         0 my @events_keys;
199              
200 0 0       0 if (ref $code eq 'CODE') {
    0          
201 0         0 @events_keys = $code->();
202             }
203             elsif (ref $code eq 'ARRAY') {
204 0         0 @events_keys = @$code;
205             }
206             else {
207 0         0 die "Expected keys is either CODE REF|ARRAY REF";
208             }
209              
210 0         0 my @mapped_keys = ();
211              
212 0         0 foreach my $my_alias(@events_keys) {
213 0   0     0 my $setting = $data->{$my_alias} || $my_alias;
214              
215 0 0       0 if (ref $setting eq 'HASH') {
216 0         0 push @mapped_keys, split /,/, $setting->{field_name};
217             }
218             else {
219 0         0 push @mapped_keys, $setting;
220             }
221             }
222              
223 0     0   0 $events->{keys} = sub { @mapped_keys };
  0         0  
224             }
225              
226 5   50     64 my $debug = $self->$method($path, \%mapped_data, $headers ||= {}, $events ||= {});
      50        
227              
228 5 100       17281 if ($events->{test_request_object}) {
229 2         31 return $debug;
230             }
231              
232 3 50       21 if ($events->{test_response_object}) {
233 0         0 return $debug;
234             }
235              
236 3 100       12 my $resp_specs = ref $resp_spec eq 'ARRAY' ? $resp_spec : [$resp_spec];
237              
238 3         6 my @result = ();
239              
240 3         8 foreach my $resp_spec(@$resp_specs) {
241 5         19 my $resp = _get($self->json_response, $resp_spec->{key});
242              
243 5         12 my $row_spec = $resp_spec->{row};
244              
245             my $response_attr = sub {
246 7     7   15 my ($row) = @_;
247 7         14 my %mapped_row;
248 7 100       8 my @other_keys = @{$row_spec->{_others} || []};
  7         30  
249 7         32 while (my ($my_alias, $from_exchange) = each %$row_spec) {
250 21 100       52 next if $my_alias =~ m/^_/;
251 20         27 my $attr = $row->{$from_exchange};
252 20         39 my $attr_func = "response_attr_$my_alias";
253 20 100       77 if ($self->can($attr_func)) {
254 1         3 $attr = $self->$attr_func($attr, $row);
255             }
256 20         73 $mapped_row{$my_alias} = $attr;
257             }
258 7         17 foreach my $key(@other_keys) {
259 2         3 my $attr = $row->{$key};
260 2         5 my $attr_func = "response_attr_$key";
261 2 50       17 if ($self->can($attr_func)) {
262 0         0 $attr = $self->$attr_func($attr, $row);
263             }
264 2         6 $mapped_row{_others}{$key} = $attr;
265             }
266 7         33 return %mapped_row;
267 5         30 };
268              
269 5 50       39 if (my $code = $resp_spec->{raw_process}) {
270 0         0 push @result, $self->$code(
271             request => {
272             method => $method,
273             path => $path,
274             data => $data,
275             headers => $headers,
276             event => $events
277             },
278             response => $resp,
279             );
280 0         0 next;
281             }
282              
283 5 100       17 if (ref $resp eq 'ARRAY') {
284 2         4 my @mapped_rows;
285 2         5 foreach my $row(@$resp) {
286 4         8 my %mapped_row = $response_attr->($row);
287 4 50       26 if (my $filter = $resp_spec->{row_filter}) {
288 0   0     0 my $action = $self->$filter(\%mapped_row) || '';
289 0 0 0     0 if ($action && $action !~ m/^(next|last)$/) {
290 0         0 die "Row Filter returns expected either 'next' or 'last' or '' or undef";
291             }
292 0 0       0 if ($action eq 'next') {
    0          
293 0         0 next;
294             }
295             elsif ($action eq 'last') {
296 0         0 last;
297             }
298             }
299 4         12 push @mapped_rows, \%mapped_row;
300             }
301              
302 2 50       8 if (my $sort = $resp_spec->{sort}) {
303 0         0 @mapped_rows = sort { $self->$sort($a, $b) } @mapped_rows;
  0         0  
304             }
305              
306 2 50       13 if (my $primary_key = $resp_spec->{array2hash}) {
    50          
    50          
307 0         0 my %mapped_rows = map { $_->{$primary_key} => $_ } @mapped_rows;
  0         0  
308 0 0       0 if (my $code = $resp_spec->{post_row}) {
309 0         0 map { $self->$code($_, \%mapped_rows) } @mapped_rows;
  0         0  
310             }
311 0         0 push @result, \%mapped_rows;
312             }
313             elsif (my $pri_key = $resp_spec->{'array2[hash]'}) {
314 0         0 my %mapped_rows = ();
315 0         0 foreach my $row(@mapped_rows) {
316 0   0     0 push @{$mapped_rows{$row->{$pri_key}} ||= []}, $row;
  0         0  
317             }
318              
319 0 0       0 if (my $sort = $resp_spec->{'array2[hash.sort]'}) {
320 0         0 foreach my $list(values %mapped_rows) {
321 0         0 @$list = sort { $self->$sort($a, $b) } @$list;
  0         0  
322             }
323             }
324              
325 0 0       0 if (my $code = $resp_spec->{post_row}) {
326 0         0 map { $self->$code($_, \%mapped_rows) } @mapped_rows;
  0         0  
327             }
328 0         0 push @result, \%mapped_rows;
329             }
330             elsif (my $code = $resp_spec->{post_row}) {
331 0         0 map { $self->$code($_, \@mapped_rows) } @mapped_rows;
  0         0  
332             }
333              
334 2 50       6 if (my $sort = $resp_spec->{sort_by}) {
335 2         6 @mapped_rows = _sort_rows($sort, @mapped_rows);
336             }
337              
338 2         28 push @result, \@mapped_rows;
339             }
340             else {
341 3         9 my %mapped_row = $response_attr->($resp);
342              
343 3 50       12 if (my $code = $resp_spec->{post_row}) {
344 0         0 $self->$code(\%mapped_row);
345             }
346              
347 3         28 push @result, \%mapped_row;
348             }
349             }
350              
351 3 100       28 return wantarray ? @result : $result[0];
352             }
353              
354             sub _sort_rows {
355 5     5   98 my ($sorts, @rows) = @_;
356              
357 5         11 my @sort = ();
358              
359 5         12 foreach my $sort(@$sorts) {
360 6         18 my ($way, $field) = each %$sort;
361              
362 6         19 $field =~ s/'/\\'/g;
363              
364 6 100       29 if ($way =~ m/desc/) {
    50          
365 3 100       13 if ($way =~ m/^n/) {
366 2         10 push @sort, "_defor(_get(\$b, '$field'), 0) <=> _defor(_get(\$a, '$field'), 0)";
367             }
368             else {
369 1         5 push @sort, "_defor(_get(\$b, '$field'), '') cmp _defor(_get(\$a, '$field'), '')";
370             }
371             }
372             elsif ($way =~ m/asc/) {
373 3 100       8 if ($way =~ m/^n/) {
374 1         4 push @sort, "_defor(_get(\$a, '$field'), 0) <=> _defor(_get(\$b, '$field'), 0)";
375             }
376             else {
377 2         10 push @sort, "_defor(_get(\$a, '$field'), '') cmp _defor(_get(\$b, '$field'), '')";
378             }
379             }
380             else {
381 0         0 die "Invalid sorting $sort. Only accept asc, desc, nasc and ndesc";
382             }
383             }
384              
385 5         31 my $sort = sprintf 'sort {%s} @rows', join '||', @sort;
386              
387 5 50       17 if ($ENV{DEBUG}) {
388 0         0 print "SORT: $sort\n";
389             }
390              
391 5         555 return eval $sort;
392             }
393              
394             sub _defor {
395 28     28   140 my ($default, $or) = @_;
396 28 100 66     364 return (defined($default) && length($default)) ? $default : $or;
397             }
398              
399             sub _get {
400 38     38   639 my ($data, $path) = @_;
401              
402 38 100       142 return $data->{$path}
403             if $path !~ m/\./;
404              
405 8         14 my $xpath = '';
406              
407 8         26 foreach my $item(split /\./, $path) {
408 25 100       43 if (!$item) {
409 1         9 die "Invalid path: $path";
410             }
411              
412 24         31 $xpath .= ".$item";
413              
414 24 100       56 if (ref $data eq 'HASH') {
    100          
415 21 100       36 if (!exists $data->{$item}) {
416 1         9 warn "$xpath is not exists";
417             }
418 21         500 $data = $data->{$item};
419             }
420             elsif (ref $data eq 'ARRAY') {
421 2 100       16 if (!defined $data->[$item]) {
422 1         7 warn "$xpath is not exists";
423             }
424 2         449 $data = $data->[$item];
425             }
426             else {
427 1         8 die "Path deadend $xpath";
428             }
429             }
430              
431 6         25 return $data;
432             }
433              
434       5 0   sub DEMOLISH {}
435              
436 9     9   145 no Moo;
  9         88  
  9         70  
437              
438             1;