File Coverage

blib/lib/WWW/Google/Translate.pm
Criterion Covered Total %
statement 124 176 70.4
branch 32 82 39.0
condition 10 26 38.4
subroutine 18 18 100.0
pod 3 4 75.0
total 187 306 61.1


line stmt bran cond sub pod time code
1             package WWW::Google::Translate;
2              
3             our $VERSION = '0.08';
4              
5 1     1   15604 use strict;
  1         2  
  1         30  
6 1     1   4 use warnings;
  1         1  
  1         20  
7             {
8 1     1   3 use Carp;
  1         4  
  1         63  
9 1     1   2685 use URI;
  1         5297  
  1         22  
10 1     1   5 use File::Spec;
  1         1  
  1         17  
11 1     1   641 use JSON qw( from_json );
  1         10310  
  1         3  
12 1     1   2890 use LWP::UserAgent;
  1         31709  
  1         30  
13 1     1   7 use HTTP::Status qw( HTTP_BAD_REQUEST );
  1         1  
  1         142  
14 1     1   493 use Readonly;
  1         2293  
  1         49  
15 1     1   517 use English qw( -no_match_vars $EVAL_ERROR $OS_ERROR );
  1         3011  
  1         9  
16 1     1   2639 use Data::Dumper;
  1         5467  
  1         1641  
17             }
18              
19             my ( $REST_HOST, $REST_URL, $CONSOLE_URL, %SIZE_LIMIT_FOR, $TEMP_FILE );
20             {
21             Readonly $REST_HOST => 'www.googleapis.com';
22             Readonly $REST_URL => "https://$REST_HOST/language/translate/v2";
23             Readonly $CONSOLE_URL => "https://code.google.com/apis/console";
24             Readonly %SIZE_LIMIT_FOR => (
25             translate => 2000, # google states 2K but observed results vary
26             detect => 2000,
27             languages => 9999, # N/A
28             );
29             Readonly $TEMP_FILE => 'www-google-translate.dat';
30             }
31              
32             sub new {
33 3     3 1 64 my ( $class, $param_rh ) = @_;
34              
35 3         39 my %self = (
36             key => 0,
37             format => 0,
38             prettyprint => 0,
39             default_source => 0,
40             default_target => 0,
41             data_format => 'perl',
42             timeout => 60,
43             force_post => 0,
44             rest_url => $REST_URL,
45             agent => ( sprintf '%s/%s', __PACKAGE__, $VERSION ),
46             cache_results => 0,
47             headers => {},
48             );
49              
50 3         28 for my $property ( keys %self ) {
51              
52 36 100       55 if ( exists $param_rh->{$property} ) {
53              
54 3   50     14 my $type = ref $param_rh->{$property} || 'String';
55 3   50     13 my $expected_type = ref $self{$property} || 'String';
56              
57 3 50       8 croak "$property should be a $expected_type"
58             if $expected_type ne $type;
59              
60 3         7 $self{$property} = delete $param_rh->{$property};
61             }
62             }
63              
64 3         5 for my $property ( keys %{$param_rh} ) {
  3         8  
65              
66 0         0 carp "$property is not a supported parameter";
67             }
68              
69 3         7 for my $default (qw( cache_results default_source default_target )) {
70              
71 9 50       16 if ( !$self{$default} ) {
72              
73 9         13 delete $self{$default};
74             }
75             }
76              
77 3 50       8 if ( exists $self{cache_results} ) {
78              
79 0         0 my $tmpdir = File::Spec->tmpdir();
80              
81 0 0       0 if ($tmpdir) {
82              
83 0         0 $self{cache_rh} = {};
84 0         0 $self{cache_file} = File::Spec->catfile( $tmpdir, $TEMP_FILE );
85              
86 0 0       0 if ( stat $self{cache_file} ) {
87              
88 0 0       0 croak $self{cache_file}, ' is not writable'
89             if !-w $self{cache_file};
90              
91 0 0       0 croak $self{cache_file}, ' is not readable'
92             if !-r $self{cache_file};
93              
94 0         0 $self{cache_rh} = do $self{cache_rh};
95             }
96             }
97             else {
98              
99 0         0 carp 'unable to find a writable temp directory';
100             }
101             }
102              
103 3 50       8 croak "key is a required parameter"
104             if !$self{key};
105              
106 3 50       17 croak "data_format must either be Perl or JSON"
107             if $self{data_format} !~ m{\A (?: perl|json ) \z}xmsi;
108              
109 3         19 $self{ua} = LWP::UserAgent->new();
110 3         3317 $self{ua}->agent( delete $self{agent} );
111              
112 3 50       113 if ( keys %{ $self{headers} } )
  3         13  
113             {
114 0         0 $self{ua}->default_header( %{ $self{headers} } );
  0         0  
115             }
116              
117 3         12 return bless \%self, $class;
118             }
119              
120             sub translate {
121 1     1 1 6 my ( $self, $arg_rh ) = @_;
122              
123 1 50       4 croak 'q is a required parameter'
124             if !exists $arg_rh->{q};
125              
126 1         2 my $result;
127              
128 1 50       3 if ( $arg_rh->{q} ) {
129              
130 1   33     3 $arg_rh->{source} ||= $self->{default_source};
131 1   33     3 $arg_rh->{target} ||= $self->{default_target};
132              
133 1         5 $self->{default_source} = $arg_rh->{source};
134 1         15 $self->{default_target} = $arg_rh->{target};
135              
136 1         6 my %is_supported = (
137             format => 1,
138             prettyprint => 1,
139             q => 1,
140             source => 1,
141             target => 1,
142             );
143              
144 5         8 my @unsupported = grep { !exists $is_supported{$_} }
  1         3  
145 1         2 keys %{$arg_rh};
146              
147 1 50       4 croak "unsupported parameters: ", ( join ',', @unsupported )
148             if @unsupported;
149              
150 1 50       4 if ( !exists $arg_rh->{prettyprint} ) {
151              
152 0 0       0 if ( $self->{prettyprint} ) {
153              
154 0         0 $arg_rh->{prettyprint} = $self->{prettyprint};
155             }
156             }
157              
158 1 50       3 if ( !exists $arg_rh->{format} ) {
159              
160 0 0       0 if ( $self->{format} ) {
    0          
161              
162 0         0 $arg_rh->{format} = $self->{format};
163             }
164             elsif ( $arg_rh->{q} =~ m{ < [^>]+ > }xms ) {
165              
166 0         0 $arg_rh->{format} = 'html';
167             }
168             else {
169              
170 0         0 $arg_rh->{format} = 'text';
171             }
172             }
173              
174 1         2 my $cache_key;
175              
176 1 50       4 if ( exists $self->{cache_rh} ) {
177              
178 0         0 $cache_key
179             = join ',',
180 0         0 map { $arg_rh->{$_} }
181 0         0 sort grep { exists $arg_rh->{$_} }
182             keys %is_supported;
183              
184 0 0       0 return $self->{cache_rh}->{$cache_key}
185             if exists $self->{cache_rh}->{$cache_key};
186             }
187              
188 1         4 $result = $self->_rest( 'translate', $arg_rh );
189              
190 1 50       5 if ($cache_key) {
191              
192 0         0 $self->{cache_rh}->{$cache_key} = $result;
193              
194 0         0 my $count = keys %{ $self->{cache_rh} };
  0         0  
195              
196 0 0       0 if ( $count % 10 == 0 ) {
197              
198 0         0 $self->_store_cache();
199             }
200             }
201             }
202              
203 1         4 return $result;
204             }
205              
206             sub languages {
207 1     1 0 16 my ( $self, $arg_rh ) = @_;
208              
209 1 50       4 croak 'target is a required parameter'
210             if !exists $arg_rh->{target};
211              
212 1         2 my $result;
213              
214 1 50       4 if ( $arg_rh->{target} ) {
215              
216 1         2 my @unsupported = grep { $_ ne 'target' } keys %{$arg_rh};
  1         4  
  1         2  
217              
218 1 50       3 croak "unsupported parameters: ", ( join ',', @unsupported )
219             if @unsupported;
220              
221 1         3 $result = $self->_rest( 'languages', $arg_rh );
222             }
223              
224 1         2 return $result;
225             }
226              
227             sub detect {
228 1     1 1 6 my ( $self, $arg_rh ) = @_;
229              
230 1 50       4 croak 'q is a required parameter'
231             if !exists $arg_rh->{q};
232              
233 1         1 my $result;
234              
235 1 50       2 if ( $arg_rh->{q} ) {
236              
237 1         2 my @unsupported = grep { $_ ne 'q' } keys %{$arg_rh};
  1         5  
  1         6  
238              
239 1 50       3 croak "unsupported parameters: ", ( join ',', @unsupported )
240             if @unsupported;
241              
242 1         2 $result = $self->_rest( 'detect', $arg_rh );
243             }
244              
245 1         2 return $result;
246             }
247              
248             sub _rest {
249 3     3   4 my ( $self, $operation, $arg_rh ) = @_;
250              
251 3 100       12 my $url
252             = $operation eq 'translate'
253             ? $self->{rest_url}
254             : $self->{rest_url} . "/$operation";
255              
256 3         5 my $force_post = $self->{force_post};
257              
258 3         10 my %form = (
259             key => $self->{key},
260 3         4 %{$arg_rh},
261             );
262              
263 3 50 66     13 if ( exists $arg_rh->{source} && !$arg_rh->{source} ) {
264              
265 0         0 delete $form{source};
266 0         0 delete $arg_rh->{source};
267             }
268              
269 3 100       8 my $byte_size = exists $form{q} ? length $form{q} : 0;
270 3         16 my $get_size_limit = $SIZE_LIMIT_FOR{$operation};
271              
272 3         20 my ( $method, $response );
273              
274 3 50 33     13 if ( $force_post || $byte_size > $get_size_limit ) {
275              
276 0         0 $method = 'POST';
277              
278 0         0 $response = $self->{ua}->post(
279             $url,
280             'X-HTTP-Method-Override' => 'GET',
281             'Content' => \%form
282             );
283             }
284             else {
285              
286 3         4 $method = 'GET';
287              
288 3         11 my $uri = URI->new($url);
289              
290 3         7335 $uri->query_form( \%form );
291              
292 3         358 $response = $self->{ua}->get($uri);
293             }
294              
295 3   50     24 my $json = $response->content() || "";
296              
297 3         13 my ($message) = $json =~ m{ "message" \s* : \s* "( [^"]+ )" }xms;
298              
299 3   33     13 $message ||= $response->status_line();
300              
301 3 50       12 if ( $response->code() == HTTP_BAD_REQUEST ) {
    50          
302              
303 0         0 my $dump = join ",\n", map {"$_ => $arg_rh->{$_}"} keys %{$arg_rh};
  0         0  
  0         0  
304              
305 0         0 warn "request failed: $dump\n";
306              
307 0         0 require Sys::Hostname;
308              
309 0   0     0 my $host = Sys::Hostname::hostname() || 'this machine';
310 0         0 $host = uc $host;
311              
312 0         0 die "unsuccessful $operation $method for $byte_size bytes: ",
313             $message,
314             "\n",
315             "check that $host is has API Access for this API key",
316             "\n",
317             "at $CONSOLE_URL\n";
318             }
319             elsif ( !$response->is_success() ) {
320              
321 0         0 croak "unsuccessful $operation $method ",
322             "for $byte_size bytes, message: $message\n";
323             }
324              
325 3 50       30 return $json
326             if 'json' eq lc $self->{data_format};
327              
328 3         3 $json =~ s{ NaN }{-1}xmsg; # prevent from_json failure
329              
330 3         4 my $trans_rh;
331              
332 3         3 eval { $trans_rh = from_json( $json, { utf8 => 1 } ); };
  3         14  
