File Coverage

blib/lib/Net/Async/Hetzner/Cloud.pm
Criterion Covered Total %
statement 75 77 97.4
branch 7 8 87.5
condition 9 16 56.2
subroutine 25 25 100.0
pod 7 7 100.0
total 123 133 92.4


line stmt bran cond sub pod time code
1             package Net::Async::Hetzner::Cloud;
2              
3             # ABSTRACT: Async Hetzner Cloud API client for IO::Async
4              
5 2     2   264248 use strict;
  2         5  
  2         79  
6 2     2   11 use warnings;
  2         4  
  2         136  
7 2     2   15 use parent 'IO::Async::Notifier';
  2         3  
  2         15  
8              
9 2     2   29022 use Carp qw(croak);
  2         4  
  2         115  
10 2     2   10 use Future;
  2         3  
  2         44  
11 2     2   1061 use URI;
  2         8155  
  2         63  
12 2     2   1140 use HTTP::Request;
  2         22330  
  2         82  
13 2     2   1392 use WWW::Hetzner::Cloud;
  2         621950  
  2         95  
14 2     2   23 use WWW::Hetzner::HTTPResponse;
  2         5  
  2         2072  
15              
16             our $VERSION = '0.003';
17              
18             sub configure {
19 11     11 1 519570 my ($self, %params) = @_;
20              
21 11 100       76 $self->{token} = delete $params{token} if exists $params{token};
22 11 100       50 $self->{base_url} = delete $params{base_url} if exists $params{base_url};
23              
24 11         40 $self->SUPER::configure(%params);
25             }
26              
27             sub token {
28 10     10 1 58 my ($self) = @_;
29             $self->{token} // $ENV{HETZNER_API_TOKEN}
30 10   66     1541 // croak "No Cloud API token configured.\n\n"
      33        
31             . "Set token via:\n"
32             . " Environment: HETZNER_API_TOKEN\n"
33             . " Option: token => \$token\n\n"
34             . "Get token at: https://console.hetzner.cloud/ -> Select project -> Security -> API tokens\n";
35             }
36              
37 10   100 10 1 349 sub base_url { $_[0]->{base_url} // 'https://api.hetzner.cloud/v1' }
38              
39             # Internal: sync Cloud instance for request building and response parsing
40             sub _cloud {
41 9     9   34 my ($self) = @_;
42 9   66     52 $self->{_cloud} //= WWW::Hetzner::Cloud->new(
43             token => $self->token,
44             base_url => $self->base_url,
45             );
46             }
47              
48             # Internal: Net::Async::HTTP instance
49             sub _http {
50 15     15   33 my ($self) = @_;
51 15 50       63 unless ($self->{_http}) {
52 0         0 require Net::Async::HTTP;
53 0         0 $self->{_http} = Net::Async::HTTP->new(
54             user_agent => 'Net-Async-Hetzner/' . $VERSION,
55             max_connections_per_host => 0,
56             );
57             }
58 15         75 return $self->{_http};
59             }
60              
61             sub _add_to_loop {
62 7     7   679 my ($self, $loop) = @_;
63 7         28 $self->add_child($self->_http);
64             }
65              
66             # ============================================================================
67             # ASYNC HTTP TRANSPORT
68             # ============================================================================
69              
70             sub _do_request {
71 8     8   21 my ($self, $req) = @_;
72              
73 8         80 my $uri = URI->new($req->url);
74              
75 8         12476 my $http_req = HTTP::Request->new($req->method => $uri);
76 8         763 my $headers = $req->headers;
77 8         34 for my $key (keys %$headers) {
78 16         699 $http_req->header($key => $headers->{$key});
79             }
80 8 100       537 $http_req->content($req->content) if $req->has_content;
81              
82             return $self->_http->do_request(
83             request => $http_req,
84             )->then(sub {
85 8     8   2123 my ($response) = @_;
86 8   33     32 return Future->done(WWW::Hetzner::HTTPResponse->new(
      50        
87             status => $response->code,
88             content => $response->decoded_content // $response->content // '',
89             ));
90 8         109 });
91             }
92              
93             # ============================================================================
94             # ASYNC API METHODS - all return Futures
95             # ============================================================================
96              
97             sub get {
98 5     5 1 597 my ($self, $path, %params) = @_;
99 5         16 my $cloud = $self->_cloud;
100 5         154 my $req = $cloud->_build_request('GET', $path, %params);
101             return $self->_do_request($req)->then(sub {
102 5     5   3301 my ($response) = @_;
103 5         30 return Future->done($cloud->_parse_response($response, 'GET', $path));
104 5         1810 });
105             }
106              
107             sub post {
108 1     1 1 165 my ($self, $path, $data) = @_;
109 1         6 my $cloud = $self->_cloud;
110 1         54 my $req = $cloud->_build_request('POST', $path, body => $data);
111             return $self->_do_request($req)->then(sub {
112 1     1   399 my ($response) = @_;
113 1         8 return Future->done($cloud->_parse_response($response, 'POST', $path));
114 1         142 });
115             }
116              
117             sub put {
118 1     1 1 165 my ($self, $path, $data) = @_;
119 1         5 my $cloud = $self->_cloud;
120 1         52 my $req = $cloud->_build_request('PUT', $path, body => $data);
121             return $self->_do_request($req)->then(sub {
122 1     1   389 my ($response) = @_;
123 1         9 return Future->done($cloud->_parse_response($response, 'PUT', $path));
124 1         174 });
125             }
126              
127             sub delete {
128 1     1 1 152 my ($self, $path) = @_;
129 1         6 my $cloud = $self->_cloud;
130 1         54 my $req = $cloud->_build_request('DELETE', $path);
131             return $self->_do_request($req)->then(sub {
132 1     1   411 my ($response) = @_;
133 1         7 return Future->done($cloud->_parse_response($response, 'DELETE', $path));
134 1         151 });
135             }
136              
137             1;
138              
139             __END__