moved shit to trunk
[catagits/Reaction.git] / lib / Reaction / InterfaceModel / DBIC / ObjectClass.pm
CommitLineData
7adfd53f 1package Reaction::InterfaceModel::DBIC::ObjectClass;
2
3use Reaction::ClassExporter;
4use Reaction::Class;
5use aliased 'Reaction::InterfaceModel::DBIC::Collection';
6use Class::MOP;
7
8use aliased 'Reaction::InterfaceModel::Action::DBIC::ResultSet::Create';
9use aliased 'Reaction::InterfaceModel::Action::DBIC::Result::Update';
10use aliased 'Reaction::InterfaceModel::Action::DBIC::Result::Delete';
11
12use aliased 'Reaction::Meta::InterfaceModel::Action::Class' => 'ActionClass';
13
14class 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
189sub 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
226sub 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
2821;
283
284=head1 NAME
285
286Reaction::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
338See L<Reaction::Class> for authors.
339
340=head1 LICENSE
341
342See L<Reaction::Class> for the license.
343
344=cut