File Coverage

blib/lib/WWW/GoodData.pm
Criterion Covered Total %
statement 15 258 5.8
branch 0 92 0.0
condition 0 60 0.0
subroutine 5 41 12.2
pod 26 29 89.6
total 46 480 9.5


line stmt bran cond sub pod time code
1             package WWW::GoodData;
2              
3             =head1 NAME
4              
5             WWW::GoodData - Client library for GoodData REST-ful API
6              
7             =head1 SYNOPSIS
8              
9             use WWW::GoodData;
10             my $gdc = new WWW::GoodData;
11             print $gdc->get_uri ('md', { title => 'My Project' });
12              
13             =head1 DESCRIPTION
14              
15             B is the client for GoodData JSON-based API
16             built atop L client agent, with focus
17             on usefullness and correctness of implementation.
18              
19             It provides code for navigating the REST-ful API structure as well as
20             wrapper funcitons for common actions.
21              
22             =cut
23              
24 1     1   4691 use strict;
  1         1  
  1         102  
25 1     1   7 use warnings;
  1         3  
  1         49  
26              
27 1     1   5 use WWW::GoodData::Agent;
  1         2  
  1         25  
28 1     1   5 use JSON;
  1         1  
  1         11  
29 1     1   476 use URI;
  1         11  
  1         13026  
