Commit | Line | Data |
7adfd53f |
1 | package Reaction::InterfaceModel::DBIC::ObjectClass; |
2 | |
3 | use Reaction::ClassExporter; |
4 | use Reaction::Class; |
5 | use aliased 'Reaction::InterfaceModel::DBIC::Collection'; |
6 | use Class::MOP; |
7 | |
8 | use aliased 'Reaction::InterfaceModel::Action::DBIC::ResultSet::Create'; |
9 | use aliased 'Reaction::InterfaceModel::Action::DBIC::Result::Update'; |
10 | use aliased 'Reaction::InterfaceModel::Action::DBIC::Result::Delete'; |
11 | |
12 | use aliased 'Reaction::Meta::InterfaceModel::Action::Class' => 'ActionClass'; |
13 | |
14 | class ObjectClass, is 'Reaction::InterfaceModel::ObjectClass', which { |
15 | override exports_for_package => sub { |
16 | my ($self, $package) = @_; |
17 | my %exports = $self->SUPER::exports_for_package($package); |
18 | |
19 | $exports{reflect_actions} = sub { |
20 | |
21 | my %actions = @_; |
22 | my $meta = $package->meta; |
23 | my $defaults = { |
24 | 'Create' => { base => Create }, |
25 | 'Update' => { base => Update }, |
26 | 'Delete' => { base => Delete }, |
27 | }; |
28 | |
29 | while (my($name,$opts) = each %actions) { |
30 | my $action_class = delete $opts->{class} || |
31 | $package->_default_action_class_for($name); |
32 | |
33 | #support this for now, I don't know about defaults yet though. |
34 | #especially, '*' for all writtable attributes. ugh |
35 | my $super = delete $opts->{base} || $defaults->{$name}->{base} || []; |
36 | my $attrs = delete $opts->{attrs} || []; |
37 | $super = (ref($super) ne 'ARRAY' && $super) ? [ $super ] : []; |
38 | |
39 | $self->reflect_action($meta, $action_class, $super, $attrs); |
40 | } |
41 | }; |
42 | |
43 | |
44 | my $orig_domain_model = delete $exports{domain_model}; |
45 | $exports{domain_model} = sub { |
46 | my($dm_name, %opts) = @_; |
47 | |
48 | my $reflect = delete $opts{reflect}; |
49 | my $inflate_result = delete $opts{inflate_result}; |
50 | |
51 | my @attr_names = map {ref $_ ? $_->[0] : $_ } @$reflect; |
52 | $opts{reflect} = [@attr_names]; |
53 | $orig_domain_model->($dm_name, %opts); |
54 | |
55 | #Create an inflate result_method for DBIC objects |
56 | my $meta = $package->meta; |
57 | if ($inflate_result) { |
58 | my $inflate = sub { |
59 | my $class = shift; my ($source) = @_; |
60 | if($source->isa('DBIx::Class::ResultSourceHandle')) |
61 | { |
62 | $source = $source->resolve; |
63 | } |
64 | return $class->new |
65 | ($dm_name, $source->result_class->inflate_result(@_)); |
66 | }; |
67 | $meta->add_method('inflate_result', $inflate); |
68 | } |
69 | |
70 | #relationship magic |
71 | my %rel_attrs = map{ @$_ } grep {ref $_} @$reflect; |
72 | my $dm_meta = $opts{isa}->meta; |
73 | |
74 | for my $attr_name ( @attr_names ) { |
75 | |
76 | my $from_attr = $dm_meta->find_attribute_by_name($attr_name); |
77 | confess "Failed to get attribute $attr_name from class $opts{isa}" |
78 | unless $from_attr; |
79 | |
80 | if ( my $info = $opts{isa}->result_source_instance |
81 | ->relationship_info($attr_name) ) { |
82 | |
83 | next unless(my $rel_accessor = $info->{attrs}->{accessor}); |
84 | |
85 | unless ( $rel_attrs{$attr_name} ) { |
86 | my ($im_class) = ($package =~ /^(.*)::\w+$/); |
87 | my ($rel_class) = ($attr_name =~ /^(.*?)(_list)?$/); |
88 | $rel_class = join '', map{ ucfirst($_) } split '_', $rel_class; |
89 | $rel_attrs{$attr_name} = "${im_class}::${rel_class}"; |
90 | } |
91 | Class::MOP::load_class($rel_attrs{$attr_name}) || |
92 | confess "Could not load ".$rel_attrs{$attr_name}; |
93 | |
94 | #has_many rels |
95 | if ($rel_accessor eq 'multi' && |
96 | ( $from_attr->type_constraint->name eq 'ArrayRef' || |
97 | $from_attr->type_constraint->is_subtype_of('ArrayRef') ) |
98 | ) { |
99 | |
100 | # # remove the old attribute and recreate it with new isa |
101 | my %attr_opts = ( is => 'ro', |
102 | lazy_build => 1, |
103 | isa => Collection, |
104 | clearer => "_clear_${attr_name}", |
105 | domain_model => $dm_name, |
106 | orig_attr_name => $attr_name, |
107 | ); |
108 | $meta->add_attribute( $attr_name, %attr_opts); |
109 | |
110 | #remove old build and add a better one |
111 | #proper collections will remove the result_class uglyness. |
112 | my $build_method = sub { |
113 | my $rs = shift->$dm_name->search_related_rs |
114 | ($attr_name, {}, |
115 | { |
116 | result_class => $rel_attrs{$attr_name} }); |
117 | return bless($rs => Collection); |
118 | }; |
119 | $meta->remove_method( "build_${attr_name}"); |
120 | $meta->add_method( "build_${attr_name}", $build_method); |
121 | } elsif ($rel_accessor eq 'single') { |
122 | # # remove the old attribute and recreate it with new isa |
123 | my %attr_opts = ( is => 'ro', |
124 | lazy_build => 1, |
125 | isa => $rel_attrs{$attr_name}, |
126 | clearer => "_clear_${attr_name}", |
127 | domain_model => $dm_name, |
128 | orig_attr_name => $attr_name, |
129 | ); |
130 | $meta->add_attribute( $attr_name, %attr_opts); |
131 | |
132 | #delete and recreate the build method to properly inflate the |
133 | #result into an IM::O class instead of the original |
134 | #this probably needs some cleaning |
135 | #proper collections will remove the result_class uglyness. |
136 | my $build_method = sub { |
137 | shift->$dm_name->find_related |
138 | ($attr_name, {}, |
139 | { |
140 | result_class => $rel_attrs{$attr_name}}); |
141 | }; |
142 | $meta->remove_method( "build_${attr_name}"); |
143 | $meta->add_method( "build_${attr_name}", $build_method); |
144 | } |
145 | } elsif ( $from_attr->type_constraint->name eq 'ArrayRef' || |
146 | $from_attr->type_constraint->is_subtype_of('ArrayRef') |
147 | ) { |
148 | #m2m magicness |
149 | next unless $attr_name =~ m/^(.*)_list$/; |
150 | my $mm_name = $1; |
151 | my ($hm_source, $far_side); |
152 | # we already get one for the rel info check, unify that?? |
153 | my $source = $opts{isa}->result_source_instance; |
154 | eval { $hm_source = $source->related_source("links_to_${mm_name}_list"); } |
155 | || confess "Can't find links_to_${mm_name}_list has_many for ${mm_name}_list"; |
156 | eval { $far_side = $hm_source->related_source($mm_name); } |
157 | || confess "Can't find ${mm_name} belongs_to on ".$hm_source->result_class |
158 | ." traversing many-many for ${mm_name}_list"; |
159 | |
160 | # # remove the old attribute and recreate it with new isa |
161 | my %attr_opts = ( is => 'ro', |
162 | lazy_build => 1, |
163 | isa => Collection, |
164 | clearer => "_clear_${attr_name}", |
165 | domain_model => $dm_name, |
166 | orig_attr_name => $attr_name, |
167 | ); |
168 | $meta->add_attribute( $attr_name, %attr_opts); |
169 | |
170 | #proper collections will remove the result_class uglyness. |
171 | my $build_method = sub { |
172 | my $rs = shift->$dm_name->result_source |
173 | ->related_source("links_to_${mm_name}_list") |
174 | ->related_source(${mm_name}) |
175 | ->resultset->search_rs |
176 | ({},{result_class => $rel_attrs{$attr_name} }); |
177 | return bless($rs => Collection); |
178 | }; |
179 | $meta->remove_method( "build_${attr_name}"); |
180 | $meta->add_method( "build_${attr_name}", $build_method); |
181 | } |
182 | } |
183 | }; |
184 | return %exports; |
185 | }; |
186 | }; |
187 | |
188 | |
189 | sub reflect_action{ |
190 | my($self, $meta, $action_class, $super, $attrs) = @_; |
191 | |
192 | Class::MOP::load_class($_) for @$super; |
193 | |
194 | #create the class |
195 | my $ok = eval { Class::MOP::load_class($action_class) }; |
196 | |
197 | confess("Class '${action_class}' does not seem to support method 'meta'") |
198 | if $ok && !$action_class->can('meta'); |
199 | |
200 | my $action_meta = $ok ? |
201 | $action_class->meta : ActionClass->create($action_class, superclasses => $super); |
202 | |
203 | $action_meta->make_mutable if $action_meta->is_immutable; |
204 | |
205 | foreach my $attr_name (@$attrs){ |
206 | my $attr = $meta->find_attribute_by_name($attr_name); |
207 | my $dm_isa = $meta->find_attribute_by_name($attr->domain_model)->_isa_metadata; |
208 | my $from_attr = $dm_isa->meta->find_attribute_by_name($attr->orig_attr_name); |
209 | |
210 | #Don't reflect read-only attributes to actions |
211 | if ($from_attr->_is_metadata ne 'rw') { |
212 | warn("Not relecting read-only attribute ${attr_name} to ${action_class}"); |
213 | next; |
214 | } |
215 | |
216 | #add the attribute to the class |
217 | $action_class->meta->add_attribute |
218 | ( $attr_name => |
219 | $self->reflected_attr_opts($meta, $dm_isa, $from_attr) |
220 | ); |
221 | } |
222 | |
223 | $action_class->meta->make_immutable; |
224 | } |
225 | |
226 | sub reflected_attr_opts{ |
227 | my ($self, $meta, $dm, $attr) = @_; |
228 | my $attr_name = $attr->name; |
229 | |
230 | my %opts = ( |
231 | is => 'rw', |
232 | isa => $attr->_isa_metadata, |
233 | required => $attr->is_required, |
234 | predicate => "has_${attr_name}", |
235 | ); |
236 | |
237 | if ($opts{required}) { |
238 | $opts{default} = !$attr->has_default ? |
239 | sub{confess("${attr_name} must be provided before calling reader")} |
240 | : $attr->default; |
241 | $opts{lazy} = 1; |
242 | } |
243 | |
244 | #test for relationships |
245 | my $source = $dm->result_source_instance; |
246 | my $constraint = $attr->type_constraint; |
247 | if (my $info = $source->relationship_info($attr_name)) { |
248 | if ( $info->{attrs}->{accessor} && |
249 | $info->{attrs}->{accessor} eq 'multi') { |
250 | confess "${attr_name} is multi and rw. we are confoos."; |
251 | } else { |
252 | $opts{valid_values} = sub { |
253 | $_[0]->target_model->result_source |
254 | ->related_source($attr_name)->resultset; |
255 | }; |
256 | } |
257 | } elsif ($constraint->name eq 'ArrayRef' || |
258 | $constraint->is_subtype_of('ArrayRef')) { |
259 | # it's a many-many. time for some magic. |
260 | my $link_rel = "links_to_${attr_name}"; |
261 | my ($mm_name) = ($attr_name =~ m/^(.*)_list$/); |
262 | confess "Many-many attr must be called <name>_list for reflection" |
263 | unless $mm_name; |
264 | |
265 | my ($hm_source, $far_side); |
266 | eval { $hm_source = $source->related_source($link_rel); } |
267 | || confess "Can't find ${link_rel} has_many for ${attr_name}"; |
268 | eval { $far_side = $hm_source->related_source($mm_name); } |
269 | || confess "Can't find ${mm_name} belongs_to on " . |
270 | $hm_source->result_class." traversing many-many for ${attr_name}"; |
271 | |
272 | $opts{default} = sub { [] }; |
273 | $opts{valid_values} = sub { |
274 | $_[0]->target_model->result_source->related_source($link_rel) |
275 | ->related_source($mm_name)->resultset; |
276 | }; |
277 | } |
278 | |
279 | return \%opts; |
280 | } |
281 | |
282 | 1; |
283 | |
284 | =head1 NAME |
285 | |
286 | Reaction::InterfaceModel::DBIC::ObjectClass |
287 | |
288 | =head1 SYNOPSIS |
289 | |
290 | =head2 domain_model |
291 | |
292 | package Prefab::AdminModel::User; |
293 | |
294 | class User, is Object, which{ |
295 | #create an attribute _user_store with type constraint MyApp::DB::User |
296 | domain_model '_user_store' => |
297 | (isa => 'MyApp::DB::User', |
298 | #mirror the following attributes from MyApp::DB::User |
299 | #will create collections for rels which use result_classes of: |
300 | # Prefab::AdminModel::(Group|ImagedDocument) |
301 | # Prefab::AdminModel::DocumentNotes |
302 | reflect => [qw/id username password created_d group_list imaged_document/, |
303 | [doc_notes_list => 'Prefab::AdminModel::DocumentNotes'] |
304 | ], |
305 | #automatically add a sub inflate_result that inflates the DBIC obj |
306 | #to a Prefab::AdminModel::User with the dbic obj in _user_store |
307 | inflate_result => 1, |
308 | ); |
309 | }; |
310 | |
311 | =head2 reflect_actions |
312 | |
313 | reflect_actions |
314 | ( |
315 | Create => { attrs =>[qw(first_name last_name baz_list)] }, |
316 | Update => { attrs =>[qw(first_name last_name baz_list)] }, |
317 | Delete => {}, |
318 | ); |
319 | |
320 | =head1 DESCRIPTION |
321 | |
322 | =head1 ATTRIBUTES |
323 | |
324 | =head2 isa |
325 | |
326 | =head2 reflect |
327 | |
328 | =head2 inflate_result |
329 | |
330 | =head2 handles |
331 | |
332 | =head1 METHODS |
333 | |
334 | =head2 reflect_actions |
335 | |
336 | =head1 AUTHORS |
337 | |
338 | See L<Reaction::Class> for authors. |
339 | |
340 | =head1 LICENSE |
341 | |
342 | See L<Reaction::Class> for the license. |
343 | |
344 | =cut |