File Coverage

blib/lib/SignalWire/Agents/REST/HttpClient.pm
Criterion Covered Total %
statement 35 80 43.7
branch 2 14 14.2
condition 1 15 6.6
subroutine 12 20 60.0
pod 0 5 0.0
total 50 134 37.3


line stmt bran cond sub pod time code
1             package SignalWire::Agents::REST::HttpClient;
2 4     4   214893 use strict;
  4         12  
  4         154  
3 4     4   33 use warnings;
  4         9  
  4         250  
4 4     4   463 use Moo;
  4         7617  
  4         33  
5              
6 4     4   6310 use HTTP::Tiny;
  4         256290  
  4         257  
7 4     4   2852 use JSON qw(encode_json decode_json);
  4         52756  
  4         31  
8 4     4   2187 use MIME::Base64 qw(encode_base64);
  4         2410  
  4         3404  
9              
10             has 'project' => ( is => 'ro', required => 1 );
11             has 'token' => ( is => 'ro', required => 1 );
12             has 'host' => ( is => 'ro', required => 1 );
13             has 'base_url' => ( is => 'lazy' );
14             has '_ua' => ( is => 'lazy' );
15             has '_auth_header' => ( is => 'lazy' );
16              
17             sub _build_base_url {
18 1     1   3929 my ($self) = @_;
19 1         10 return 'https://' . $self->host;
20             }
21              
22             sub _build__ua {
23 0     0   0 my ($self) = @_;
24 0         0 return HTTP::Tiny->new(
25             agent => 'signalwire-agents-perl-rest/1.0',
26             default_headers => {
27             'Content-Type' => 'application/json',
28             'Accept' => 'application/json',
29             'Authorization' => $self->_auth_header,
30             },
31             timeout => 30,
32             );
33             }
34              
35             sub _build__auth_header {
36 1     1   1051 my ($self) = @_;
37 1         10 my $credentials = $self->project . ':' . $self->token;
38 1         10 return 'Basic ' . encode_base64($credentials, '');
39             }
40              
41             sub _request {
42 0     0     my ($self, $method, $path, %opts) = @_;
43 0           my $url = $self->base_url . $path;
44              
45             # Add query params to URL
46 0 0 0       if ($opts{params} && ref $opts{params} eq 'HASH' && %{$opts{params}}) {
  0   0        
47 0           my @pairs;
48 0           for my $key (sort keys %{$opts{params}}) {
  0            
49 0   0       my $val = $opts{params}{$key} // '';
50 0           push @pairs, _uri_encode($key) . '=' . _uri_encode($val);
51             }
52 0           $url .= '?' . join('&', @pairs);
53             }
54              
55 0           my %request_opts;
56 0 0         if ($opts{body}) {
57 0           $request_opts{content} = encode_json($opts{body});
58             }
59              
60 0           my $response = $self->_ua->request($method, $url, \%request_opts);
61              
62 0 0         unless ($response->{success}) {
63 0   0       my $body = $response->{content} // '';
64 0           my $parsed;
65 0           eval { $parsed = decode_json($body) };
  0            
66 0 0         $parsed = $body if $@;
67             die SignalWire::Agents::REST::HttpClient::Error->new(
68             status_code => $response->{status},
69 0           body => $parsed,
70             url => $path,
71             method => $method,
72             );
73             }
74              
75             # 204 No Content or empty body
76 0 0 0       if ($response->{status} == 204 || !$response->{content}) {
77 0           return {};
78             }
79              
80 0           my $result;
81 0           eval { $result = decode_json($response->{content}) };
  0            
82 0 0         if ($@) {
83 0           return { raw => $response->{content} };
84             }
85 0           return $result;
86             }
87              
88             sub get {
89 0     0 0   my ($self, $path, %opts) = @_;
90 0           return $self->_request('GET', $path, params => $opts{params});
91             }
92              
93             sub post {
94 0     0 0   my ($self, $path, %opts) = @_;
95 0           return $self->_request('POST', $path, body => $opts{body}, params => $opts{params});
96             }
97              
98             sub put {
99 0     0 0   my ($self, $path, %opts) = @_;
100 0           return $self->_request('PUT', $path, body => $opts{body});
101             }
102              
103             sub patch {
104 0     0 0   my ($self, $path, %opts) = @_;
105 0           return $self->_request('PATCH', $path, body => $opts{body});
106             }
107              
108             sub delete_request {
109 0     0 0   my ($self, $path) = @_;
110 0           return $self->_request('DELETE', $path);
111             }
112              
113             # Simple URI encoding
114             sub _uri_encode {
115 0     0     my ($str) = @_;
116 0           $str =~ s/([^A-Za-z0-9\-_.~])/sprintf("%%%02X", ord($1))/ge;
  0            
117 0           return $str;
118             }
119              
120             # --- Error class ---
121             package SignalWire::Agents::REST::HttpClient::Error;
122 4     4   33 use Moo;
  4         8  
  4         35  
123 4     4   1646 use JSON qw(encode_json);
  4         10  
  4         27  
124              
125             has 'status_code' => ( is => 'ro', required => 1 );
126             has 'body' => ( is => 'ro', default => sub { '' } );
127             has 'url' => ( is => 'ro', default => sub { '' } );
128             has 'method' => ( is => 'ro', default => sub { 'GET' } );
129              
130             use overload '""' => sub {
131 2     2   6602 my ($self) = @_;
132 2 100 50     24 my $body = ref $self->body ? encode_json($self->body) : ($self->body // '');
133 2         30 return sprintf('%s %s returned %s: %s',
134             $self->method, $self->url, $self->status_code, $body);
135 4     4   940 };
  4         8  
  4         37  
136              
137             1;