File Coverage

blib/lib/WebAPI/DBIC/Resource/Role/Relationship.pm
Criterion Covered Total %
statement 9 129 6.9
branch 0 90 0.0
condition 0 7 0.0
subroutine 3 9 33.3
pod 1 2 50.0
total 13 237 5.4


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