333              
334 3 50       176 if ($EVAL_ERROR) {
335 0         0 warn "$json\n$EVAL_ERROR";
336 0         0 return $json;
337             }
338              
339 3         24 return $trans_rh;
340             }
341              
342             sub _store_cache {
343 3     3   3 my ($self) = @_;
344              
345             return
346 3 50 33     11 if !exists $self->{cache_rh} || !exists $self->{cache_file};
347              
348 0         0 my $fh;
349              
350 0 0       0 open $fh, '>', $self->{cache_file}
351             or die 'open ', $self->{cache_file}, ": $OS_ERROR";
352              
353 0         0 local $Data::Dumper::Terse = 1;
354 0         0 local $Data::Dumper::Indent = 1;
355 0         0 local $Data::Dumper::Quotekeys = 0;
356 0         0 local $Data::Dumper::Sortkeys = 1;
357              
358 0 0       0 print {$fh} Dumper( $self->{cache_rh} )
  0         0  
359             or die 'print ', $self->{cache_file}, ": $OS_ERROR";
360              
361 0 0       0 close $fh
362             or die 'close ', $self->{cache_file}, ": $OS_ERROR";
363              
364 0         0 return 1;
365             }
366              
367             sub DESTROY {
368 3     3   2229 my ($self) = @_;
369              
370 3         8 $self->_store_cache();
371              
372 3         85 return;
373             }
374              
375             1;