File Coverage

blib/lib/WebAPI/DBIC/Resource/JSONAPI/Role/DBIC.pm
Criterion Covered Total %
statement 12 111 10.8
branch 0 50 0.0
condition 0 16 0.0
subroutine 4 13 30.7
pod 0 5 0.0
total 16 195 8.2


line stmt bran cond sub pod time code
1             package WebAPI::DBIC::Resource::JSONAPI::Role::DBIC;
2             $WebAPI::DBIC::Resource::JSONAPI::Role::DBIC::VERSION = '0.003002';
3              
4 2     2   25162387 use Carp qw(croak confess);
  2         33  
  2         391  
5 2     2   1115 use Devel::Dwarn;
  2         18452  
  2         15  
6 2     2   1162 use JSON::MaybeXS qw(JSON);
  2         1434  
  2         95  
7              
8 2     2   941 use Moo::Role;
  2         42246  
  2         11  
9              
10              
11             requires 'get_url_for_item_relationship';
12             requires 'render_item_as_plain_hash';
13             requires 'path_for_item';
14             requires 'add_params_to_url';
15             requires 'prefetch';
16             requires 'type_namer';
17              
18              
19              
20             sub jsonapi_type {
21 0     0 0   my ($self) = @_;
22 0           return $self->type_namer->type_name_for_resultset($self->set);
23             }
24              
25              
26             sub top_link_for_relname { # XXX cacheable
27 0     0 0   my ($self, $relname) = @_;
28              
29 0           my $link_url_templated = $self->get_url_template_for_set_relationship($self->set, $relname);
30 0 0         return if not defined $link_url_templated;
31              
32             # XXX a hack to keep the template urls readable!
33 0           $link_url_templated =~ s/%7B/{/g;
34 0           $link_url_templated =~ s/%7D/}/g;
35              
36 0           my $rel_info = $self->set->result_class->relationship_info($relname);
37 0   0       my $result_class = $rel_info->{class}||die "panic";
38              
39 0           my $rel_jsonapi_type = $self->type_namer->type_name_for_result_class($result_class);
40              
41 0           my $path = $self->jsonapi_type .".". $relname;
42 0           return $path => {
43             href => "$link_url_templated", # XXX stringify the URL object
44             type => $rel_jsonapi_type,
45             };
46             }
47              
48              
49             sub render_jsonapi_response { # return top-level document hashref
50 0     0 0   my ($self) = @_;
51              
52 0           my $set = $self->set;
53              
54 0           my %item_edit_rel_hooks;
55              
56             my %top_links;
57 0           my %compound_links;
58              
59 0 0         for my $prefetch (@{$self->prefetch||[]}) {
  0            
60 0           while (my ($relname, $sub_rel) = each %{$prefetch}){
  0            
61              
62 0 0         next if $self->param('distinct');
63              
64             #Dwarn
65 0           my $rel_info = $set->result_class->relationship_info($relname);
66 0   0       my $result_class = $rel_info->{class}||die "panic";
67              
68 0           my @idcolumns = $result_class->unique_constraint_columns('primary'); # XXX wrong
69 0 0         if (@idcolumns > 1) { # eg many-to-many that doesn't have a separate id
70 0 0         warn "Result class $result_class has multiple keys (@idcolumns) so relations like $relname won't have links generated.\n"
71             unless our $warn_once->{"$result_class $relname"}++;
72 0           next;
73             }
74              
75 0 0         my ($top_link_key, $top_link_value) = $self->top_link_for_relname($relname)
76             or next;
77 0           $top_links{$top_link_key} = $top_link_value;
78              
79 0           my $rel_typename = $self->type_namer->type_name_for_result_class($rel_info->{class});
80              
81             $item_edit_rel_hooks{$relname} = sub {
82 0     0     my ($jsonapi_obj, $row) = @_;
83              
84 0           my $subitem = $row->$relname();
85              
86 0   0       my $compound_links_for_rel = $compound_links{$rel_typename} ||= {};
87              
88 0           my $link_keys;
89 0 0         if (not defined $subitem) {
    0          
    0          
90 0           $link_keys = undef;
91             }
92             elsif ($subitem->isa('DBIx::Class::ResultSet')) { # one-to-many rel
93 0           $link_keys = [];
94 0           while (my $subrow = $subitem->next) {
95 0           my $id = $subrow->id;
96 0           push @$link_keys, $id;
97 0           $compound_links_for_rel->{$id} = $self->render_item_as_jsonapi_hash($subrow); # XXX typename
98             }
99             }
100             elsif ($subitem->isa('DBIx::Class::Row')) { # one-to-many rel
101 0           $link_keys = $subitem->id;
102 0           $compound_links_for_rel->{$subitem->id} = $self->render_item_as_jsonapi_hash($subitem); # XXX typename
103             }
104             else {
105 0           die "panic: don't know how to handle $row $relname value $subitem";
106             }
107              
108 0           $jsonapi_obj->{links}{$rel_typename} = $link_keys;
109             }
110 0           }
111             }
112              
113             my $set_data = $self->render_set_as_array_of_jsonapi_resource_objects($set, undef, sub {
114 0     0     my ($jsonapi_obj, $row) = @_;
115 0           $_->($jsonapi_obj, $row) for values %item_edit_rel_hooks;
116 0           });
117              
118             # construct top document to return
119 0 0         my $top_set_key = ($self->param('distinct')) ? 'data' : $self->jsonapi_type;
120 0           my $top_doc = { # http://jsonapi.org/format/#document-structure-top-level
121             $top_set_key => $set_data,
122             };
123              
124 0 0         if (keys %top_links) {
125 0           $top_doc->{links} = \%top_links
126             }
127              
128 0 0         if (keys %compound_links) {
129             #Dwarn \%compound_links;
130 0           while ( my ($k, $v) = each %compound_links) {
131             # sort just for test stability,
132 0           $top_doc->{linked}{$k} = [ @{$v}{ sort keys %$v } ];
  0            
133             }
134             }
135              
136 0           my $total_items;
137 0 0 0       if (($self->param('with')||'') =~ /count/) { # XXX
138 0           $total_items = $set->pager->total_entries;
139 0           $top_doc->{meta}{count} = $total_items; # XXX detail not in spec
140             }
141              
142 0           return $top_doc;
143             }
144              
145              
146              
147             sub render_item_as_jsonapi_hash {
148 0     0 0   my ($self, $item) = @_;
149              
150 0           my $data = $self->render_item_as_plain_hash($item);
151              
152 0   0       $data->{id} //= $item->id;
153 0           $data->{type} = $self->type_namer->type_name_for_result_class($item->result_source->result_class);
154 0           $data->{href} = $self->path_for_item($item);
155              
156             #$self->_render_prefetch_jsonapi($item, $data, $_) for @{$self->prefetch||[]};
157              
158             # add links for relationships
159              
160 0           return $data;
161             }
162              
163              
164             sub _render_prefetch_jsonapi {
165 0     0     my ($self, $item, $data, $prefetch) = @_;
166              
167 0           while (my ($rel, $sub_rel) = each %{$prefetch}){
  0            
168 0 0         next if $rel eq 'self';
169              
170 0           my $subitem = $item->$rel();
171              
172 0 0         if (not defined $subitem) {
    0          
173 0           $data->{_embedded}{$rel} = undef; # show an explicit null from a prefetch
174             }
175             elsif ($subitem->isa('DBIx::Class::ResultSet')) { # one-to-many rel
176 0 0         my $rel_set_resource = $self->web_machine_resource(
177             set => $subitem,
178             item => undef,
179             prefetch => ref $sub_rel eq 'ARRAY' ? $sub_rel : [$sub_rel],
180             );
181 0           $data->{_embedded}{$rel} = $rel_set_resource->render_set_as_array_of_jsonapi_resource_objects($subitem, undef);
182             }
183             else {
184 0           $data->{_embedded}{$rel} = $self->render_item_as_plain_hash($subitem);
185             }
186             }
187             }
188              
189             sub render_set_as_array_of_jsonapi_resource_objects {
190 0     0 0   my ($self, $set, $render_method, $edit_hook) = @_;
191 0   0       $render_method ||= 'render_item_as_jsonapi_hash';
192              
193 0           my @jsonapi_objs;
194 0           while (my $row = $set->next) {
195 0           push @jsonapi_objs, $self->$render_method($row);
196 0 0         $edit_hook->($jsonapi_objs[-1], $row) if $edit_hook;
197             }
198              
199 0           return \@jsonapi_objs;
200             }
201              
202              
203              
204              
205             sub _jsonapi_page_links {
206 0     0     my ($self, $set, $base, $page_items, $total_items) = @_;
207              
208             # XXX we ought to allow at least the self link when not pages
209 0 0         return () unless $set->is_paged;
210              
211             # XXX we break encapsulation here, sadly, because calling
212             # $set->pager->current_page triggers a "select count(*)".
213             # XXX When we're using a later version of DBIx::Class we can use this:
214             # https://metacpan.org/source/RIBASUSHI/DBIx-Class-0.08208/lib/DBIx/Class/ResultSet/Pager.pm
215             # and do something like $rs->pager->total_entries(sub { 99999999 })
216 0 0         my $rows = $set->{attrs}{rows} or confess "panic: rows not set";
217 0 0         my $page = $set->{attrs}{page} or confess "panic: page not set";
218              
219             # XXX this self link this should probably be subtractive, ie include all
220             # params by default except any known to cause problems
221 0           my $url = $self->add_params_to_url($base, { distinct=>1, with=>1, me=>1 }, { rows => $rows });
222 0           my $linkurl = $url->as_string;
223 0           $linkurl .= "&page="; # hack to optimize appending page 5 times below
224              
225 0           my @link_kvs;
226 0           push @link_kvs, self => {
227             href => $linkurl.($page),
228             title => $set->result_class,
229             };
230 0 0         push @link_kvs, next => { href => $linkurl.($page+1) }
231             if $page_items == $rows;
232 0 0         push @link_kvs, prev => { href => $linkurl.($page-1) }
233             if $page > 1;
234 0 0         push @link_kvs, first => { href => $linkurl.1 }
235             if $page > 1;
236 0 0 0       push @link_kvs, last => { href => $linkurl.$set->pager->last_page }
237             if $total_items and $page != $set->pager->last_page;
238              
239 0           return @link_kvs;
240             }
241              
242              
243             1;
244              
245             __END__
246              
247             =pod
248              
249             =encoding UTF-8
250              
251             =head1 NAME
252              
253             WebAPI::DBIC::Resource::JSONAPI::Role::DBIC
254              
255             =head1 VERSION
256              
257             version 0.003002
258              
259             =head1 NAME
260              
261             WebAPI::DBIC::Resource::JSONAPI::Role::DBIC - a role with core JSON API methods for DBIx::Class resources
262              
263             =head1 AUTHOR
264              
265             Tim Bunce <Tim.Bunce@pobox.com>
266              
267             =head1 COPYRIGHT AND LICENSE
268              
269             This software is copyright (c) 2015 by Tim Bunce.
270              
271             This is free software; you can redistribute it and/or modify it under
272             the same terms as the Perl 5 programming language system itself.
273              
274             =cut