File Coverage

blib/lib/WebAPI/DBIC/Resource/Role/Relationship.pm
Criterion Covered Total %
statement 12 150 8.0
branch 0 98 0.0
condition 0 7 0.0
subroutine 4 11 36.3
pod 1 3 33.3
total 17 269 6.3


line stmt bran cond sub pod time code
1             package WebAPI::DBIC::Resource::Role::Relationship;
2             $WebAPI::DBIC::Resource::Role::Relationship::VERSION = '0.004001';
3              
4 2     2   24711919 use Devel::Dwarn;
  2         23301  
  2         26  
5 2     2   350 use Carp qw(confess);
  2         4  
  2         107  
6 2     2   1196 use Hash::Util qw(lock_keys);
  2         5187  
  2         17  
7              
8 2     2   1247 use Moo::Role;
  2         45102  
  2         11  
9              
10             requires 'uri_for';
11             requires 'id_column_values_for_item';
12             requires 'add_params_to_url';
13              
14              
15             # recurse into a prefetch-like structure invoking a callback
16             # XXX still a work in progress, only used by ActiveModule so far
17             sub traverse_prefetch {
18 0     0 0   my $self = shift;
19 0           my $set = shift;
20 0           my $parent_rel = shift;
21 0           my $prefetch = shift;
22 0           my $callback = shift;
23              
24 0 0         return unless $prefetch;
25              
26 0 0         if (not ref($prefetch)) { # leaf node
27 0           $callback->($self, $set, $parent_rel, $prefetch);
28 0           return;
29             }
30              
31 0 0         if (ref($prefetch) eq 'HASH') {
    0          
32 0           while (my ($prefetch_key, $prefetch_value) = each(%$prefetch)) {
33             #warn "traverse_prefetch [@$parent_rel] $prefetch\{$prefetch_key}\n";
34 0           $self->traverse_prefetch($set, $parent_rel, $prefetch_key, $callback);
35             # XXX traverse_prefetch first arg is a set but this passes a class:
36 0           my $result_subclass = $set->result_class->relationship_info($prefetch_key)->{class};
37 0           $self->traverse_prefetch($result_subclass, [ @$parent_rel, $prefetch_key ], $prefetch_value, $callback);
38             }
39             }
40             elsif (ref($prefetch) eq 'ARRAY') {
41 0           for my $sub_prefetch (@$prefetch) {
42 0           $self->traverse_prefetch($set, $parent_rel, $sub_prefetch, $callback);
43             }
44             }
45             else {
46 0           confess "Unsupported ref(prefetch): " . ref($prefetch);
47             }
48              
49 0           return;
50             }
51              
52              
53             our %_get_relationship_link_info_cache;
54              
55             sub _get_relationship_link_info_cached {
56 0   0 0     my $wrapped = $_get_relationship_link_info_cache{join "\t", @_} ||= [ _get_relationship_link_info(@_) ];
57 0           return $wrapped->[0];
58             }
59              
60              
61             sub _get_relationship_link_info {
62 0     0     my ($result_class, $relname) = @_;
63 0           my $rel = $result_class->relationship_info($relname);
64              
65 0           my $link_info = { # what we'll return
66             result_class => $rel->{source},
67             id_fields => undef,
68             id_filter => undef,
69             };
70 0           lock_keys(%$link_info);
71              
72 0           my $cond = $rel->{cond};
73              
74             # https://metacpan.org/pod/DBIx::Class::Relationship::Base#add_relationship
75 0 0         if (ref $cond eq 'CODE') {
76              
77              
78             my $bail = sub {
79 0   0 0     my ($inform) = shift || '';
80 0 0         unless (our $warn_once->{"$result_class $relname"}++) {
81 0           warn "$result_class relationship $relname has coderef-based condition which is not handled yet $inform\n";
82 0 0         Dwarn $rel if $ENV{WEBAPI_DBIC_DEBUG};
83             }
84 0           return undef;
85 0           };
86              
87             return sub {
88 0     0     my ($self, $code_cond_args) = @_;
89              
90 0           my ($crosstable_cond, $joinfree_cond) = $cond->({
91             self_alias => 'self', # alias of the invoking resultset ('me' in case of a result object),
92             foreign_alias => 'foreign', # alias of the to-be-joined resultset (often matches relname),
93             %$code_cond_args # eg self_resultsource, foreign_relname, self_rowobj
94             });
95             #Dwarn [ $crosstable_cond, $joinfree_cond ] unless our $warn_once->{"$result_class $relname dwarn"}++;
96              
97             # XXX herein we attempt the insane task of mapping SQL::Abstract conditions
98             # into something usable by WebAPI::DBIC - this is a total hack
99             # There is a better way: https://github.com/timbunce/WebAPI-DBIC/issues/8
100             # but it requires DBIC 0.082801+ (2014-10-05)
101              
102 0           for my $crosstable_cond_key (keys %$crosstable_cond) {
103 0           my $cond = $crosstable_cond->{$crosstable_cond_key};
104              
105             # first we look for the FK indentity field
106 0           my $ident;
107 0 0         if (ref $cond eq 'HASH') {
108 0 0 0       if ($cond->{'-ident'}) {
    0          
    0          
109             # "foreign.artist" => { "-ident" => "self.artistid" },
110 0           $ident = $cond->{'-ident'}
111             }
112             elsif (ref $cond->{'='} eq 'HASH' && $cond->{'='}{'-ident'}) {
113             # "foreign.artist" => { "=" => { "-ident" => "self.artistid" } }
114 0           $ident = $cond->{'='}{'-ident'};
115             }
116             elsif (ref $cond->{'='} eq 'SCALAR') {
117             # "foreign.artist" => { "=" => \"self.artistid" },
118 0           $ident = ${ $cond->{'='} };
  0            
119             }
120             }
121              
122 0 0         if ($ident) {
123 0 0         $ident =~ s/^self\.// or die "panic";
124 0           $link_info->{id_fields} = [ $ident ];
125             }
126             else {
127             # other kinds of conditions which we'll translate into me.field=foo url params
128 0           return $bail->('- unknown crosstable_cond_key');
129             }
130             }
131              
132 0 0         if ($joinfree_cond) {
133             # The join-free condition returned for relationship '$rel_name' must be a hash
134             # reference with all keys being valid columns on the related result source
135 0           return $bail->('- has join-free condition');
136             }
137              
138 0           return $link_info;
139 0           };
140             }
141              
142 0 0         if (ref $cond ne 'HASH') {
143             # we'll may end up silencing this warning till we can offer better support
144 0 0         unless (our $warn_once->{"$result_class $relname"}++) {
145 0           warn "$result_class relationship $relname cond value $cond not handled yet\n";
146 0 0         Dwarn $rel if $ENV{WEBAPI_DBIC_DEBUG};
147             }
148 0           return undef;
149             }
150              
151 0 0         if (keys %$cond > 1) {
152             # if we loosen this constraint we might need to recheck it for some cases below
153 0 0         unless (our $warn_once->{"$result_class $relname"}++) {
154 0           warn "$result_class relationship $relname ignored since it has multiple conditions\n";
155 0 0         Dwarn $rel if $ENV{WEBAPI_DBIC_DEBUG};
156             }
157 0           return undef;
158             }
159              
160             # TODO support and test more kinds of relationships
161             # TODO refactor
162              
163 0 0         if ($rel->{attrs}{accessor} eq 'multi') { # a 1-to-many relationship
164              
165             # XXX are there any cases we're not dealing with here?
166             # such as multi-colum FKs
167              
168 0 0         Dwarn $rel if $ENV{WEBAPI_DBIC_DEBUG};
169              
170 0           my $foreign_key = (keys %$cond)[0];
171 0 0         $foreign_key =~ s/^foreign\.//
172             or warn "Odd, no 'foreign.' prefix on $foreign_key ($result_class, $relname)";
173              
174             # express that we want to filter the many to match the key(s) of the 1
175             # here we list the names of the fields in the foreign table that correspond
176             # to the names of the id columns in the result_class table
177 0           $link_info->{id_filter} = [ $foreign_key ];
178 0           return $link_info;
179             }
180              
181             # accessor is the inflation type (single/filter/multi)
182 0 0         if ($rel->{attrs}{accessor} !~ /^(?: single | filter )$/x) {
183 0 0         unless (our $warn_once->{"$result_class $relname"}++) {
184 0           warn "$result_class relationship $relname ignored since we only support 'single' accessors (not $rel->{attrs}{accessor}) at the moment\n";
185 0 0         Dwarn $rel if $ENV{WEBAPI_DBIC_DEBUG};
186             }
187 0           return undef;
188             }
189              
190 0           my $fieldname = (values %$cond)[0]; # first and only value
191 0 0         $fieldname =~ s/^self\.// if $fieldname;
192              
193 0 0         if (not $fieldname) {
194 0 0         unless (our $warn_once->{"$result_class $relname"}++) {
195 0           warn "$result_class relationship $relname ignored since we can't determine a fieldname (@{[ %$cond ]})\n";
  0            
196 0 0         Dwarn $rel if $ENV{WEBAPI_DBIC_DEBUG};
197             }
198 0           return undef;
199             }
200              
201 0           $link_info->{id_fields} = [ $fieldname ];
202 0           return $link_info;
203             }
204              
205              
206              
207             sub get_url_for_item_relationship {
208 0     0 1   my ($self, $item, $relname) = @_;
209              
210 0           my $result_class = $item->result_class;
211              
212             #Dwarn
213 0 0         my $rel_link_info = _get_relationship_link_info_cached($result_class, $relname)
214             or return undef;
215              
216 0 0         if (ref $rel_link_info eq 'CODE') {
217 0 0         $rel_link_info = $rel_link_info->($self, {
218             self_resultsource => $item->result_source,
219             self_rowobj => $item,
220             foreign_relname => $relname, # XXX ?
221             })
222             or return undef;
223             }
224              
225 0           my @uri_for_args;
226 0 0         if ($rel_link_info->{id_fields}) { # link to an item (1-1)
227 0           my @id_kvs = map { $item->get_column($_) } @{ $rel_link_info->{id_fields} };
  0            
  0            
228 0 0         return undef if grep { not defined } @id_kvs; # no link because a key value is null
  0            
229 0           push @uri_for_args, map { $_ => shift @id_kvs } 1..@id_kvs;
  0            
230             }
231              
232 0 0         my $dst_class = $rel_link_info->{result_class} or die "panic";
233 0           push @uri_for_args, result_class => $dst_class;
234              
235 0           my $linkurl = $self->uri_for( @uri_for_args );
236              
237 0 0         if (not $linkurl) { # XXX this comment is HAL-specific
238 0 0         warn "Result source $dst_class has no resource uri in this app so relations (like $result_class $relname) won't have _links for it.\n"
239             unless our $warn_once->{"$result_class $relname $dst_class"}++;
240 0           return undef;
241             }
242              
243 0           my %params;
244 0 0         if (my $id_filter = $rel_link_info->{id_filter}) {
245 0           my @id_vals = $self->id_column_values_for_item($item);
246 0 0         die "panic" if @id_vals != @$id_filter;
247 0           for my $id_field (@$id_filter) {
248 0           $params{ "me.".$id_field } = shift @id_vals;
249             }
250             }
251              
252 0           my $href = $self->add_params_to_url(
253             $linkurl,
254             {},
255             \%params,
256             );
257              
258 0           return $href;
259             }
260              
261              
262             sub get_url_template_for_set_relationship { # XXX hack, for jsonapi, move?
263 0     0 0   my ($self, $set, $relname) = @_;
264              
265 0           my $result_class = $set->result_class;
266 0           my $result_source = $set->result_source;
267              
268             #Dwarn
269 0 0         my $rel_link_info = _get_relationship_link_info_cached($result_class, $relname)
270             or return undef;
271              
272 0 0         if (ref $rel_link_info eq 'CODE') {
273 0 0         $rel_link_info = $rel_link_info->($self, {
274             self_resultsource => $result_source,
275             self_rowobj => undef, # XXX
276             foreign_relname => $relname, # XXX ?
277             })
278             or return undef;
279             }
280              
281 0           my @uri_for_args;
282 0 0         if ($rel_link_info->{id_fields}) { # link to an item (1-1)
283 0           my @id_kvs = @{ $rel_link_info->{id_fields} };
  0            
284 0           push @uri_for_args, map {
285 0           my $name = shift @id_kvs;
286 0           $_ => "{$relname.$name}"
287             } 1..@id_kvs;
288             }
289              
290 0 0         my $dst_class = $rel_link_info->{result_class} or die "panic";
291 0           push @uri_for_args, result_class => $dst_class;
292              
293 0           my $linkurl = $self->uri_for( @uri_for_args );
294              
295 0 0         if (not $linkurl) {
296 0 0         warn "Result source $dst_class has no resource uri in this app so relations (like $result_class $relname) won't have _links for it.\n"
297             unless our $warn_once->{"$result_class $relname $dst_class"}++;
298 0           return undef;
299             }
300              
301 0           my %params;
302 0 0         if (my $id_filter = $rel_link_info->{id_filter}) {
303 0           my @names = $result_source->unique_constraint_columns( $self->id_unique_constraint_name );
304 0 0         die "panic" if @names != @$id_filter;
305 0           for my $id_field (@$id_filter) {
306 0           my $name = shift @names;
307 0           $params{ "me.".$id_field } = "{$relname.$name}";
308             }
309             }
310              
311 0           my $href = $self->add_params_to_url(
312             $linkurl,
313             {},
314             \%params,
315             );
316              
317 0           return $href;
318             }
319              
320             1;
321              
322             __END__
323              
324             =pod
325              
326             =encoding UTF-8
327              
328             =head1 NAME
329              
330             WebAPI::DBIC::Resource::Role::Relationship
331              
332             =head1 VERSION
333              
334             version 0.004001
335              
336             =head1 NAME
337              
338             WebAPI::DBIC::Resource::Role::Relationship - methods relating to relationships between resources
339              
340             =begin example
341              
342              
343              
344              
345             =end example
346              
347             $Data::Dumper::Deparse = 1;
348             return {
349             "$$args{'foreign_alias'}.artist", {-'ident', "$$args{'self_alias'}.artistid"},
350             "$$args{'foreign_alias'}.year", 1984
351             },
352             $$args{'self_resultobj'} && {
353             "$$args{'foreign_alias'}.artist", $$args{'self_resultobj'}->artistid,
354             "$$args{'foreign_alias'}.year", 1984
355             };
356              
357             =head2 get_url_for_item_relationship
358              
359             $url = $self->get_url_for_item_relationship($item, $relname);
360              
361             Given a specific item and relationship name return a url for the related
362             records, if possible else return undef.
363              
364             =head1 AUTHOR
365              
366             Tim Bunce <Tim.Bunce@pobox.com>
367              
368             =head1 COPYRIGHT AND LICENSE
369              
370             This software is copyright (c) 2015 by Tim Bunce.
371              
372             This is free software; you can redistribute it and/or modify it under
373             the same terms as the Perl 5 programming language system itself.
374              
375             =cut