File Coverage

blib/lib/Crypto/API.pm
Criterion Covered Total %
statement 88 145 60.6
branch 34 74 45.9
condition 5 21 23.8
subroutine 10 13 76.9
pod 0 3 0.0
total 137 256 53.5


line stmt bran cond sub pod time code
1             package Crypto::API;
2             $Crypto::API::VERSION = '0.04';
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 4     4   120699 use Moo;
  4         29438  
  4         19  
61 4     4   5731 use URI::Escape qw( uri_escape );
  4         5075  
  4         237  
62 4     4   1948 use Digest::SHA qw( hmac_sha256_hex );
  4         10915  
  4         290  
63 4     4   1667 use MIME::Base64 qw( encode_base64 );
  4         2143  
  4         192  
64 4     4   1901 use HTTP::API::Client;
  4         390478  
  4         5441  
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(@_) );
76             }
77              
78             our $AUTOLOAD = '';
79              
80             sub AUTOLOAD {
81 9     9   18463 my ($self, @args) = @_;
82              
83 9         35 my ($function) = reverse split /::/, $AUTOLOAD;
84              
85 9 100       57 if (!$self->can("set_$function")) {
86 1         16 die "Can't call method '$function'";
87             }
88              
89 8         33 return $self->_call_function(func => $function, @args);
90             }
91              
92             sub _call_function {
93 8     8   22 my ($self, %o) = @_;
94              
95             my $function = delete $o{func}
96 8 50       25 or die "What is the function name??";
97              
98 8         16 my $route_spec_func = "set_$function";
99              
100 8         24 my $route = $self->$route_spec_func;
101              
102 8         180 my ($req_spec, $resp_spec) = @$route{qw(request response)};
103              
104 8 100       23 if (!$req_spec) {
105 1         12 die 'Missing request';
106             }
107              
108 7 100       25 if (!$resp_spec) {
109 1         10 die 'Missing response';
110             }
111              
112 6         19 my ($method, $path, $data, $headers, $events) = @$req_spec{qw{
113             method path data headers events
114             }};
115              
116 6 100       21 if (!$method) {
117 1         12 die 'Missing method';
118             }
119              
120 5 100       35 if (!$path) {
121 1         11 die 'Missing path or URL';
122             }
123              
124 4         5 my %mapped_data = ();
125              
126 4   50     19 $data ||= {};
127              
128 4         8 $events->{not_include} = {};
129              
130 4         17 while (my ($my_alias, $setting) = each %$data) {
131 4         7 my ($to_exchange, $required, $default, $include);
132              
133 4 100       12 if (ref $setting eq 'HASH') {
134             $to_exchange = $setting->{field_name}
135 3 50       7 or die "Missing setting: field_name";
136 3         7 ($required, $default, $include) = @$setting{qw(required default include)};
137             }
138             else {
139 1         2 $to_exchange = $setting;
140             }
141              
142 4   50     30 $include ||= '';
143              
144 4         7 my $value = $o{$my_alias};
145              
146 4 100       9 if (!defined $value) {
147 2 100       4 if ($required) {
    50          
148 1         16 die "Missing argument: $my_alias";
149             }
150             elsif ($default) {
151 1 50       4 if (ref $default eq 'CODE') {
152 0         0 $value = $self->$default($my_alias, $setting);
153             }
154             else {
155 1         3 $value = $default;
156             }
157             }
158             }
159              
160 3         6 my $format = "request_attr_$my_alias";
161              
162 3 50       13 if ($self->can($format)) {
163 3         9 $value = $self->$format($value);
164             }
165              
166 3 50 33     42 if (defined($value) || $include eq 'always') {
167 3         13 $mapped_data{$to_exchange} = $value;
168             }
169             else {
170 0         0 $events->{not_include}{$to_exchange} = 1;
171             }
172             }
173              
174 3 50       9 if (my $code = $events->{keys}) {
175 0         0 my @events_keys;
176              
177 0 0       0 if (ref $code eq 'CODE') {
    0          
178 0         0 @events_keys = $code->();
179             }
180             elsif (ref $code eq 'ARRAY') {
181 0         0 @events_keys = @$code;
182             }
183             else {
184 0         0 die "Expected keys is either CODE REF|ARRAY REF";
185             }
186              
187 0         0 my @mapped_keys = ();
188              
189 0         0 foreach my $my_alias(@events_keys) {
190 0   0     0 my $setting = $data->{$my_alias} || $my_alias;
191              
192 0 0       0 if (ref $setting eq 'HASH') {
193 0         0 push @mapped_keys, $setting->{field_name};
194             }
195             else {
196 0         0 push @mapped_keys, $setting;
197             }
198             }
199              
200 0     0   0 $events->{keys} = sub { @mapped_keys };
  0         0  
201             }
202              
203 3   50     23 my $debug = $self->$method($path, \%mapped_data, $headers ||= {}, $events ||= {});
      50        
204              
205 3 100       11946 if ($events->{test_request_object}) {
206 2         22 return $debug;
207             }
208              
209 1         4 my $resp = $self->json_response;
210              
211 1 50       8 if (my $key = $resp_spec->{key}) {
212 1         2 $resp = $resp->{$key};
213             }
214              
215 1         2 my $row_spec = $resp_spec->{row};
216              
217             my $response_attr = sub {
218 1     1   2 my ($row) = @_;
219 1         1 my %mapped_row;
220 1 50       1 my @other_keys = @{$row_spec->{_others} || []};
  1         4  
221 1         5 while (my ($my_alias, $from_exchange) = each %$row_spec) {
222 3 100       19 next if $my_alias =~ m/^_/;
223 2         6 my $attr = $row->{$from_exchange};
224 2         4 my $attr_func = "response_attr_$my_alias";
225 2 100       11 if ($self->can($attr_func)) {
226 1         4 $attr = $self->$attr_func($attr);
227             }
228 2         16 $mapped_row{$my_alias} = $attr;
229             }
230 1         3 foreach my $key(@other_keys) {
231 2         3 my $attr = $row->{$key};
232 2         5 my $attr_func = "response_attr_$key";
233 2 50       10 if ($self->can($attr_func)) {
234 0         0 $attr = $self->$attr_func($attr);
235             }
236 2         6 $mapped_row{_others}{$key} = $attr;
237             }
238 1         5 return %mapped_row;
239 1         6 };
240              
241 1 50       3 if (ref $resp eq 'ARRAY') {
242 0         0 my @mapped_rows;
243 0         0 foreach my $row(@$resp) {
244 0         0 my %mapped_row = $response_attr->($row);
245 0 0       0 if (my $filter = $resp_spec->{row_filter}) {
246 0   0     0 my $action = $self->$filter(\%mapped_row) || '';
247 0 0 0     0 if ($action && $action !~ m/^(next|last)$/) {
248 0         0 die "Row Filter returns expected either 'next' or 'last' or '' or undef";
249             }
250 0 0       0 if ($action eq 'next') {
    0          
251 0         0 next;
252             }
253             elsif ($action eq 'last') {
254 0         0 last;
255             }
256             }
257 0         0 push @mapped_rows, \%mapped_row;
258             }
259              
260 0 0       0 if (my $sort = $resp_spec->{sort}) {
261 0         0 @mapped_rows = sort { $self->$sort($a, $b) } @mapped_rows;
  0         0  
262             }
263              
264 0 0       0 if (my $primary_key = $resp_spec->{array2hash}) {
    0          
    0          
265 0         0 my %mapped_rows = map { $_->{$primary_key} => $_ } @mapped_rows;
  0         0  
266 0 0       0 if (my $code = $resp_spec->{post_row}) {
267 0         0 map { $self->$code($_, \%mapped_rows) } @mapped_rows;
  0         0  
268             }
269 0         0 return \%mapped_rows;
270             }
271             elsif (my $pri_key = $resp_spec->{'array2[hash]'}) {
272 0         0 my %mapped_rows = ();
273 0         0 foreach my $row(@mapped_rows) {
274 0   0     0 push @{$mapped_rows{$row->{$pri_key}} ||= []}, $row;
  0         0  
275             }
276              
277 0 0       0 if (my $sort = $resp_spec->{'array2[hash.sort]'}) {
278 0         0 foreach my $list(values %mapped_rows) {
279 0         0 @$list = sort { $self->$sort($a, $b) } @$list;
  0         0  
280             }
281             }
282              
283 0 0       0 if (my $code = $resp_spec->{post_row}) {
284 0         0 map { $self->$code($_, \%mapped_rows) } @mapped_rows;
  0         0  
285             }
286 0         0 return \%mapped_rows;
287             }
288             elsif (my $code = $resp_spec->{post_row}) {
289 0         0 map { $self->$code($_, \@mapped_rows) } @mapped_rows;
  0         0  
290             }
291              
292 0         0 return \@mapped_rows;
293             }
294             else {
295 1         3 my %mapped_row = $response_attr->($resp);
296              
297 1 50       5 if (my $code = $resp_spec->{post_row}) {
298 0         0 $self->$code(\%mapped_row);
299             }
300              
301 1         14 return \%mapped_row;
302             }
303             }
304              
305       3 0   sub DEMOLISH {}
306              
307 4     4   41 no Moo;
  4         17  
  4         31  
308              
309             1;