File Coverage

lib/WebService/MyGengo/Client.pm
Criterion Covered Total %
statement 1 3 33.3
branch n/a
condition n/a
subroutine 1 1 100.0
pod n/a
total 2 4 50.0


line stmt bran cond sub pod time code
1             package WebService::MyGengo::Client;
2              
3 6     6   41925 use Moose;
  0            
  0            
4             use MooseX::NonMoose;
5             use namespace::autoclean;
6              
7             extends qw(WebService::MyGengo::Base);
8              
9             use LWP::UserAgent;
10             use Scalar::Util qw(blessed);
11             use URI;
12              
13             use WebService::MyGengo;
14             use WebService::MyGengo::Account;
15             use WebService::MyGengo::Job;
16              
17             use WebService::MyGengo::Comment;
18             use WebService::MyGengo::Feedback;
19             use WebService::MyGengo::Revision;
20              
21             use WebService::MyGengo::Language;
22             use WebService::MyGengo::LanguagePair;
23              
24             use WebService::MyGengo::RequestFactory;
25             use WebService::MyGengo::Response;
26             use WebService::MyGengo::Exception;
27              
28             =head1 NAME
29              
30             WebService::MyGengo::Client - Client for interacting with the myGengo API
31              
32             =head1 DESCRIPTION
33              
34             A perl library for accessing the MyGengo (L<http://mygengo.com>) API.
35              
36             =head1 SYNOPSIS
37              
38             use WebService::MyGengo::Client;
39             my $client = WebService::MyGengo::Client->new({
40             public_key => 'pubkey'
41             , private_key => 'privkey'
42             , use_sandbox => 1
43             });
44              
45             # Alternative constructor syntax
46             $client = WebService::MyGengo::Client->new('pubkey', 'privkey', $use_sandbox);
47              
48             # A WebService::MyGengo::Job
49             my $job = $client->get_job( 123 );
50              
51             # Seeing what went wrong by inspecting the `last_response`
52             unless ( $job = $client->get_job( "BLARGH!" ) ) {
53             MyApp::Exception->throw({ message => "Oops: ".$client->last_response->message });
54             }
55              
56             =head1 ATTRIBUTES
57              
58             All attributes are read-only unless otherwise specified.
59              
60             If you need a Client with different parameters, just create a new one :)
61              
62             =head2 public_key (Str)
63              
64             Your public API key.
65              
66             =cut
67             has public_key => (
68             is => 'rw'
69             , isa => 'Str'
70             , required => 1
71             , trigger => sub { shift->clear_request_factory }
72             );
73              
74             =head2 private_key (Str)
75              
76             Your private API key.
77              
78             =cut
79             has private_key => (
80             is => 'ro'
81             , isa => 'Str'
82             , required => 1
83             , trigger => sub { shift->clear_request_factory }
84             );
85              
86             =head2 use_sandbox (Bool)
87              
88             A boolean flag that determines whether to use the API sandbox or the live site.
89              
90             =cut
91             has use_sandbox => (
92             is => 'ro'
93             , isa => 'Bool'
94             , default => 1
95             , trigger => sub {
96             my ( $self, $val ) = ( shift, @_ );
97             $self->clear_request_factory;
98             my $url = $val
99             ? 'http://api.sandbox.mygengo.com/v1.1'
100             : 'http://api.mygengo.com/v1.1'
101             ;
102             $self->_set_root_uri( URI->new( $url ) );
103             }
104             );
105              
106             =head2 root_uri (L<URI>)
107              
108             The L<URI> to be used as the base for all API endpoints.
109              
110             This value is set automatically according to the L<use_sandbox> attribute.
111              
112             eg, 'http://api.sandbox.mygengo.com/v1.1'
113              
114             =cut
115             has root_uri => (
116             is => 'rw'
117             , isa => 'URI'
118             , init_arg => undef
119             , writer => '_set_root_uri'
120             );
121              
122             =head2 DEBUG (Bool)
123              
124             A read-write flag indicating whether to dump debugging information to STDERR.
125              
126             =cut
127             has DEBUG => (
128             is => 'rw'
129             , isa => 'Bool'
130             , default => 0
131             );
132              
133             =head2 _user_agent (L<LWP::UserAgent>)
134              
135             This is a semi-private attribute, as most people won't use it.
136              
137             You can use _set_user_agent to supply your own UserAgent object to be used
138             for API calls, eg L<LWPx::ParanoidAgent>.
139              
140             The agent must pass the `->isa('LWP::UserAgent')` test.
141              
142             In DEBUG mode, a request_send and request_done handler will be registered
143             with the agent to dump raw requests and responses.
144              
145             =cut
146             has _user_agent => (
147             is => 'rw'
148             , writer => '_set_user_agent' # Semi-private
149             , isa => 'LWP::UserAgent'
150             , lazy => 1
151             , builder => '_build__user_agent'
152             , init_arg => undef
153             );
154             sub _build__user_agent {
155             my ( $self ) = ( shift );
156              
157             my $ua = LWP::UserAgent->new(
158             agent => $self->_user_agent_string
159             , timeout => 30
160             , max_redirect => 5
161             );
162              
163             if ( $self->DEBUG ) {
164             $ua->add_handler("request_send", sub {
165             print STDERR "RAW REQUEST:";
166             shift->dump( maxlength => 2048 );
167             print STDERR "\n";
168             return;
169             });
170             $ua->add_handler("response_done", sub {
171             print STDERR "RAW RESPONSE:";
172             shift->dump( maxlength => 10000);
173             print STDERR "\n";
174             return;
175             });
176             }
177              
178             return $ua;
179             }
180              
181             #=head2 _user_agent_string (Str)
182             #
183             #The User-Agent string reported by the client.
184             #
185             #=cut
186             has _user_agent_string => (
187             is => 'ro'
188             , isa => 'Str'
189             , lazy => 1
190             , init_arg => undef
191             , default => sub {
192             __PACKAGE__." ".$WebService::MyGengo::VERSION
193             }
194             );
195              
196             =head2 request_factory (L<WebService::MyGengo::RequestFactory>)
197              
198             A L<WebService::MyGengo::RequestFactory> instance used to generate API requests.
199              
200             =cut
201             has request_factory => (
202             is => 'ro'
203             , isa => 'WebService::MyGengo::RequestFactory'
204             , lazy_build => 1
205             , init_arg => undef
206             );
207             sub _build_request_factory {
208             my ( $self ) = ( shift );
209              
210             return WebService::MyGengo::RequestFactory->new({
211             public_key => $self->public_key
212             , private_key => $self->private_key
213             , root_uri => $self->root_uri
214             });
215             }
216              
217             =head2 last_response (L<WebService::MyGengo::Response>)
218              
219             The last raw response object received from the API.
220              
221             =cut
222             has last_response => (
223             is => 'rw'
224             , isa => 'WebService::MyGengo::Response'
225             , init_arg => undef
226             , writer => '_set_last_response'
227             );
228              
229             =head1 METHODS
230              
231             Unless otherwise specified, all methods will:
232              
233             =over
234              
235             =item Return a true value on success
236              
237             =item Return a false value on failure
238              
239             =item Throw an exception on bad arguments
240              
241             =item Make no effort to trap exceptions from the transport layer
242              
243             =back
244              
245             You can retrieve the last raw response via the `last_response` attribute to
246             inspect any specific error conditions. See the L<SYNOPSIS>.
247              
248             =cut
249              
250             #=head2 BUILDARGS
251             #
252             #Support alternative construction syntax.
253             #
254             #=cut
255             around BUILDARGS => sub {
256             my ( $orig, $class, $args ) = ( shift, shift, @_ );
257              
258             ref($args) eq 'HASH' and return $class->$orig(@_);
259              
260             my %args;
261             @args{ qw/public_key private_key use_sandbox _user_agent_string/ }
262             = @_;
263              
264             return \%args;
265             };
266              
267             =head2 get_account( )
268              
269             Returns the L<WebService::MyGengo::Account> associated with your API keys.
270              
271             Calls L<get_account_stats> and L<get_account_balance> internally to gather
272             the parameters necessary to construct the Account object.
273              
274             =cut
275             sub get_account {
276             my ($self) = @_;
277              
278             my $stats = $self->get_account_stats();
279              
280             !( $self->last_response->is_success ) and
281             WebService::MyGengo::Exception->throw({
282             message => "Could not retrieve account stats"
283             });
284              
285             my $balance = $self->get_account_balance();
286              
287             !( $self->last_response->is_success ) and
288             WebService::MyGengo::Exception->throw({
289             message => "Could not retrieve account balance"
290             });
291              
292             my %args;
293             @args{ keys %$stats } = values %$stats;
294             @args{ keys %$balance } = values %$balance;
295              
296             return WebService::MyGengo::Account->new( \%args );
297             }
298              
299             =head2 get_account_stats( )
300              
301             Returns a reference to a hash of account statistics.
302              
303             You may find it easier to simply use L<get_account>.
304              
305             See: L<http://mygengo.com/api/developer-docs/methods/account-stats-get/>
306              
307             =cut
308             sub get_account_stats {
309             my ( $self ) = ( shift );
310             my $res = $self->_send_request('GET', '/account/stats/');
311             return $res->response_struct;
312             }
313              
314             =head2 get_account_balance( )
315              
316             Returns a reference to a hash of account balance information.
317              
318             You may find it easier to simply use L<get_account>.
319              
320             See: L<http://mygengo.com/api/developer-docs/methods/account-balance-get/>
321              
322             =cut
323             sub get_account_balance {
324             my ( $self ) = ( shift );
325             my $res = $self->_send_request('GET', '/account/balance/');
326             return $res->response_struct;
327             }
328              
329             =head2 get_job( $id, $get_comments=false?, $get_revisions=false?, $get_feedback=false? )
330              
331             Retrieves a job from myGengo with the specified id.
332              
333             If $get_comments is true, additional API calls will be made to populate
334             the Job's `comments` list.
335              
336             If $get_revisions is true, additional API calls will be made to populate
337             the Job's `revisions` list.
338              
339             If $get_feedback is true, an additional API call will be made to populate
340             the Job's `feedback` attribute.
341              
342             Returns a L<WebService::MyGengo::Job> or undef if the Job can't be found.
343              
344             See: L<http://mygengo.com/api/developer-docs/methods/translate-job-id-get/>
345              
346             =cut
347             sub get_job {
348             my ( $self, $id ) = ( shift, shift, @_ );
349              
350             my $jobs = $self->get_jobs( [$id], @_ )
351             or return undef;
352              
353             return $jobs->[0];
354             }
355              
356             =head2 get_jobs( @($id) )
357              
358             =head2 get_jobs( \@($id), $get_comments=false?, $get_revisons=false?, $get_feedback=false? )
359              
360             Retrieves the given Jobs from the API.
361              
362             The second form allows control over prefetching of comments, revisions and
363             feedback for the Jobs.
364              
365             Returns a reference to an array of L<WebService::MyGengo::Job> objects on
366             success, undef on failure. (If no results are found but the request succeeded,
367             you'll get a reference to an empty array, as expected.)
368              
369             See: L<http://mygengo.com/api/developer-docs/methods/translate-jobs-ids-get/>
370              
371             =cut
372             sub get_jobs {
373             my ( $self ) = ( shift, @_ );
374              
375             my ( $get_comments, $get_revisions, $get_feedback );
376             my @ids;
377              
378             # Support multiple calling signatures
379             if ( ref($_[0]) eq 'ARRAY' ) {
380             @ids = @{shift()};
381             ($get_comments, $get_revisions, $get_feedback) = @_;
382             }
383             else {
384             @ids = @_;
385             }
386              
387             !scalar(@ids) and WebService::MyGengo::Exception->throw({
388             message => "Cant get_jobs without an Job ids"
389             });
390              
391             my $res = $self->_send_request(
392             'GET'
393             , '/translate/jobs/'. join(",",@ids)
394             );
395              
396             $res->is_error and return undef;
397              
398             my @jobs = map { WebService::MyGengo::Job->new($_) }
399             @{$res->response_struct->{jobs}};
400              
401             foreach my $job ( @jobs ) {
402             $get_comments and
403             $job->_set_comments( $self->get_job_comments( $job ) );
404              
405             $get_revisions and
406             $job->_set_revisions( $self->get_job_revisions( $job ) );
407              
408             $get_feedback and
409             $job->_set_feedback( $self->get_job_feedback( $job ) );
410             }
411              
412             return \@jobs;
413             }
414              
415             =head2 search_jobs( $status?, $timestamp_after?, $count? )
416              
417             Searches the API for Jobs according to several optional filters.
418              
419             Available filters are:
420              
421             =over
422              
423             =item $status - Get only Jobs of status $status (default: no filter)
424              
425             Legal values: unpaid, available, pending, reviewable, approved, rejected
426             , canceled
427              
428             =item $timestamp_after - Get only Jobs created after this epoch time (default:
429             no filter)
430              
431             =item $count - Get a maximum of $count Jobs (default: no filter)
432              
433             =back
434              
435             Returns a reference to an array of L<WebService::MyGengo::Job>s on success,
436             undef on failure. (If no results are found but the request succeeded, you'll
437             get a reference to an empty array, as expected.)
438              
439             See: L<http://mygengo.com/api/developer-docs/methods/translate-jobs-get/>
440              
441             =cut
442             #todo Support wantarray?
443             #todo Support get_comments/feedback/revisions
444             sub search_jobs {
445             my ($self, $status, $timestamp_after, $count) = ( shift, @_ );
446            
447             my $res = $self->_send_request( 'GET', '/translate/jobs/', {
448             status => $status
449             , timestamp_after => $timestamp_after
450             , count => $count
451             } );
452              
453             $res->is_error and return undef;
454              
455             my @jobs;
456             push @jobs, $self->get_job( $_->{job_id} )
457             foreach ( @{ $res->response_struct->{elements} } );
458              
459             return \@jobs;
460             }
461              
462             =head2 get_job_comments( $WebService::MyGengo::Job|$id )
463              
464             Returns a reference to an array of L<WebService::MyGengo::Comment> objects.
465              
466             You may find it easier to simply use L<get_job> with the $get_comments flag.
467              
468             See: L<http://mygengo.com/api/developer-docs/methods/translate-job-id-comments-get/>
469              
470             =cut
471             #todo Support wantarray?
472             sub get_job_comments {
473             my ( $self, $job ) = ( shift, @_ );
474              
475             my $job_id = blessed($job) ? $job->id : $job;
476              
477             !defined($job_id) and WebService::MyGengo::Exception->throw({
478             message => "Cannot get_job_comments without a Job"
479             });
480              
481             my $res = $self->_send_request(
482             'GET'
483             , '/translate/job/'.$job_id.'/comments'
484             );
485              
486             $res->is_error and return undef;
487              
488             my @comments = map { WebService::MyGengo::Comment->new($_) }
489             @{ $res->response_struct->{thread} };
490              
491             return \@comments;
492             }
493              
494             =head2 get_job_revision( $WebService::MyGengo::Job|$id, $revision_id )
495              
496             Gets a specific revision for the given Job.
497              
498             Returns an L<WebService::MyGengo::Revision> object.
499              
500             See L<http://mygengo.com/api/developer-docs/methods/translate-job-id-revision-rev-id-get/>
501              
502             =cut
503             sub get_job_revision {
504             my ( $self, $job, $rev_id ) = ( shift, @_ );
505              
506             my $job_id = blessed($job) ? $job->id : $job;
507              
508             !( defined($job_id) && defined($rev_id) ) and
509             WebService::MyGengo::Exception->throw({
510             message => "Cannot get_job_revision without a Job and Revision ID"
511             });
512              
513             my $res = $self->_send_request('GET'
514             , '/translate/job/'.$job_id.'/revision/'.$rev_id);
515             $res->is_error and return undef;
516              
517             # We don't get the rev_id back in the struct
518             my $struct = $res->response_struct->{revision};
519             $struct->{rev_id} = $rev_id;
520              
521             return WebService::MyGengo::Revision->new( $struct );
522             }
523              
524             =head2 get_job_revisions( $WebService::MyGengo::Job|$id )
525              
526             Gets all revisions for the given job
527              
528             Revisions are created each time a translator or Senior Translator updates the
529             job.
530              
531             Returns a reference to an array of L<WebService::MyGengo::Revision> objects.
532              
533             You may find it easier to simply use L<get_job> with the $get_revisions flag.
534              
535             See: L<http://mygengo.com/api/developer-docs/methods/translate-job-id-revisions-get/>
536              
537             =cut
538             #todo Support wantarray?
539             sub get_job_revisions {
540             my ( $self, $job ) = ( shift, @_ );
541              
542             my $job_id = blessed($job) ? $job->id : $job;
543              
544             !defined($job_id) and WebService::MyGengo::Exception->throw({
545             message => "Cannot get_job_revisions without a Job"
546             });
547              
548             my $res = $self->_send_request('GET'
549             , '/translate/job/'.$job_id.'/revisions');
550              
551             $res->is_error and return undef;
552              
553             my @revs = map { $self->get_job_revision( $job_id, $_->{rev_id} ) }
554             @{ $res->response_struct->{revisions} };
555              
556             return \@revs;
557             }
558              
559             =head2 get_job_feedback( $WebService::MyGengo::Job|$id )
560              
561             Gets feedback for the given Job.
562              
563             Returns an L<WebService::MyGengo::Feedback> object.
564              
565             You may find it easier to simply use L<get_job> with the $get_feedback flag.
566              
567             B<Note:> Even for Jobs without feedback, the API will still return one with
568             a 'rating' of 3.0 and an empty 'for_translator' attribute. This client
569             makes no attempt to handle this situation.
570              
571             See L<http://mygengo.com/api/developer-docs/methods/translate-job-id-feedback-get/>
572              
573             =cut
574             sub get_job_feedback {
575             my ( $self, $job ) = ( shift, @_ );
576              
577             my $job_id = blessed($job) ? $job->id : $job;
578              
579             !defined($job_id) and WebService::MyGengo::Exception->throw({
580             message => "Cannot get_job_feedback without a Job"
581             });
582              
583             my $res = $self->_send_request('GET'
584             , '/translate/job/'.$job_id.'/feedback');
585              
586             $res->is_error and return undef;
587              
588             return WebService::MyGengo::Feedback->new(
589             $res->response_struct->{feedback}
590             );
591             }
592              
593             =head2 determine_translation_cost( $WebService::MyGengo::Job|\@(WebService::MyGengo::Job) )
594              
595             Given a single Job, or a reference to a list of Jobs, determines the cost to
596             translate them. The Jobs are not saved to myGengo and the user is not charged
597             for them.
598              
599             The only Job fields required to determine cost are: lc_src, lc_tgt, tier and
600             body_src. (However, this method makes no effort to ensure you have set
601             them on each Job.)
602              
603             Returns a reference to an array of Jobs with the 'unit_count' and 'credits'
604             attributes set.
605              
606             See: L<http://mygengo.com/api/developer-docs/methods/translate-service-quote-post/>
607              
608             =cut
609             #todo Proper Job cloning. We might lose comments/etc. as-is, although people probably won't be passing in fully-composed Jobs anyhow.
610             sub determine_translation_cost {
611             my ( $self, $jobs ) = ( shift, @_ );
612              
613             !( $jobs ) and
614             WebService::MyGengo::Exception->throw({
615             message => "Cannot determine_translation_cost without a Job."
616             });
617              
618             ref($jobs) ne 'ARRAY' and $jobs = [$jobs];
619              
620             my $i = 0;
621             my $res = $self->_send_request('POST', '/translate/service/quote', {
622             jobs => {map { "job_".++$i => $_->to_hash } @$jobs}
623             });
624              
625             $res->is_error and return undef;
626              
627             # Assuming the API always returns Jobs in the order in which we provided
628             # them
629             $i = 0;
630             my $struct = $res->response_struct;
631             my @jobs;
632             foreach my $job ( @$jobs ) {
633             # todo Real cloning. MooseX::Clone?
634             push @jobs, WebService::MyGengo::Job->new(
635             %{$job->to_hash([])}, %{$struct->{jobs}->{"job_".++$i}}
636             );
637             }
638              
639             return \@jobs;
640             }
641              
642             =head2 submit_job( $WebService::MyGengo::Job|\%job )
643              
644             Submits a new translation Job.
645              
646             Returns the full L<WebService::MyGengo::Job> fetched from the API on success.
647              
648             Will cowardly refuse to submit a Job without a body_src, lc_src, lc_tgt
649             and tier by throwing an Exception.
650              
651             See: L<http://mygengo.com/api/developer-docs/methods/translate-job-post/>
652              
653             =cut
654             sub submit_job {
655             my ( $self, $job ) = ( shift, @_ );
656              
657             my $hash = ref($job) eq 'HASH' ? $job : $job->to_hash;
658              
659             foreach ( qw/body_src lc_src lc_tgt tier/ ) {
660             !length($hash->{$_}) and
661             WebService::MyGengo::Exception->throw({
662             message => "Cannot submit_job without a body_src, lc_src"
663             . ", lc_tgt and tier"
664             });
665             }
666              
667             my $res = $self->_send_request('POST', '/translate/job/', {
668             job => $hash
669             });
670             $res->is_error and return undef;
671              
672             my $new_job = WebService::MyGengo::Job->new(
673             $res->response_struct->{job}
674             );
675              
676             # If there was a comment submitted with the new Job then make sure
677             # we fetch it back from the API
678             exists($hash->{comment}) and
679             $new_job->_set_comments( $self->get_job_comments( $new_job ) );
680              
681             return $new_job;
682             }
683              
684             =head2 submit_jobs( \@(WebService::MyGengo::Job), $as_group=false? )
685              
686             Submit multiple Jobs to the API in one call.
687              
688             If you would like to specify that a single translator work on all of the Jobs,
689             set $as_group to a true value.
690              
691             Returns a reference to an array of L<WebService::MyGengo::Job> objects from the
692             API on success, undef on failure.
693              
694             B<Note:> There are some restrictions on what Jobs can be grouped; this client
695             makes no attempt to validate these conditions for you.
696              
697             See: L<http://mygengo.com/api/developer-docs/methods/translate-jobs-post/>
698              
699             =cut
700             sub submit_jobs {
701             my ( $self, $jobs, $as_group ) = ( shift, @_ );
702              
703             $as_group //= 0;
704              
705             !( ref($jobs) eq 'ARRAY' and scalar(@$jobs) )
706             and WebService::MyGengo::Exception->throw({
707             message => "Cannot submit jobs without a list of Jobs."
708             });
709              
710             # todo Should to_hash be done in the Request layer?
711             my @jobs_to_submit = map { blessed($_) ? $_->to_hash : $_ } @$jobs;
712              
713             my $res = $self->_send_request('POST', '/translate/jobs', {
714             jobs => \@jobs_to_submit
715             , as_group => $as_group
716             });
717              
718             $res->is_error and return undef;
719              
720             my $struct = $res->response_struct;
721             my @jobs;
722             foreach ( @{$struct->{jobs}} ) {
723             # todo For some reason, the API returns an arrayref for element 0
724             # instead of a hashref, and the subsequent hashrefs are keyed
725             # unnecessarily by increment ID.
726             # This is actually in the raw JSON string...
727             my $args =
728             ref($_) eq 'ARRAY'
729             ? $_->[0]
730             : $_->{ (keys %$_)[0] };
731              
732             $struct->{group_id} and $args->{group_id} = $struct->{group_id};
733              
734             push @jobs, WebService::MyGengo::Job->new($args);
735             }
736              
737             return \@jobs;
738             }
739              
740             =head2 add_job_comment( $WebService::MyGengo::Job|$id, $WebService::MyGengo::Comment|$body )
741              
742             Adds a comment to the specified Job.
743              
744             Returns the Job with the `comments` collection refreshed on success,
745             undef on error.
746              
747             See: L<http://mygengo.com/api/developer-docs/methods/translate-job-id-comment-post/>
748              
749             =cut
750             sub add_job_comment {
751             my ( $self, $job, $comment ) = ( shift, @_ );
752            
753             my $job_id = blessed($job) ? $job->id : $job;
754             my $body = blessed($comment) ? $comment->body : $comment;
755              
756             !( defined($job_id) && defined($body) ) and
757             WebService::MyGengo::Exception->throw({
758             message => "Cannot add_job_comment without a Job and comment body"
759             });
760              
761             my $res = $self->_send_request(
762             'POST'
763             , '/translate/job/'.$job->id.'/comment'
764             , { body => $body }
765             );
766              
767             $res->is_error and return undef;
768              
769             # 1 flag is to force comment refresh
770             return $self->get_job( $job->id, 1 );
771             }
772              
773             =head2 delete_job( $WebService::MyGengo::Job|$id )
774              
775             =head2 cancel_job( $WebService::MyGengo::Job|$id )
776              
777             Deletes (cancels) a Job.
778              
779             Returns true on success, false on failure.
780              
781             B<Note:> You can only cancel a Job if it has not yet been started by a
782             translator.
783              
784             See: L<http://mygengo.com/api/developer-docs/methods/translate-job-id-delete/>
785              
786             =cut
787             sub cancel_job { shift->delete_job( @_ ) }
788             sub delete_job {
789             my ( $self, $job ) = ( shift, @_ );
790              
791             my $job_id = blessed($job) ? $job->id : $job;
792              
793             !defined($job_id) and WebService::MyGengo::Exception->throw({
794             message => "Cannot delete_job without a Job"
795             });
796              
797             my $res = $self->_send_request('DELETE', '/translate/job/'.$job_id);
798             return $res->is_success;
799             }
800              
801             =head2 UNIMPLEMENTED delete_jobs( \@jobs )
802              
803             =head2 UNIMPLEMENTED cancel_jobs( \@jobs )
804              
805             Deletes (cancels) several Jobs in one API call.
806              
807             You can only cancel a Job if it has not yet been started by a translator.
808              
809             Returns true on success, false on failure.
810              
811             B<Note:> This endpoint is documented at the link below, but every calling method
812             I've tried yields a 500 error from the sandbox.
813              
814             See: L<http://mygengo.com/api/developer-docs/methods/translate-jobs-delete/>
815              
816             =cut
817             sub cancel_jobs { shift->delete_jobs( @_ ) }
818             sub delete_jobs {
819             my ( $self, $jobs ) = ( shift, @_ );
820              
821             die "Unimplemented.";
822              
823             ref($jobs) ne 'ARRAY' and WebService::MyGengo::Exception->throw({
824             message => "Cannot delete_jobs without a list of Jobs"
825             });
826              
827             my @ids = map { blessed($_) ? $_->id : $_ } @$jobs;
828              
829             my $res = $self->_send_request(
830             'DELETE'
831             , '/translate/jobs'
832             , { job_ids => \@ids }
833             );
834              
835             return $res->is_success;
836             }
837              
838             =head2 request_job_revision( $WebService::MyGengo::Job|id, $comment )
839              
840             Requests a revision to the given Job.
841              
842             Instructions for the translator must be provided in $comment.
843              
844             Returns the Job, updated from the API, on success.
845              
846             B<Note:> Synonym `revise_job` is provided for convenience, although it
847             does not accurately describe the action performed.
848              
849             See L<http://mygengo.com/api/developer-docs/methods/translate-job-id-put/>
850              
851             =cut
852             #todo Making these specialized requests into subclasses, or using roles/traits, would make things nicer. The client shouldnt have to know this much about the guts of a request.
853             sub revise_job { shift->request_job_revision(@_) }
854             sub request_job_revision {
855             my ( $self, $job, $comment ) = ( shift, @_ );
856              
857             my $job_id = blessed($job) ? $job->id : $job;
858              
859             !( $job_id && $comment ) and
860             WebService::MyGengo::Exception->throw({
861             message => "Cannot request_job_revision without a Job and comment"
862             });
863              
864             my $res = $self->_update_job( $job_id, {
865             action => 'revise'
866             , comment => $comment
867             } );
868             $res->is_error and return undef;
869              
870             # Revisions add comments, so fetch them as well
871             return $self->get_job( $job_id, 1 );
872             }
873              
874             =head2 approve_job( $WebService::MyGengo::Job|id, $rating, $comment_for_translator?, $comment_for_mygengo?, $public_comments? )
875              
876             Approves the most recent translation of the given Job.
877              
878             A rating is required and must be between 1 (poor) and 5 (excellent.)
879              
880             Feedback for the translator or for myGengo are optional. If the
881             $public_comments flag is true your feedback may be shared publicly by myGengo.
882              
883             Returns the Job, updated from the API, on success.
884              
885             See L<http://mygengo.com/api/developer-docs/methods/translate-job-id-put/>
886              
887             =cut
888             #todo Making these specialized requests into subclasses, or using roles/traits, would make things nicer. The client shouldnt have to know this much about the guts of a request.
889             sub approve_job {
890             my ( $self, $job, $rating, $for_translator, $for_mygengo, $public_comment)
891             = ( shift, @_ );
892              
893             my $job_id = blessed($job) ? $job->id : $job;
894              
895             !( $job_id && $rating && ( $rating > 0 && $rating < 6 ) ) and
896             WebService::MyGengo::Exception->throw({
897             message => "Cannot approve_job without a Job and rating between 1 and 5"
898             });
899              
900             my $res = $self->_update_job( $job_id, {
901             action => 'approve'
902             , rating => $rating
903             , defined($for_translator) ? (for_translator => $for_translator) : ()
904             , defined($for_mygengo) ? (for_mygengo => $for_mygengo) : ()
905             , defined($public_comment) ? (public => $public_comment) : ()
906             } );
907             $res->is_error and return undef;
908              
909             # Approvals add comments, revisions and feedback, so fetch them as well
910             return $self->get_job( $job_id, 1, 1, 1 );
911             }
912              
913             =head2 reject_job( $WebService::MyGengo::Job|id, $reason, $comment, $captcha, $follow_up=requeue? )
914              
915             Rejects the given Job.
916              
917             A reason for the rejection is required and must be one of: quality, incomplete, other
918              
919             A comment regarding the rejection and the human-readable text from the image
920             refered to in the Job's `captcha_url` attribute are also required.
921              
922             You may supply an optional follow-up action for the Job. Legal values are:
923             requeue (re-submit the Job for translation automatically), cancel (cancel the
924             Job outright)
925              
926             Returns the Job, updated from the API, on success.
927              
928             See L<http://mygengo.com/api/developer-docs/methods/translate-job-id-put/>
929              
930             =cut
931             #todo Making these specialized requests into subclasses, or using roles/traits, would make things nicer. The client shouldnt have to know this much about the guts of a request.
932             sub reject_job {
933             my ( $self, $job, $reason, $comment, $captcha, $follow_up ) = ( shift, @_ );
934              
935             my $job_id = blessed($job) ? $job->id : $job;
936              
937             !( $job_id && $reason && ( $reason =~ /quality|incomplete|other/ )
938             && $comment && $captcha )
939             and
940             WebService::MyGengo::Exception->throw({
941             message => "Cannot reject_job without a Job, reason, comment"
942             . " and captcha text"
943             });
944              
945             my $res = $self->_update_job( $job_id, {
946             action => 'reject'
947             , reason => $reason
948             , comment => $comment
949             , captcha => $captcha
950             , defined($follow_up) ? (follow_up => $follow_up) : ()
951             } );
952             $res->is_error and return undef;
953              
954             # Rejection adds comments, so fetch them as well
955             return $self->get_job( $job_id, 1 );
956             }
957              
958             #=head2 _update_job( $WebService::MyGengo::Job|id, \%parameters )
959             #
960             #Internal method to updates Job status.
961             #
962             #Different statuses accept different parameters.
963             #
964             #See the individual revise|approve|reject_job methods for details.
965             #
966             #=cut
967             sub _update_job {
968             my ( $self, $job, $params ) = ( shift, @_ );
969              
970             my $job_id = blessed($job) ? $job->id : $job;
971              
972             return $self->_send_request('PUT', '/translate/job/'.$job_id, $params);
973             }
974              
975             =head2 get_service_language_pairs( $source_language_code? )
976              
977             Returns supported translation language pairs, tiers, and credit prices.
978              
979             $source_language_code is the ISO 2-character code. If provided, only language
980             pairs with that source language will be returned.
981              
982             Returns a refernece to an array of L<WebService::MyGengo::LanguagePair> objects.
983              
984             See: L<http://mygengo.com/api/developer-docs/methods/translate-service-language-pairs-get/>
985              
986             =cut
987             #todo Support wantarray?
988             sub get_service_language_pairs {
989             my ( $self, $lc_src ) = ( shift, @_ );
990            
991             my $res = $self->_send_request('GET', '/translate/service/language_pairs', {
992             defined($lc_src) ? (lc_src => $lc_src) : ()
993             });
994              
995             $res->is_error and return undef;
996              
997             my @pairs;
998             push @pairs, WebService::MyGengo::LanguagePair->new( $_ )
999             foreach @{ $res->response_struct->{elements} };
1000              
1001             return \@pairs;
1002             }
1003              
1004             =head2 get_service_languages( )
1005              
1006             Gets all languages supported by myGengo.
1007              
1008             Returns a reference to an array of L<WebService::MyGengo::Language> objects.
1009              
1010             See: L<http://mygengo.com/api/developer-docs/methods/translate-service-languages-get/>
1011              
1012             =cut
1013             #todo Support wantarray?
1014             sub get_service_languages {
1015             my ($self) = @_;
1016              
1017             my $res = $self->_send_request('GET', '/translate/service/languages');
1018              
1019             $res->is_error and return undef;
1020              
1021             my @langs;
1022             foreach my $lang ( @{ $res->response_struct->{elements} } ) {
1023             push @langs, WebService::MyGengo::Language->new( $lang );
1024             }
1025              
1026             return \@langs;
1027             }
1028              
1029             #=head2 _send_request( $http_method_name, $api_endpoint, [\%params] )
1030             #
1031             #Internal method that retrieves a request from the `request_factory`, sends
1032             #it via the `_user_agent` and returns a L<WebService::MyGengo::Response> object.
1033             #
1034             #See POD for the individual methods to determine what they will return.
1035             #
1036             #The last raw API response object is stored in the Client's L<last_response>
1037             #attribute.
1038             #
1039             #=cut
1040             sub _send_request {
1041             my ( $self, @args ) = ( shift, @_ );
1042              
1043             my $req = $self->request_factory->new_request( @args );
1044              
1045             my $res = WebService::MyGengo::Response->new(
1046             $self->_user_agent->request( $req )
1047             );
1048              
1049             $self->_set_last_response( $res );
1050              
1051             return $res;
1052             }
1053              
1054              
1055             __PACKAGE__->meta->make_immutable();
1056             1;
1057              
1058             =head1 TODO
1059              
1060             * Add caching support
1061              
1062             * Make fetching of comments/feedback/revisions into global switches
1063              
1064             * Use concurrent requests to fetch jobs/comments/revisions/feedback/etc.
1065              
1066             * Support perl < 5.10? Necessary?
1067              
1068             * I'm not 100% sold on this implementation. I waffle back and forth on whether or not the real API objects should be able to do their own requesting /error handling/etc instead of this client.
1069              
1070             =head1 ACKNOWLEDGEMENTS
1071              
1072             Portions of this library are based on the original myGengo.pm library, available
1073             here: L<https://github.com/myGengo/mygengo-perl-new>.
1074              
1075             That library is Copyright (c) 2011 myGengo, Inc. (L<http://mygengo.com>) and
1076             is available under the
1077             L<http://mygengo.com/services/api/dev-docs/mygengo-code-license/> New BSD
1078             License.
1079              
1080             At the time of this writing the above link is broken. If any portion of this
1081             library is in violation of the myGengo license please notify me.
1082              
1083             =head1 SEE ALSO
1084              
1085             myGengo API documentation: L<http://mygengo.com/api/developer-docs/>
1086              
1087             This module on GitHub: L<https://github.com/nheinric/WebService--MyGengo>
1088              
1089             =head1 AUTHOR
1090              
1091             Nathaniel Heinrichs
1092              
1093             =head1 LICENSE
1094              
1095             Copyright (c) 2011, Nathaniel Heinrichs <nheinric-at-cpan.org>.
1096             All rights reserved.
1097              
1098             This library is free software. You can redistribute it and/or modify
1099             it under the same terms as Perl itself.
1100              
1101             =cut