30              
31             our $root = new URI ('https://secure.gooddata.com/gdc');
32              
33             =head1 METHODS
34              
35             =over 4
36              
37             =item B [PARAMS]
38              
39             Create a new client instance.
40              
41             You can optionally pass a hash reference with properties that would be
42             blessed, otherwise a new one is created. Possible properties include:
43              
44             =over 8
45              
46             =item B
47              
48             A L instance to use.
49              
50             =item B
51              
52             A number of retries to obtain results of asynchronous tasks, such as
53             report exports or data uploads. See B.
54              
55             Defaults to 3600 (delay of one hour).
56              
57             =back
58              
59             =cut
60              
61             sub new
62             {
63 0     0 1   my $class = shift;
64 0   0       my $self = shift || {};
65 0           bless $self, $class;
66 0   0       $self->{agent} ||= new WWW::GoodData::Agent ($root);
67 0   0       $self->{retries} ||= 3600;
68 0           return $self;
69             }
70              
71             # API hierarchy traversal Cache
72             our %links;
73             sub get_canonical_links
74             {
75 0     0 0   my $self = shift;
76 0           my $root = shift;
77 0 0         my @path = map { ref $_ ? $_ : { category => $_ } } @_;
  0            
78 0           my $link = shift @path;
79              
80 0 0         unless ($links{$root}) {
81 0           my $response = $self->{agent}->get ($root);
82             # Various ways to get the links
83 0 0 0       if (exists $response->{about}) {
    0          
    0          
84             # Ordinary structure with about section
85 0           $links{$root} = $response->{about}{links};
86             } elsif (exists $response->{query} and exists $response->{query}{entries}) {
87             # Inconsistent query entries
88 0           $links{$root} = $response->{query}{entries};
89             } elsif (scalar keys %$response == 1) {
90 0           my @elements = ($response);
91 0           my ($structure) = keys %$response;
92              
93             # Aggregated resources (/gdc/account/profile/666/projects)
94 0           @elements = @{$response->{$structure}}
95 0 0         if ref $response->{$structure} eq 'ARRAY';
96              
97 0           $links{$root} = [];
98 0           foreach my $element (@elements) {
99 0           my $root = $root;
100 0           my ($type) = keys %$element;
101              
102             # Metadata with interesting information outside "links"
103 0 0 0       if (exists $element->{$type}{links}{self}
104             and exists $element->{$type}{meta}) {
105 0           my $link = new URI ($element->{$type}{links}{self})->abs ($root);
106 0           push @{$links{$root}}, {
107 0           %{$element->{$type}{meta}},
  0            
108             category => $type,
109             structure => $structure,
110             link => $link,
111             };
112 0           $root = $link;
113             }
114              
115             # The links themselves
116 0           foreach my $category (keys %{$element->{$type}{links}}) {
  0            
117 0           my $link = new URI ($element->{$type}{links}{$category})->abs ($root);
118 0           push @{$links{$root}}, {
  0            
119             structure => $structure,
120             category => $category,
121             type => $type,
122             link => $link,
123             };
124             }
125             }
126              
127             } else {
128 0           die 'No links';
129             }
130             }
131              
132             # Canonicalize the links
133 0           $_->{link} = new URI ($_->{link})->abs ($root) foreach @{$links{$root}};
  0            
134              
135             my @matches = grep {
136 0           my $this_link = $_;
137             # Filter out those, who lack any of our keys or
138             # hold a different value for it.
139 0           not map { not exists $link->{$_}
140             or not exists $this_link->{$_}
141 0 0 0       or $link->{$_} ne $this_link->{$_}
    0          
142             ? 1 : () } keys %$link
143 0           } @{$links{$root}};
  0            
144              
145             # Fully resolved
146 0 0         return @matches unless @path;
147              
148 0 0         die 'Nonexistent component in path' unless @matches;
149 0 0         die 'Ambigious path' unless scalar @matches == 1;
150              
151             # Traverse further
152 0           return $self->get_canonical_links ($matches[0]->{link}, @path);
153             }
154              
155             # This is a 'normalized' version, for convenience and compatibility
156             sub get_links
157             {
158 0     0 0   my $self = shift;
159 0 0 0       my $root = (ref $_[0] and ref $_[0] ne 'HASH') ? shift : '';
160              
161             # Canonicalize URIs
162 0           $root = new URI ($root)->abs ($self->{agent}{root});
163              
164             # And decanonicalize, ommiting the scheme and authority part if possible
165 0           my @links = $self->get_canonical_links ($root, @_);
166             $_->{link} = $_->{link}->rel ($root)->authority
167 0 0         ? $_->{link} : new URI ($_->{link}->path) foreach @links;
168              
169 0           return @links;
170             }
171              
172             =item B PATH
173              
174             Traverse the links in resource hierarchy following given PATH,
175             starting from API root (L by default).
176              
177             PATH is an array of dictionaries, where each key-value pair
178             matches properties of a link. If a plain string is specified,
179             it is considered to be a match against B property:
180              
181             $gdc->links ('md', { 'category' => 'projects' });
182              
183             The above call returns a list of all projects, with links to
184             their metadata resources.
185              
186             =cut
187              
188             sub links
189             {
190 0     0 1   my @links = get_links @_;
191 0 0         return @links if @links;
192 0           %links = ();
193 0           return get_links @_;
194             }
195              
196             =item B PATH
197              
198             Follows the same samentics as B() call, but returns an
199             URI of the first matching resource instead of complete link
200             structure.
201              
202             =cut
203              
204             sub get_uri
205             {
206 0     0 1   [links @_]->[0]{link};
207             }
208              
209             =item B EMAIL PASSWORD
210              
211             Obtain a SST (login token).
212              
213             =cut
214              
215             sub login
216             {
217 0     0 1   my $self = shift;
218 0           my ($login, $password) = @_;
219              
220 0           my $root = new URI ($self->{agent}{root});
221 0           my $staging = $self->get_uri ('uploads')->abs ($root);
222 0           my $netloc = $staging->host.':'.$staging->port;
223              
224 0           $self->{agent}->credentials ($netloc,
225             'GoodData project data staging area', $login => $password);
226              
227 0           $self->{login} = $self->{agent}->post ($self->get_uri ('login'),
228             {postUserLogin => {
229             login => $login,
230             password => $password,
231             remember => 0}});
232             }
233              
234             =item B
235              
236             Make server invalidate the client session and drop
237             credential tokens.
238              
239             Is called upon destruction of the GoodData client instance.
240              
241             =cut
242              
243             sub logout
244             {
245 0     0 1   my $self = shift;
246              
247 0 0         die 'Not logged in' unless defined $self->{login};
248              
249             # Forget Basic authentication
250 0           my $root = new URI ($self->{agent}{root});
251 0           my $staging = $self->get_uri ('uploads');
252 0           my $netloc = $staging->host.':'.$staging->port;
253 0           $self->{agent}->credentials ($netloc,
254             'GoodData project data staging area', undef, undef);
255              
256             # The redirect magic does not work for POSTs and we can't really
257             # handle 401s until the API provides reason for them...
258 0           $self->{agent}->get ($self->get_uri ('token'));
259              
260 0           $self->{agent}->delete ($self->{login}{userLogin}{state});
261 0           $self->{login} = undef;
262             }
263              
264             =item B OLD NEW
265              
266             Change user password given the old and new password.
267              
268             =cut
269              
270             sub change_passwd
271             {
272 0     0 1   my $self = shift;
273 0 0         my $old_passwd = shift or die 'No old password given';
274 0 0         my $new_passwd = shift or die 'No new password given';
275              
276 0 0         die 'Not logged in' unless defined $self->{login};
277              
278 0           my $profile = $self->{agent}->get ($self->{login}{userLogin}{profile});
279             my $new_profile = {
280             'accountSetting' => {
281             'old_password' => $old_passwd,
282             'password' => $new_passwd,
283             'verifyPassword' => $new_passwd,
284             'firstName' => $profile->{accountSetting}->{firstName},
285             'lastName' => $profile->{accountSetting}->{lastName}
286             }
287 0           };
288              
289 0           $self->{agent}->put ($self->{login}{userLogin}{profile}, $new_profile);
290             }
291              
292             =item B
293              
294             Return array of links to project resources on metadata server.
295              
296             =cut
297              
298             sub projects
299             {
300 0     0 1   my $self = shift;
301 0 0         die 'Not logged in' unless $self->{login};
302 0           $self->get_links (new URI ($self->{login}{userLogin}{profile}),
303             qw/projects project/);
304             }
305              
306             =item B IDENTIFIER
307              
308             Delete a project given its identifier.
309              
310             =cut
311              
312             sub delete_project
313             {
314 0     0 1   my $self = shift;
315 0           my $project = shift;
316              
317             # Instead of directly DELETE-ing the URI gotten, we check
318             # the existence of a project with such link, as a sanity check
319 0 0         my $uri = $self->get_uri (new URI ($project),
320             { category => 'self', type => 'project' }) # Validate it's a project
321             or die "No such project: $project";
322 0           $self->{agent}->delete ($uri);
323             }
324              
325             =item B TITLE SUMMARY TEMPLATE
326              
327             Create a project given its title and optionally summary and project template,
328             return its identifier.
329              
330             The list of valid project templates is available from the template server:
331             L.
332              
333             =cut
334              
335             sub create_project
336             {
337 0     0 1   my $self = shift;
338 0 0         my $title = shift or die 'No title given';
339 0   0       my $summary = shift || '';
340 0           my $template = shift;
341              
342             # The redirect magic does not work for POSTs and we can't really
343             # handle 401s until the API provides reason for them...
344 0           $self->{agent}->get ($self->get_uri ('token'));
345              
346             return $self->{agent}->post ($self->get_uri ('projects'), {
347             project => {
348             # No hook to override this; use web UI
349             content => { guidedNavigation => 1 },
350             meta => {
351             summary => $summary,
352             title => $title,
353             ($template ? (projectTemplate => $template) : ()),
354             }
355 0 0         }})->{uri};
356             }
357              
358             =item B PROJECT_URI
359              
360             Wait until project identified by its uri is in enabled state,
361             return its identifier.
362              
363             =cut
364              
365             sub wait_project_enabled
366             {
367 0     0 1   my $self = shift;
368 0   0       my $project_uri = shift || die 'Project uri was not specified.';
369              
370 0           my $state;
371             my $exported = $self->poll (
372 0     0     sub { $self->{agent}->get ($project_uri) },
373             sub { $_[0] and exists $_[0]->{project} and exists $_[0]->{project}{content} and exists $_[0]->{project}{content}{state} and
374 0 0 0 0     (($state = $_[0]->{project}{content}{state}) !~ /^(PREPARING|PREPARED|LOADING)$/)
      0        
      0        
375             }
376 0 0         ) or die 'Timed out waiting for project preparation';
377 0 0         ($state eq 'ENABLED') or die "Unable to enable project";
378             }
379              
380             =item B LOGIN PASSWORD FIRST_NAME LAST_NAME PHONE COMPANY
381              
382             Create a user given its login, password, first name, surname, phone and optionally company,
383             return his identifier.
384              
385             =cut
386              
387             sub create_user
388             {
389 0     0 1   my $self = shift;
390 0           my $login = shift;
391 0           my $passwd = shift;
392 0           my $firstname = shift;
393 0           my $lastname = shift;
394 0           my $phone = shift;
395 0   0       my $company = shift || '';
396              
397             return $self->{agent}->post ('/gdc/account/domains/default/users', { #TODO links does not exists
398             accountSetting => {
399             login => $login,
400             password => $passwd,
401             verifyPassword => $passwd,
402             firstName => $firstname,
403             lastName => $lastname,
404             phoneNumber => $phone,
405             companyName => $company
406 0           }})->{uri};
407             }
408              
409             =item B
410              
411             Gets project roles. Project is identified by its id.
412             return array of project roles.
413              
414             =cut
415              
416             sub get_roles
417             {
418 0     0 1   my $self = shift;
419 0           my $project = shift;
420              
421             return $self->{agent}->get (
422 0           $self->get_uri (new URI($project), 'roles'))->{projectRoles}{roles};
423             }
424              
425             =item B
426              
427             Gets project roles. Project is identified by its id.
428             return hash map role id => role uri.
429              
430             =cut
431              
432             sub get_roles_by_id
433             {
434 0     0 1   my $self = shift;
435 0           my $project = shift;
436 0           my $rolesUris = $self->get_roles ($project);
437              
438 0           my %roles;
439              
440 0           foreach my $roleUri (@$rolesUris) {
441 0           my $role = $self->{agent}-> get ($roleUri);
442 0           my $roleId = $role->{projectRole}{meta}{identifier};
443 0           $roles{$roleId} = $roleUri;
444             }
445 0           return %roles;
446             }
447              
448             =item B USER PROJECT ROLE
449              
450             Assign user to project.
451             return his identifier.
452              
453             =cut
454              
455             sub assign_user
456             {
457 0     0 1   my $self = shift;
458 0           my $user = shift;
459 0           my $project = shift;
460 0           my $role = shift;
461              
462 0           my @userRoles = ($role);
463              
464 0           return $self->{agent}->post ($self->get_uri (new URI($project),'users'), {
465             user => {
466             content => {
467             status => "ENABLED",
468             userRoles => \@userRoles
469             },
470             links => {
471             self => $user
472             }
473             }
474             });
475             }
476              
477             =item B PROJECT_URI CRON PARAMS HIDDEN_PARAMS
478              
479             Create a schedule given its project, type, cron expression and optionally
480             parameters and hidden parameters, return created schedule object.
481              
482             =cut
483              
484             sub schedule {
485 0     0 1   my $self = shift;
486 0           my $project_uri = shift;
487 0           my $type = shift;
488 0           my $cron = shift;
489 0   0       my $params = shift || { };
490 0   0       my $hidden_params = shift || { };
491              
492 0           return $self->{agent}->post ($project_uri.'/schedules', {schedule => { #TODO no link to schedules does not exists
493             type => $type,
494             params => $params,
495             hiddenParams => $hidden_params,
496             cron => $cron
497             }});
498             }
499              
500             =item B PROJECT_URI TRANSFORMATION_ID GRAPH_NAME CRON PARAMS HIDDEN_PARAMS
501              
502             Create a MSETL schedule given its project, clover transformation id,
503             clover graph to schedule, cron expression and optionally
504             parameters and hidden parameters, return created schedule object.
505              
506             =cut
507              
508             sub schedule_msetl_graph {
509 0     0 1   my $self = shift;
510 0           my $project_uri = shift;
511 0           my $trans_id = shift;
512 0           my $graph = shift;
513 0           my $cron = shift;
514 0   0       my $params = shift || { };
515 0   0       my $hidden_params = shift || { };
516              
517 0           my $type = "MSETL";
518              
519 0           $params->{"TRANSFORMATION_ID"} = $trans_id;
520 0           $params->{"CLOVER_GRAPH"} = $graph;
521              
522 0           return $self->schedule (
523             $project_uri, $type, $cron, $params, $hidden_params);
524             }
525              
526             =item B PROJECT_URI TEMPLATE TRANSFORMATION_ID NAME
527              
528             Create a clover transformation given its project uri, template, clover
529             transformation id in template and optionaly name, return created transformation
530             object.
531              
532             =cut
533              
534             sub create_clover_transformation
535             {
536 0     0 1   my $self = shift;
537 0           my $projectUri = shift;
538 0           my $template = shift;
539 0           my $transformation = shift;
540 0   0       my $name = shift || $transformation;
541              
542 0           my $file = $transformation.'.zip';
543 0           my $path = '/uploads/'.$file;
544              
545             # download clover transformation zip file from project template
546 0           my $content = $self->{agent}->get ($template.'/'.$file);
547              
548             # upload clover transformation zip file
549 0           my $uploads = new URI ($self->get_uri ('uploads'));
550 0           $uploads->path_segments ($uploads->path_segments, $file);
551             $self->{agent}->request (new HTTP::Request (PUT => $uploads,
552 0           ['Content-Type' => 'application/zip'], $content->{raw}));
553              
554             # create transformation
555 0           return $self->{agent}->post ($projectUri."/etl/clover/transformations", { #TODO links does not exists
556             cloverTransformation => {
557             name => $name,
558             path => $path
559             }
560             });
561             }
562              
563             =item B PROJECT
564              
565             Return array of links to repoort resources on metadata server.
566              
567             =cut
568              
569             sub reports
570             {
571 0     0 1   my $self = shift;
572 0           my $project = shift;
573              
574 0 0         die 'Not logged in' unless $self->{login};
575 0           $self->get_links (new URI ($project),
576             { category => 'self', type => 'project' }, # Validate it's a project
577             qw/metadata query reports/, {});
578             }
579              
580             =item B REPORT
581              
582             Trigger a report computation and return the URI of the result resource.
583              
584             =cut
585              
586             sub compute_report
587             {
588 0     0 1   my $self = shift;
589 0           my $report = shift;
590              
591             return $self->{agent}->post (
592             $self->get_uri (qw/xtab xtab-executor3/),
593             { report_req => { report => $report }}
594 0           )->{reportResult2}{meta}{uri};
595             }
596              
597             =item B REPORT FORMAT
598              
599             Submit an exporter task for a computed report (see B),
600             wait for completion and return raw data in desired format.
601              
602             =cut
603              
604             sub export_report
605             {
606 0     0 1   my $self = shift;
607 0           my $report = shift;
608 0           my $format = shift;
609              
610             # Compute the report
611             my $result = $self->{agent}->post (
612 0           $self->get_uri (qw/report-exporter exporter-executor/),
613             { result_req => { format => $format,
614             report => $self->compute_report ($report) }}
615             );
616              
617             # This is for new release, where location is finally set correctly;
618 0 0         $result = $result->{uri} if ref $result eq 'HASH';
619              
620             # Trigger the export
621             my $exported = $self->poll (
622 0     0     sub { $self->{agent}->get ($result) },
623 0 0 0 0     sub { $_[0] and exists $_[0]->{raw} and $_[0]->{raw} ne 'null' }
624 0 0         ) or die 'Timed out waiting for report to export';
625              
626             # Follow the link
627 0 0         $exported = $self->{agent}->get ($exported->{uri}) if exists $exported->{uri};
628              
629             # Gotten the correctly coded result?
630             return $exported->{raw} if $exported->{type} eq {
631             png => 'image/png',
632             pdf => 'application/pdf',
633             xls => 'application/vnd.ms-excel',
634 0 0         }->{$format};
635              
636 0           die 'Wrong type of content returned';
637             }
638              
639             =item B PROJECT
640              
641             Return picture of Logical Data Model (LDM) in PNG format.
642              
643             =cut
644              
645             sub ldm_picture
646             {
647 0     0 1   my $self = shift;
648 0           my $project = shift;
649              
650             my $model = $self->{agent}->get ($self->{agent}->get (
651             $self->get_uri (new URI ($project),
652 0           { category => 'ldm' }))->{uri});
653 0 0         die 'Expected PNG image' unless $model->{type} eq 'image/png';
654              
655 0           return $model->{raw};
656             }
657              
658             =item B PROJECT MAQL
659              
660             Execute MAQL statement for a project.
661              
662             =cut
663              
664             sub ldm_manage
665             {
666 0     0 1   my $self = shift;
667 0           my $project = shift;
668 0           my $maql = shift;
669              
670 0           $maql = "# WWW::GoodData MAQL execution\n$maql";
671 0           chomp $maql;
672              
673             $self->{agent}->post (
674 0           $self->get_uri (new URI ($project), qw/metadata ldm ldm-manage/),
675             { manage => { maql => $maql }});
676             }
677              
678             =item B PROJECT MANIFEST
679              
680             Upload and integrate a new data load via Single Loading Interface (SLI).
681              
682             =cut
683              
684             sub upload
685             {
686 0     0 1   my $self = shift;
687 0           my $project = shift;
688 0           my $file = shift;
689              
690             # Parse the manifest
691 0           my $upload_info = decode_json (slurp_file ($file));
692             die "$file: not a SLI manifest"
693 0 0         unless $upload_info->{dataSetSLIManifest};
694              
695             # Construct unique URI in staging area to upload to
696 0           my $uploads = new URI ($self->get_uri ('uploads'));
697             $uploads->path_segments ($uploads->path_segments,
698 0           $upload_info->{dataSetSLIManifest}{dataSet}.'-'.time);
699 0           $self->{agent}->request (new HTTP::Request (MKCOL => $uploads));
700              
701             # Upload the manifest
702 0           my $manifest = $uploads->clone;
703 0           $manifest->path_segments ($manifest->path_segments, 'upload_info.json');
704 0           $self->{agent}->request (new HTTP::Request (PUT => $manifest,
705             ['Content-Type' => 'application/json'], encode_json ($upload_info)));
706              
707             # Upload CSV
708 0           my $csv = $uploads->clone;
709 0           $csv->path_segments ($csv->path_segments, $upload_info->{dataSetSLIManifest}{file});
710             $self->{agent}->request (new HTTP::Request (PUT => $csv,
711             ['Content-Type' => 'application/csv'],
712             (slurp_file ($upload_info->{dataSetSLIManifest}{file})
713 0   0       || die 'No CSV file specified in SLI manifest')));
714              
715             # Trigger the integration
716             my $task = $self->{agent}->post (
717             $self->get_uri (new URI ($project),
718             { category => 'self', type => 'project' }, # Validate it's a project
719             qw/metadata etl pull/),
720             { pullIntegration => [$uploads->path_segments]->[-1] }
721 0           )->{pullTask}{uri};
722              
723             # Wait for the task to enter a stable state
724             my $result = $self->poll (
725 0     0     sub { $self->{agent}->get ($task) },
726 0     0     sub { shift->{taskStatus} !~ /^(RUNNING|PREPARED)$/ }
727 0 0         ) or die 'Timed out waiting for integration to finish';
728              
729 0 0         return if $result->{taskStatus} eq 'OK';
730 0 0         warn 'Upload finished with warnings' if $result->{taskStatus} eq 'WARNING';
731 0           die 'Upload finished with '.$result->{taskStatus}.' status';
732             }
733              
734             =item B BODY CONDITION
735              
736             Should only be used internally.
737              
738             Run BODY passing its return value to call to CONDITION until it
739             evaluates to true or B (see properties) times out.
740              
741             Returns value is of last iteration of BODY in case
742             CONDITION succeeds, otherwise undefined (in case of timeout).
743              
744             =cut
745              
746             sub poll
747             {
748 0     0 1   my $self = shift;
749 0           my ($body, $cond) = @_;
750 0           my $retries = $self->{retries};
751              
752 0           while ($retries--) {
753 0           my $ret = $body->();
754 0 0         return $ret if $cond->($ret);
755 0           sleep 1;
756             }
757              
758 0           return undef;
759             }
760              
761             =item B PROJECT URI TYPE TITLE SUMMARY EXPRESSION
762              
763             Create a new metadata object of type TYPE with EXPRESSION as the only content.
764              
765             =cut
766              
767             sub create_object_with_expression
768             {
769 0     0 1   my $self = shift;
770 0           my $project = shift;
771 0           my $uri = shift;
772 0 0         my $type = shift or die 'No type given';
773 0 0         my $title = shift or die 'No title given';
774 0   0       my $summary = shift || '';
775 0 0         my $expression = shift or die 'No expression given';
776              
777 0 0         if (defined $uri) {
778 0           $uri = new URI ($uri);
779             } else {
780 0           $uri = $self->get_uri (new URI ($project), qw/metadata obj/);
781             }
782              
783             return $self->{agent}->post (
784             $uri,
785             { $type => {
786             content => {
787             expression => $expression
788             },
789             meta => {
790             summary => $summary,
791             title => $title,
792             }
793             }}
794 0           )->{uri};
795             }
796              
797             =item B PROJECT URI TITLE SUMMARY METRICS DIM FILTERS
798              
799             Create a new reportDefinition in metadata.
800              
801             =cut
802              
803             sub create_report_definition
804             {
805 0     0 1   my $self = shift;
806 0           my $project = shift;
807 0           my $uri = shift;
808 0 0         my $title = shift or die 'No title given';
809 0   0       my $summary = shift || '';
810 0   0       my $metrics = shift || [];
811 0   0       my $dim = shift || [];
812 0   0       my $filters = shift || [];
813              
814 0 0         if (defined $uri) {
815 0           $uri = new URI ($uri);
816             } else {
817 0           $uri = $self->get_uri (new URI ($project), qw/metadata obj/);
818             }
819              
820             return $self->{agent}->post (
821             $uri,
822             { reportDefinition => {
823             content => {
824             filters => [ map +{ expression => $_ }, @$filters ],
825             grid => {
826             columns => [ "metricGroup" ],
827             metrics => [ map +{ alias => '', uri => $_ }, @$metrics ],
828             rows => [ map +{ attribute => { alias => '', uri => $_,
829             totals => [[]] } }, @$dim ],
830             sort => {
831             columns => [],
832             rows => [],
833             },
834             columnWidths => []
835             },
836             format => "grid"
837             },
838             meta => {
839             summary => $summary,
840             title => $title,
841             }
842             }}
843 0           )->{uri};
844             }
845              
846             =item B
847              
848             Log out the session with B unless not logged in.
849              
850             =cut
851              
852             sub DESTROY
853             {
854 0     0     my $self = shift;
855 0 0         $self->logout if $self->{login};
856             }
857              
858             sub slurp_file
859             {
860 0     0 0   my $file = shift;
861 0 0         open (my $fh, '<', $file) or die "$file: $!";
862 0           return join '', <$fh>;
863             }
864              
865             =back
866              
867             =head1 SEE ALSO
868              
869             =over
870              
871             =item *
872              
873             L -- API documentation
874              
875             =item *
876              
877             L -- Browsable GoodData API
878              
879             =item *
880              
881             L -- GoodData API-aware user agent
882              
883             =back
884              
885             =head1 COPYRIGHT
886              
887             Copyright 2011, 2012 Lubomir Rintel
888              
889             Copyright 2012 Adam Stulpa, Jan Orel, Tomas Janousek
890              
891             This program is free software; you can redistribute it and/or modify it
892             under the same terms as Perl itself.
893              
894             =head1 AUTHORS
895              
896             Lubomir Rintel C
897              
898             Adam Stulpa C
899              
900             Jan Orel C
901              
902             Tomas Janousek C
903              
904             =cut
905              
906             1;