File Coverage

blib/lib/Net/HTTP/Knork.pm
Criterion Covered Total %
statement 118 128 92.1
branch 21 32 65.6
condition 8 13 61.5
subroutine 23 25 92.0
pod 1 7 14.2
total 171 205 83.4


line stmt bran cond sub pod time code
1             package Net::HTTP::Knork;
2              
3             # ABSTRACT: Lightweight implementation of Spore specification
4 6     6   442743 use Moo;
  6         68840  
  6         30  
5 6     6   9765 use Sub::Install;
  6         7734  
  6         37  
6 6     6   18205 use Try::Tiny;
  6         1831  
  6         308  
7 6     6   28 use Carp;
  6         10  
  6         241  
8 6     6   3076 use JSON;
  6         47268  
  6         30  
9 6     6   3589 use Data::Rx;
  6         85857  
  6         173  
10 6     6   15618 use LWP::UserAgent;
  6         227340  
  6         163  
11 6     6   29 use URI;
  6         7  
  6         124  
12 6     6   3070 use File::ShareDir ':ALL';
  6         32437  
  6         1073  
13 6     6   3065 use Subclass::Of;
  6         89086  
  6         35  
14 6     6   3667 use Net::HTTP::Knork::Request;
  6         22  
  6         204  
15 6     6   43 use Net::HTTP::Knork::Response;
  6         9  
  6         7906  
16              
17             with 'Net::HTTP::Knork::Role::Middleware';
18              
19              
20             has 'client' => ( is => 'lazy', );
21              
22             # option that allows one to pass optional parameters that are not specified
23             # in the spore 'optional_params' section for a given method
24              
25             has 'lax_optionals' => ( is => 'rw', default => sub {0} );
26              
27             has 'base_url' => (
28             is => 'rw',
29             lazy => 1,
30             builder => sub {
31 8     8   2720 return $_[0]->spec->{base_url};
32             }
33             );
34              
35             has 'request' => (
36             is => 'rw',
37             lazy => 1,
38             clearer => 1,
39             builder => sub {
40 8     8   2018 return Net::HTTP::Knork::Request->new( $_[0]->env );
41             }
42             );
43              
44             has 'env' => ( is => 'rw', );
45              
46             has 'spec' => (
47             is => 'lazy',
48             required => 1,
49             coerce => sub {
50             my $json_spec = $_[0];
51             my $spec;
52              
53             # it could be a file
54             try {
55             open my $fh, '<', $json_spec or croak 'Cannot read the spec file';
56             local $/ = undef;
57             binmode $fh;
58             $spec = from_json(<$fh>);
59             close $fh;
60             }
61             catch {
62             try {
63             $spec = from_json($json_spec);
64             }
65              
66             # it is not json, so we are returning the string as is
67             catch {
68             $spec = $json_spec;
69             };
70             };
71             return $spec;
72             }
73             );
74              
75             has 'default_params' => (
76             is => 'rw',
77             default => sub { {} },
78             predicate => 1,
79             clearer => 1,
80             writer => 'set_default_params',
81             );
82              
83             has 'spore_rx' => (
84             is => 'rw',
85             default => sub {
86             return dist_file(
87             'Net-HTTP-Knork',
88             'config/specs/spore_validation.rx'
89             );
90             }
91             );
92              
93             has 'http_options' => (
94             is => 'rw',
95             default => sub { {} },
96             );
97              
98             # Change the namespace of a given instance, so that there won't be any
99             # method collision between two instances
100              
101             sub BUILD {
102 9     9 0 132 my $self = shift;
103 9         104 my $subclass = subclass_of('Net::HTTP::Knork');
104 9         15680 bless( $self, $subclass );
105 9         60 $self->build_from_spec();
106             }
107              
108             sub _build_client {
109 0     0   0 my $self = shift;
110 0         0 return LWP::UserAgent->new( %{ $self->http_options } );
  0         0  
111             }
112              
113             sub validate_spore {
114 9     9 0 23 my ( $self, $spec ) = @_;
115 9         76 my $rx = Data::Rx->new;
116 9         99134 my $spore_schema;
117 9 50       384 if ( -f $self->spore_rx ) {
118 9         297 open my $fh, "<", $self->spore_rx;
119 9         26 binmode $fh;
120 9         47 local $/ = undef;
121 9         190 $spore_schema = <$fh>;
122 9         103 close $fh;
123             }
124             else {
125 0         0 croak "Spore schema " . $self->spore_rx . " could not be found";
126             }
127 9         50 my $json_schema = from_json($spore_schema);
128 9         553 my $schema = $rx->make_schema($json_schema);
129             try {
130 9     9   371 my $valid = $schema->assert_valid($spec);
131             }
132             catch {
133 0     0   0 croak "Spore specification is invalid, please fix it\n" . $_;
134 9         15611 };
135             }
136              
137             # take a spec and instanciate methods that matches those
138              
139             sub build_from_spec {
140 9     9 0 18 my $self = shift;
141 9         115 my $spec = $self->spec;
142              
143 9         3115 $self->validate_spore($spec);
144 9         11454 my $base_url = $self->base_url;
145 9 50       716 croak
146             'We need a base URL, either in the spec or as a parameter to build_from_spec'
147             unless $base_url;
148 9         58 $self->build_methods();
149             }
150              
151             sub build_methods {
152 9     9 0 17 my $self = shift;
153 9         15 foreach my $method ( keys %{ $self->spec->{methods} } ) {
  9         182  
154 63         3676 my $sub_from_spec =
155             $self->make_sub_from_spec( $self->spec->{methods}->{$method} );
156 63         258 Sub::Install::install_sub(
157             { code => $sub_from_spec,
158             into => ref($self),
159             as => $method,
160             }
161             );
162             }
163             }
164              
165             sub make_sub_from_spec {
166 64     64 1 883 my $reg = shift;
167 64         63 my $meth_spec = shift;
168             return sub {
169 9     9   4604 my $self = shift;
170 9         156 $self->clear_request;
171              
172 9         1904 my %param_spec;
173 9 100 66     76 if (scalar @_ == 1 and ref($_[0]) eq 'HASH') {
174 4         9 %param_spec = %{ $_[0] };
  4         19  
175             }
176             else {
177 5         13 %param_spec = @_;
178             }
179              
180 9 100       67 if ( $self->has_default_params ) {
181 8         12 foreach my $d_param ( keys( %{ $self->default_params } ) ) {
  8         56  
182 1         4 $param_spec{$d_param} = $self->default_params->{$d_param};
183             }
184             }
185 9         20 my %method_args = %{$meth_spec};
  9         41  
186 9         22 my $method = $method_args{method};
187 9 50       36 my $payload =
188             ( defined $param_spec{spore_payload} )
189             ? delete $param_spec{spore_payload}
190             : delete $param_spec{payload};
191              
192 9 50 33     36 if ( $method_args{required_payload} && !$payload ) {
193 0         0 croak "this method requires a payload and no payload is provided";
194             }
195 9 50 66     62 if ( $payload
196             && ( $method !~ /^(?:POST|PUT|PATCH)$/i ) )
197             {
198 0         0 croak "payload requires a PUT, PATCH or POST method";
199             }
200              
201 9   100     44 $payload //= undef;
202              
203 9 100       29 if ( $method_args{required_params} ) {
204 4         7 foreach my $required ( @{ $method_args{required_params} } ) {
  4         15  
205 4 100       13 if ( !grep { $required eq $_ } keys %param_spec ) {
  3         19  
206 1         21 croak
207             "Parameter '$required' is marked as required but is missing";
208             }
209             }
210             }
211              
212 8         13 my $params;
213 8         14 foreach ( @{ $method_args{required_params} } ) {
  8         25  
214 3         11 push @$params, $_, delete $param_spec{$_};
215             }
216              
217 8         16 foreach ( @{ $method_args{optional_params} } ) {
  8         24  
218 3 50       14 push @$params, $_, delete $param_spec{$_}
219             if ( defined( $param_spec{$_} ) );
220             }
221 8 50       25 if (%param_spec) {
222 0 0       0 if ( $self->lax_optionals ) {
223 0         0 foreach ( keys %param_spec ) {
224 0         0 push @$params, $_, delete $param_spec{$_};
225             }
226             }
227             }
228              
229 8 100       208 my $base_url =
230             ( exists $method_args{base_url} )
231             ? $method_args{base_url}
232             : $self->base_url;
233 8         106 $base_url = URI->new( $base_url );
234 8 50 50     32591 my $env = {
235             REQUEST_METHOD => $method,
236             SERVER_NAME => $base_url->host,
237             SERVER_PORT => $base_url->port,
238             SCRIPT_NAME => (
239             $base_url->path eq '/'
240             ? ''
241             : $base_url->path
242             ),
243             PATH_INFO => $method_args{path},
244             REQUEST_URI => '',
245             QUERY_STRING => '',
246             HTTP_USER_AGENT => $self->client->agent // '',
247              
248             'spore.params' => $params,
249             'spore.payload' => $payload,
250             'spore.errors' => *STDERR,
251             'spore.url_scheme' => $base_url->scheme,
252             'spore.userinfo' => $base_url->userinfo,
253              
254             };
255 8         4358 $self->env($env);
256 8         184 my $request = $self->request->finalize();
257 8         147 my $raw_response = $self->perform_request($request);
258 8         1424 return $self->generate_response($raw_response);
259 64         326 };
260             }
261              
262              
263             sub perform_request {
264 8     8 0 11 my $self = shift;
265 8         16 my $request = shift;
266 8         230 return $self->client->request($request);
267             }
268              
269             sub generate_response {
270 8     8 0 17 my $self = shift;
271 8         16 my $raw_response = shift;
272 8         12 my $orig_response = shift;
273 8         259 my $knork_response = $self->request->new_response(
274             $raw_response->code, $raw_response->message, $raw_response->headers,
275             $raw_response->content
276             );
277 8 100       33 if ( defined($orig_response) ) {
278 2 50       18 $knork_response->raw_body( $orig_response->content )
279             unless defined( ( $knork_response->raw_body ) );
280             }
281 8         105 return $knork_response;
282             }
283              
284              
285              
286             1;
287              
288             __END__