r31711@martha (orig r1246): groditi | 2009-10-02 17:01:17 -0400
[catagits/Reaction.git] / lib / Reaction / InterfaceModel / Reflector / DBIC.pm
CommitLineData
7adfd53f 1package Reaction::InterfaceModel::Reflector::DBIC;
2
3use aliased 'Reaction::InterfaceModel::Action::DBIC::ResultSet::Create';
b8faba69 4use aliased 'Reaction::InterfaceModel::Action::DBIC::ResultSet::DeleteAll';
7adfd53f 5use aliased 'Reaction::InterfaceModel::Action::DBIC::Result::Update';
6use aliased 'Reaction::InterfaceModel::Action::DBIC::Result::Delete';
7
8use aliased 'Reaction::InterfaceModel::Collection::Virtual::ResultSet';
9use aliased 'Reaction::InterfaceModel::Object';
10use aliased 'Reaction::InterfaceModel::Action';
11use Reaction::Class;
12use Class::MOP;
13
f670cfd0 14use Catalyst::Utils;
15
81393881 16use namespace::clean -except => [ qw(meta) ];
17
81393881 18has make_classes_immutable => (isa => "Bool", is => "rw", required => 1, default => sub{ 1 });
19
20#user defined actions and prototypes
21has object_actions => (isa => "HashRef", is => "rw", lazy_build => 1);
22has collection_actions => (isa => "HashRef", is => "rw", lazy_build => 1);
23
24#which actions to create by default
25has default_object_actions => (isa => "ArrayRef", is => "rw", lazy_build => 1);
26has default_collection_actions => (isa => "ArrayRef", is => "rw", lazy_build => 1);
27
28#builtin actions and prototypes
29has builtin_object_actions => (isa => "HashRef", is => "rw", lazy_build => 1);
30has builtin_collection_actions => (isa => "HashRef", is => "rw", lazy_build => 1);
31sub _build_object_actions { {} };
32sub _build_collection_actions { {} };
33sub _build_default_object_actions { [ qw/Update Delete/ ] };
34sub _build_default_collection_actions { [ qw/Create DeleteAll/ ] };
35sub _build_builtin_object_actions {
36 {
37 Update => { name => 'Update', base => Update },
38 Delete => { name => 'Delete', base => Delete, attributes => [] },
84135177 39 };
81393881 40};
41sub _build_builtin_collection_actions {
42 {
43 Create => {name => 'Create', base => Create },
44 DeleteAll => {name => 'DeleteAll', base => DeleteAll, attributes => [] }
84135177 45 };
81393881 46};
47sub _all_object_actions {
48 my $self = shift;
49 return $self->merge_hashes
50 ($self->builtin_object_actions, $self->object_actions);
51};
52sub _all_collection_actions {
53 my $self = shift;
54 return $self->merge_hashes
55 ($self->builtin_collection_actions, $self->collection_actions);
56};
57sub dm_name_from_class_name {
58 my($self, $class) = @_;
59 confess("wrong arguments") unless $class;
60 $class =~ s/::/_/g;
61 $class = "_" . $self->_class_to_attribute_name($class) . "_store";
62 return $class;
63};
64sub dm_name_from_source_name {
65 my($self, $source) = @_;
66 confess("wrong arguments") unless $source;
67 $source =~ s/([a-z0-9])([A-Z])/${1}_${2}/g ;
68 $source = "_" . $self->_class_to_attribute_name($source) . "_store";
69 return $source;
70};
71sub class_name_from_source_name {
72 my ($self, $model_class, $source_name) = @_;
73 confess("wrong arguments") unless $model_class && $source_name;
74 return join "::", $model_class, $source_name;
75};
76sub class_name_for_collection_of {
77 my ($self, $object_class) = @_;
78 confess("wrong arguments") unless $object_class;
79 return "${object_class}::Collection";
80};
81sub merge_hashes {
82 my($self, $left, $right) = @_;
83 return Catalyst::Utils::merge_hashes($left, $right);
84};
85sub parse_reflect_rules {
86 my ($self, $rules, $haystack) = @_;
87 confess('$rules must be an array reference') unless ref $rules eq 'ARRAY';
88 confess('$haystack must be an array reference') unless ref $haystack eq 'ARRAY';
89
90 my $needles = {};
91 my (@exclude, @include, $global_opts);
92 if(@$rules == 2 && $rules->[0] eq '-exclude'){
93 push(@exclude, (ref $rules->[1] eq 'ARRAY' ? @{$rules->[1]} : $rules->[1]));
94 } else {
84135177 95 for my $rule ( @$rules ){
81393881 96 if (ref $rule eq 'ARRAY' && $rule->[0] eq '-exclude'){
97 push(@exclude, (ref $rule->[1] eq 'ARRAY' ? @{$rule->[1]} : $rule->[1]));
98 } elsif( ref $rule eq 'HASH' ){
99 $global_opts = ref $global_opts eq 'HASH' ?
100 $self->merge_hashes($global_opts, $rule) : $rule;
101 } else {
102 push(@include, $rule);
84135177 103 }
104 }
81393881 105 }
106 my $check_exclude = sub{
107 for my $rule (@exclude){
108 return 1 if(ref $rule eq 'Regexp' ? $_[0] =~ /$rule/ : $_[0] eq $rule);
109 }
110 return;
f670cfd0 111 };
112
81393881 113 @$haystack = grep { !$check_exclude->($_) } @$haystack;
114 $self->merge_reflect_rules(\@include, $needles, $haystack, $global_opts);
115 return $needles;
116};
117sub merge_reflect_rules {
118 my ($self, $rules, $needles, $haystack, $local_opts) = @_;
119 for my $rule ( @$rules ){
120 if(!ref $rule && ( grep {$rule eq $_} @$haystack ) ){
121 $needles->{$rule} = defined $needles->{$rule} ?
122 $self->merge_hashes($needles->{$rule}, $local_opts) : $local_opts;
123 } elsif( ref $rule eq 'Regexp' ){
124 for my $match ( grep { /$rule/ } @$haystack ){
125 $needles->{$match} = defined $needles->{$match} ?
126 $self->merge_hashes($needles->{$match}, $local_opts) : $local_opts;
127 }
128 } elsif( ref $rule eq 'ARRAY' ){
129 my $opts;
130 $opts = pop(@$rule) if @$rule > 1 and ref $rule->[$#$rule] eq 'HASH';
131 $opts = $self->merge_hashes($local_opts, $opts) if defined $local_opts;
132 $self->merge_reflect_rules($rule, $needles, $haystack, $opts);
f670cfd0 133 }
81393881 134 }
135};
136sub reflect_schema {
137 my ($self, %opts) = @_;
138 my $base = delete $opts{base} || Object;
27f4dfaa 139 my $roles = delete $opts{roles} || [];
81393881 140 my $model = delete $opts{model_class};
141 my $schema = delete $opts{schema_class};
142 my $dm_name = delete $opts{domain_model_name};
143 my $dm_args = delete $opts{domain_model_args} || {};
144 $dm_name ||= $self->dm_name_from_class_name($schema);
145
146 #load all necessary classes
147 confess("model_class and schema_class are required parameters")
148 unless($model && $schema);
149 Class::MOP::load_class( $base );
150 Class::MOP::load_class( $schema );
27f4dfaa 151 my $meta = $self->_load_or_create(
152 $model,
153 superclasses => [$base],
154 ( @$roles ? (roles => $roles) : ()),
155 );
22a2b243 156
81393881 157 # sources => undef, #default to qr/./
158 # sources => [], #default to nothing
159 # sources => qr//, #DWIM, treated as [qr//]
160 # sources => [{...}] #DWIM, treat as [qr/./, {...} ]
161 # sources => [[-exclude => ...]] #DWIM, treat as [qr/./, [-exclude => ...]]
162 my $haystack = [ $schema->sources ];
163
27f4dfaa 164 my $rules = delete $opts{sources};
81393881 165 if(!defined $rules){
166 $rules = [qr/./];
167 } elsif( ref $rules eq 'Regexp'){
168 $rules = [ $rules ];
169 } elsif( ref $rules eq 'ARRAY' && @$rules){
170 #don't add a qr/./ rule if we have at least one match rule
171 push(@$rules, qr/./) unless grep {(ref $_ eq 'ARRAY' && $_->[0] ne '-exclude')
172 || !ref $_ || ref $_ eq 'Regexp'} @$rules;
173 }
174
175 my $sources = $self->parse_reflect_rules($rules, $haystack);
176
177 my $make_immutable = $meta->is_immutable || $self->make_classes_immutable;
178 $meta->make_mutable if $meta->is_immutable;
179
180 $meta->add_domain_model
181 ($dm_name, is => 'rw', isa => $schema, required => 1, %$dm_args);
182
183 for my $source_name (keys %$sources){
184 my $source_opts = $sources->{$source_name} || {};
185 $self->reflect_source(
186 source_name => $source_name,
187 parent_class => $model,
188 schema_class => $schema,
189 source_class => $schema->class($source_name),
190 parent_domain_model_name => $dm_name,
191 %$source_opts
192 );
193 }
194
195 $meta->make_immutable if $make_immutable;
196 return $meta;
197};
198sub _compute_source_options {
199 my ($self, %opts) = @_;
200 my $schema = delete $opts{schema_class};
201 my $source_name = delete $opts{source_name};
202 my $source_class = delete $opts{source_class};
203 my $parent = delete $opts{parent_class};
204 my $parent_dm = delete $opts{parent_domain_model_name};
205
206 #this is the part where I hate my life for promissing all sorts of DWIMery
207 confess("parent_class and source_name or source_class are required parameters")
208 unless($parent && ($source_name || $source_class));
209
210OUTER: until( $schema && $source_name && $source_class && $parent_dm ){
211 if( $schema && !$source_name){
c19feb34 212 next OUTER if $source_name = $schema->source($source_class)->source_name;
81393881 213 } elsif( $schema && !$source_class){
214 next OUTER if $source_class = eval { $schema->class($source_name) };
7adfd53f 215 }
216
c19feb34 217 my @haystack = $parent_dm ? $parent->meta->find_attribute_by_name($parent_dm) : ();
81393881 218
219 #there's a lot of guessing going on, but it should work fine on most cases
220 INNER: for my $needle (@haystack){
221 my $isa = $needle->_isa_metadata;
222 next INNER unless Class::MOP::load_class( $isa->_isa_metadata );
223 next INNER unless $isa->isa('DBIx::Class::Schema');
f670cfd0 224
81393881 225 if( $source_name ){
226 my $src_class = eval{ $isa->class($source_name) };
227 next INNER unless $src_class;
228 next INNER if($source_class && $source_class ne $src_class);
229 $schema = $isa;
230 $parent_dm = $needle->name;
231 $source_class = $src_class;
232 next OUTER;
f670cfd0 233 }
f670cfd0 234 }
235
81393881 236
237 confess("Could not determine options automatically from: schema " .
238 "'${schema}', source_name '${source_name}', source_class " .
239 "'${source_class}', parent_domain_model_name '${parent_dm}'");
240 }
241
242 return {
243 source_name => $source_name,
244 schema_class => $schema,
245 source_class => $source_class,
246 parent_class => $parent,
247 parent_domain_model_name => $parent_dm,
248 };
249};
250sub _class_to_attribute_name {
251 my ( $self, $str ) = @_;
252 confess("wrong arguments passed for _class_to_attribute_name") unless $str;
253 return join('_', map lc, split(/::|(?<=[a-z0-9])(?=[A-Z])/, $str))
254};
255sub add_source {
256 my ($self, %opts) = @_;
257
258 my $model = delete $opts{model_class};
259 my $reader = delete $opts{reader};
260 my $source = delete $opts{source_name};
261 my $dm_name = delete $opts{domain_model_name};
262 my $collection = delete $opts{collection_class};
263 my $name = delete $opts{attribute_name} || $source;
264
265 confess("model_class and source_name are required parameters")
266 unless $model && $source;
267 my $meta = $model->meta;
268
269 unless( $collection ){
270 my $object = $self->class_name_from_source_name($model, $source);
271 $collection = $self->class_name_for_collection_of($object);
272 }
273 unless( $reader ){
274 $reader = $source;
275 $reader =~ s/([a-z0-9])([A-Z])/${1}_${2}/g ;
276 $reader = $self->_class_to_attribute_name($reader) . "_collection";
277 }
278 unless( $dm_name ){
279 my @haystack = $meta->domain_models;
280 if( @haystack > 1 ){
281 @haystack = grep { $_->_isa_metadata->isa('DBIx::Class::Schema') } @haystack;
7adfd53f 282 }
81393881 283 if(@haystack == 1){
284 $dm_name = $haystack[0]->name;
285 } elsif(@haystack > 1){
286 confess("Failed to automatically determine domain_model_name. More than one " .
287 "possible match (".(join ", ", map{"'".$_->name."'"} @haystack).")");
288 } else {
289 confess("Failed to automatically determine domain_model_name. No matches.");
f670cfd0 290 }
81393881 291 }
292
293 my %attr_opts =
294 (
295 lazy => 1,
296 required => 1,
297 isa => $collection,
298 reader => $reader,
299 predicate => "has_" . $self->_class_to_attribute_name($name) ,
300 domain_model => $dm_name,
301 orig_attr_name => $source,
302 default => sub {
303 $collection->new
304 (
305 _source_resultset => $_[0]->$dm_name->resultset($source),
306 _parent => $_[0],
307 );
308 },
309 );
310
311 my $make_immutable = $meta->is_immutable;
312 $meta->make_mutable if $make_immutable;
313 my $attr = $meta->add_attribute($name, %attr_opts);
314 $meta->make_immutable if $make_immutable;
315
316 return $attr;
317};
318sub reflect_source {
319 my ($self, %opts) = @_;
320 my $collection = delete $opts{collection} || {};
321 %opts = %{ $self->merge_hashes(\%opts, $self->_compute_source_options(%opts)) };
322
323 my $obj_meta = $self->reflect_source_object(%opts);
324 my $col_meta = $self->reflect_source_collection
325 (
326 object_class => $obj_meta->name,
327 source_class => $opts{source_class},
c19feb34 328 schema => $opts{schema_class},
81393881 329 %$collection
330 );
331
332 $self->add_source(
333 %opts,
334 model_class => delete $opts{parent_class},
335 domain_model_name => delete $opts{parent_domain_model_name},
336 collection_class => $col_meta->name,
337 );
338};
339sub reflect_source_collection {
340 my ($self, %opts) = @_;
341 my $base = delete $opts{base} || ResultSet;
27f4dfaa 342 my $roles = delete $opts{roles} || [];
81393881 343 my $class = delete $opts{class};
344 my $object = delete $opts{object_class};
345 my $source = delete $opts{source_class};
346 my $action_rules = delete $opts{actions};
c19feb34 347 my $schema = delete $opts{schema};
81393881 348
349 confess('object_class and source_class are required parameters')
350 unless $object && $source;
351 $class ||= $self->class_name_for_collection_of($object);
352
353 Class::MOP::load_class( $base );
354 Class::MOP::load_class( $object );
27f4dfaa 355
356 my $meta = $self->_load_or_create(
357 $class,
358 superclasses => [$base],
359 ( @$roles ? (roles => $roles) : ()),
360 );
81393881 361
362 my $make_immutable = $meta->is_immutable || $self->make_classes_immutable;;
363 $meta->make_mutable if $meta->is_immutable;
364 $meta->add_method(_build_member_type => sub{ $object } );
365 #XXX as a default pass the domain model as a target_model until i come up with something
366 #better through the coercion method
367 my $def_act_args = sub {
368 my $super = shift;
369 return { (target_model => $_[0]->_source_resultset), %{ $super->(@_) } };
f670cfd0 370 };
81393881 371 $meta->add_around_method_modifier('_default_action_args_for', $def_act_args);
f670cfd0 372
7adfd53f 373
81393881 374 {
375 my $all_actions = $self->_all_collection_actions;
376 my $action_haystack = [keys %$all_actions];
377 if(!defined $action_rules){
378 $action_rules = $self->default_collection_actions;
379 } elsif( (!ref $action_rules && $action_rules) || (ref $action_rules eq 'Regexp') ){
380 $action_rules = [ $action_rules ];
381 } elsif( ref $action_rules eq 'ARRAY' && @$action_rules){
382 #don't add a qr/./ rule if we have at least one match rule
383 push(@$action_rules, qr/./)
384 unless grep {(ref $_ eq 'ARRAY' && $_->[0] ne '-exclude')
385 || !ref $_ || ref $_ eq 'Regexp'} @$action_rules;
f670cfd0 386 }
7adfd53f 387
81393881 388 # XXX this is kind of a dirty hack to support custom actions that are not
389 # previously defined and still be able to use the parse_reflect_rules mechanism
390 my @custom_actions = grep {!exists $all_actions->{$_}}
391 map{ $_->[0] } grep {ref $_ eq 'ARRAY' && $_->[0] ne '-exclude'} @$action_rules;
392 push(@$action_haystack, @custom_actions);
393 my $actions = $self->parse_reflect_rules($action_rules, $action_haystack);
394 for my $action (keys %$actions){
395 my $action_opts = $self->merge_hashes
396 ($all_actions->{$action} || {}, $actions->{$action} || {});
397
398 #NOTE: If the name of the action is not specified in the prototype then use it's
399 #hash key as the name. I think this is sane beahvior, but I've actually been thinking
400 #of making Action prototypes their own separate objects
401 $self->reflect_source_action(
c19feb34 402 schema => $schema,
81393881 403 name => $action,
404 object_class => $object,
405 source_class => $source,
406 %$action_opts,
407 );
408
409 # XXX i will move this to use the coercion method soon. this will be
410 # GoodEnough until then. I still need to think a little about the type coercion
411 # thing so i don't make a mess of it
412 my $act_args = sub { #override target model for this action
413 my $super = shift;
414 return { %{ $super->(@_) },
415 ($_[1] eq $action ? (target_model => $_[0]->_source_resultset) : () ) };
7adfd53f 416 };
81393881 417 $meta->add_around_method_modifier('_default_action_args_for', $act_args);
7adfd53f 418 }
81393881 419 }
420 $meta->make_immutable if $make_immutable;
421 return $meta;
422};
423sub reflect_source_object {
424 my($self, %opts) = @_;
425 %opts = %{ $self->merge_hashes(\%opts, $self->_compute_source_options(%opts)) };
7adfd53f 426
27f4dfaa 427 my $base = delete $opts{base} || Object;
428 my $roles = delete $opts{roles} || [];
429 my $class = delete $opts{class};
430 my $dm_name = delete $opts{domain_model_name};
431 my $dm_opts = delete $opts{domain_model_args} || {};
7adfd53f 432
81393881 433 my $source_name = delete $opts{source_name};
434 my $schema = delete $opts{schema_class};
435 my $source_class = delete $opts{source_class};
436 my $parent = delete $opts{parent_class};
437 my $parent_dm = delete $opts{parent_domain_model_name};
f670cfd0 438
81393881 439 my $action_rules = delete $opts{actions};
440 my $attr_rules = delete $opts{attributes};
7adfd53f 441
81393881 442 $class ||= $self->class_name_from_source_name($parent, $source_name);
f670cfd0 443
81393881 444 Class::MOP::load_class($parent);
445 Class::MOP::load_class($schema) if $schema;
446 Class::MOP::load_class($source_class);
7adfd53f 447
27f4dfaa 448 my $meta = $self->_load_or_create(
449 $class,
450 superclasses => [$base],
451 ( @$roles ? (roles => $roles) : ()),
452 );
7adfd53f 453
81393881 454 #create the domain model
455 $dm_name ||= $self->dm_name_from_source_name($source_name);
f670cfd0 456
81393881 457 $dm_opts->{isa} = $source_class;
458 $dm_opts->{is} ||= 'rw';
459 $dm_opts->{required} ||= 1;
df9a9c12 460
81393881 461 my $make_immutable = $meta->is_immutable || $self->make_classes_immutable;;
462 $meta->make_mutable if $meta->is_immutable;
7adfd53f 463
81393881 464 my $dm_attr = $meta->add_domain_model($dm_name, %$dm_opts);
465 my $dm_reader = $dm_attr->get_read_method;
df9a9c12 466
81393881 467 unless( $class->can('inflate_result') ){
468 my $inflate_method = sub {
469 my $class = shift; my ($src) = @_;
470 $src = $src->resolve if $src->isa('DBIx::Class::ResultSourceHandle');
471 $class->new($dm_name, $src->result_class->inflate_result(@_));
472 };
473 $meta->add_method('inflate_result', $inflate_method);
474 }
475
476 #XXX this is here to allow action prototypes to work with ListView
477 # maybe Collections hsould have this kind of thing too to allow you to reconstruct them?
478 #i like the possibility to be honest... as aset of key/value pairs they could be URId
479 #XXX move to using 'handles' for this?
480 $meta->add_method('__id', sub {shift->$dm_reader->id} )
481 unless $class->can('__id');
482 #XXX this one is for Action, ChooseOne and ChooseMany need this shit
483 $meta->add_method('__ident_condition', sub {shift->$dm_reader->ident_condition} )
484 unless $class->can('__ident_condition');
485
486 #XXX this is just a disaster
487 $meta->add_method('display_name', sub {shift->$dm_reader->display_name} )
488 if( $source_class->can('display_name') && !$class->can('display_name'));
489
490 #XXX as a default pass the domain model as a target_model until i come up with something
491 #better through the coercion method
492 my $def_act_args = sub {
493 my $super = shift;
494 confess "no dm reader: $dm_reader on $_[0]" unless $_[0]->can($dm_reader);
495 return { (target_model => $_[0]->$dm_reader), %{ $super->(@_) } };
7adfd53f 496 };
81393881 497 $meta->add_around_method_modifier('_default_action_args_for', $def_act_args);
7adfd53f 498
81393881 499 {
f670cfd0 500 # attributes => undef, #default to qr/./
501 # attributes => [], #default to nothing
502 # attributes => qr//, #DWIM, treated as [qr//]
503 # attributes => [{...}] #DWIM, treat as [qr/./, {...} ]
504 # attributes => [[-exclude => ...]] #DWIM, treat as [qr/./, [-exclude => ...]]
81393881 505 my $attr_haystack =
4d0bacd2 506 [ map { $_->name } $source_class->meta->get_all_attributes ];
81393881 507
f670cfd0 508 if(!defined $attr_rules){
509 $attr_rules = [qr/./];
510 } elsif( (!ref $attr_rules && $attr_rules) || (ref $attr_rules eq 'Regexp') ){
511 $attr_rules = [ $attr_rules ];
512 } elsif( ref $attr_rules eq 'ARRAY' && @$attr_rules){
513 #don't add a qr/./ rule if we have at least one match rule
514 push(@$attr_rules, qr/./) unless
515 grep {(ref $_ eq 'ARRAY' && $_->[0] ne '-exclude')
516 || !ref $_ || ref $_ eq 'Regexp'} @$attr_rules;
517 }
7adfd53f 518
81393881 519 my $attributes = $self->parse_reflect_rules($attr_rules, $attr_haystack);
520 for my $attr_name (keys %$attributes){
521 $self->reflect_source_object_attribute(
c19feb34 522 schema => $schema,
81393881 523 class => $class,
524 source_class => $source_class,
525 parent_class => $parent,
526 attribute_name => $attr_name,
527 domain_model_name => $dm_name,
528 %{ $attributes->{$attr_name} || {}},
529 );
530 }
531 }
532
533 {
534 my $all_actions = $self->_all_object_actions;
535 my $action_haystack = [keys %$all_actions];
536 if(!defined $action_rules){
537 $action_rules = $self->default_object_actions;
538 } elsif( (!ref $action_rules && $action_rules) || (ref $action_rules eq 'Regexp') ){
539 $action_rules = [ $action_rules ];
540 } elsif( ref $action_rules eq 'ARRAY' && @$action_rules){
541 #don't add a qr/./ rule if we have at least one match rule
542 push(@$action_rules, qr/./)
543 unless grep {(ref $_ eq 'ARRAY' && $_->[0] ne '-exclude')
544 || !ref $_ || ref $_ eq 'Regexp'} @$action_rules;
545 }
7adfd53f 546
81393881 547 # XXX this is kind of a dirty hack to support custom actions that are not
548 # previously defined and still be able to use the parse_reflect_rules mechanism
549 my @custom_actions = grep {!exists $all_actions->{$_}} map{ $_->[0] }
550 grep {ref $_ eq 'ARRAY' && $_->[0] ne '-exclude'} @$action_rules;
551 push(@$action_haystack, @custom_actions);
552 my $actions = $self->parse_reflect_rules($action_rules, $action_haystack);
553 for my $action (keys %$actions){
554 my $action_opts = $self->merge_hashes
555 ($all_actions->{$action} || {}, $actions->{$action} || {});
556
557 #NOTE: If the name of the action is not specified in the prototype then use it's
558 #hash key as the name. I think this is sane beahvior, but I've actually been thinking
559 #of making Action prototypes their own separate objects
560 $self->reflect_source_action(
c19feb34 561 schema => $schema,
81393881 562 name => $action,
563 object_class => $class,
564 source_class => $source_class,
565 %$action_opts,
566 );
567
568 # XXX i will move this to use the coercion method soon. this will be
569 # GoodEnough until then. I still need to think a little about the type coercion
570 # thing so i don't make a mess of it
571 my $act_args = sub { #override target model for this action
572 my $super = shift;
573 confess "no dm reader: $dm_reader on $_[0]" unless $_[0]->can($dm_reader);
574 return { %{ $super->(@_) },
575 ($_[1] eq $action ? (target_model => $_[0]->$dm_reader) : () ) };
576 };
577 $meta->add_around_method_modifier('_default_action_args_for', $act_args);
578 }
579 }
7adfd53f 580
81393881 581 $meta->make_immutable if $make_immutable;
582 return $meta;
583};
7adfd53f 584
81393881 585# needs class, attribute_name domain_model_name
586sub reflect_source_object_attribute {
587 my ($self, %opts) = @_;
588 unless( $opts{attribute_name} && $opts{class} && $opts{parent_class}
589 && ( $opts{source_class} || $opts{domain_model_name} ) ){
590 confess( "Error: class, parent_class, attribute_name, and either " .
591 "domain_model_name or source_class are required parameters" );
592 }
7adfd53f 593
81393881 594 my $meta = $opts{class}->meta;
595 my $attr_opts = $self->parameters_for_source_object_attribute(%opts);
7adfd53f 596
81393881 597 my $make_immutable = $meta->is_immutable;
598 $meta->make_mutable if $meta->is_immutable;
599
600 my $attr = $meta->add_attribute($opts{attribute_name}, %$attr_opts);
7adfd53f 601
81393881 602 $meta->make_immutable if $make_immutable;
603 return $attr;
604};
df9a9c12 605
81393881 606# needs class, attribute_name domain_model_name
607sub parameters_for_source_object_attribute {
608 my ($self, %opts) = @_;
609
610 my $class = delete $opts{class};
611 my $attr_name = delete $opts{attribute_name};
612 my $dm_name = delete $opts{domain_model_name};
613 my $source_class = delete $opts{source_class};
614 my $parent_class = delete $opts{parent_class};
c19feb34 615 my $schema = $opts{schema};
81393881 616 confess("parent_class is a required argument") unless $parent_class;
617 confess("You must supply at least one of domain_model_name and source_class")
618 unless $dm_name || $source_class;
619
c19feb34 620 my $source = $schema->source($source_class);
81393881 621 my $from_attr = $source_class->meta->find_attribute_by_name($attr_name);
622 my $reader = $from_attr->get_read_method;
09d2d18d 623 die("Could not find reader for attribute '$attr_name' on $source_class")
624 unless $reader;
81393881 625
626 #default options. lazy build but no outsider method
627 my %attr_opts = ( is => 'ro', lazy => 1, required => 1,
628 clearer => "_clear_${attr_name}",
629 predicate => {
630 "has_${attr_name}" =>
631 sub { defined(shift->$dm_name->$reader) }
632 },
633 domain_model => $dm_name,
634 orig_attr_name => $attr_name,
635 );
7b6797a0 636 $attr_opts{coerce} = 1 if $from_attr->should_coerce;
81393881 637
638 #m2m / has_many
639 my $m2m_meta;
640 if(my $coderef = $source->result_class->can('_m2m_metadata')){
641 $m2m_meta = $source->result_class->$coderef;
642 }
643
644 my $constraint_is_ArrayRef =
645 $from_attr->type_constraint->name eq 'ArrayRef' ||
646 $from_attr->type_constraint->is_subtype_of('ArrayRef');
647
648 if( my $rel_info = $source->relationship_info($attr_name) ){
649 my $rel_accessor = $rel_info->{attrs}->{accessor};
c19feb34 650 my $rel_moniker = $schema->source($rel_info->{class})->source_name;
81393881 651
652 if($rel_accessor eq 'multi' && $constraint_is_ArrayRef) {
653 #has_many
654 my $sm = $self->class_name_from_source_name($parent_class, $rel_moniker);
655 #type constraint is a collection, and default builds it
656 my $isa = $attr_opts{isa} = $self->class_name_for_collection_of($sm);
657 $attr_opts{default} = eval "sub {
658 my \$rs = shift->${dm_name}->related_resultset('${attr_name}');
659 return ${isa}->new(_source_resultset => \$rs);
660 }";
661 } elsif( $rel_accessor eq 'single' || $rel_accessor eq 'filter' ) {
662 #belongs_to
663 #type constraint is the foreign IM object, default inflates it
664 my $isa = $attr_opts{isa} = $self->class_name_from_source_name($parent_class, $rel_moniker);
665 $attr_opts{default} = eval "sub {
666 if (defined(my \$o = shift->${dm_name}->${reader})) {
667 return ${isa}->inflate_result(\$o->result_source, { \$o->get_columns });
668 }
669 return undef;
670 }";
df9a9c12 671 }
81393881 672 } elsif( $constraint_is_ArrayRef && $attr_name =~ m/^(.*)_list$/ ) {
673 #m2m magic
674 my $mm_name = $1;
675 my $link_table = "links_to_${mm_name}_list";
676 my ($hm_source, $far_side);
677 eval { $hm_source = $source->related_source($link_table); }
678 || confess "Can't find ${link_table} has_many for ${mm_name}_list";
679 eval { $far_side = $hm_source->related_source($mm_name); }
680 || confess "Can't find ${mm_name} belongs_to on ".$hm_source->result_class
681 ." traversing many-many for ${mm_name}_list";
682
683 my $sm = $self->class_name_from_source_name($parent_class,$far_side->source_name);
684 my $isa = $attr_opts{isa} = $self->class_name_for_collection_of($sm);
685
686 #proper collections will remove the result_class uglyness.
687 $attr_opts{default} = eval "sub {
688 my \$rs = shift->${dm_name}->related_resultset('${link_table}')->related_resultset('${mm_name}');
689 return ${isa}->new(_source_resultset => \$rs);
690 }";
691 } elsif( $constraint_is_ArrayRef && defined $m2m_meta && exists $m2m_meta->{$attr_name} ){
692 #m2m if using introspectable m2m component
693 my $rel = $m2m_meta->{$attr_name}->{relation};
694 my $far_rel = $m2m_meta->{$attr_name}->{foreign_relation};
695 my $far_source = $source->related_source($rel)->related_source($far_rel);
696 my $sm = $self->class_name_from_source_name($parent_class, $far_source->source_name);
697 my $isa = $attr_opts{isa} = $self->class_name_for_collection_of($sm);
698
699 my $rs_meth = $m2m_meta->{$attr_name}->{rs_method};
700 $attr_opts{default} = eval "sub {
701 return ${isa}->new(_source_resultset => shift->${dm_name}->${rs_meth});
702 }";
703 } else {
704 #no rel
705 $attr_opts{isa} = $from_attr->_isa_metadata;
09d2d18d 706 my $default_code = "sub{ shift->${dm_name}->${reader} }";
707 $attr_opts{default} = eval $default_code;
708 die "Could not generate default for attribute, code '$default_code' did not compile with: $@" if $@;
81393881 709 }
710 return \%attr_opts;
711};
712sub reflect_source_action {
713 my($self, %opts) = @_;
27f4dfaa 714 my $name = delete $opts{name};
715 my $base = delete $opts{base} || Action;
716 my $roles = delete $opts{roles} || [];
717 my $class = delete $opts{class};
81393881 718 my $object = delete $opts{object_class};
719 my $source = delete $opts{source_class};
c19feb34 720 my $schema = delete $opts{schema};
81393881 721
722 confess("name, object_class and source_class are required arguments")
723 unless $source && $name && $object;
724
725 my $attr_rules = delete $opts{attributes};
726 $class ||= $object->_default_action_class_for($name);
727
728 Class::MOP::load_class( $base );
729 Class::MOP::load_class( $object );
730 Class::MOP::load_class( $source );
731
732 #print STDERR "\n\t", ref $attr_rules eq 'ARRAY' ? @$attr_rules : $attr_rules,"\n";
733 # attributes => undef, #default to qr/./
734 # attributes => [], #default to nothing
735 # attributes => qr//, #DWIM, treated as [qr//]
736 # attributes => [{...}] #DWIM, treat as [qr/./, {...} ]
737 # attributes => [[-exclude => ...]] #DWIM, treat as [qr/./, [-exclude => ...]]
738 my $attr_haystack = [ map { $_->name } $object->parameter_attributes ];
739 if(!defined $attr_rules){
740 $attr_rules = [qr/./];
741 } elsif( (!ref $attr_rules && $attr_rules) || (ref $attr_rules eq 'Regexp') ){
742 $attr_rules = [ $attr_rules ];
743 } elsif( ref $attr_rules eq 'ARRAY' && @$attr_rules){
744 #don't add a qr/./ rule if we have at least one match rule
745 push(@$attr_rules, qr/./) unless
746 grep {(ref $_ eq 'ARRAY' && $_->[0] ne '-exclude')
747 || !ref $_ || ref $_ eq 'Regexp'} @$attr_rules;
748 }
749
750 #print STDERR "${name}\t${class}\t${base}\n";
751 #print STDERR "\t${object}\t${source}\n";
752 #print STDERR "\t",@$attr_rules,"\n";
753
754 my $o_meta = $object->meta;
755 my $s_meta = $source->meta;
c19feb34 756 my $attributes = $self->parse_reflect_rules($attr_rules, $attr_haystack);
81393881 757
758 #create the class
27f4dfaa 759 my $meta = $self->_load_or_create(
760 $class,
761 superclasses => [$base],
762 ( @$roles ? (roles => $roles) : ()),
763 );
81393881 764 my $make_immutable = $meta->is_immutable || $self->make_classes_immutable;
765 $meta->make_mutable if $meta->is_immutable;
766
767 for my $attr_name (keys %$attributes){
768 my $attr_opts = $attributes->{$attr_name} || {};
769 my $o_attr = $o_meta->find_attribute_by_name($attr_name);
770 my $s_attr_name = $o_attr->orig_attr_name || $attr_name;
771 my $s_attr = $s_meta->find_attribute_by_name($s_attr_name);
772 confess("Unable to find attribute for '${s_attr_name}' via '${source}'")
773 unless defined $s_attr;
774 next unless $s_attr->get_write_method
775 && $s_attr->get_write_method !~ /^_/; #only rw attributes!
776
777 my $attr_params = $self->parameters_for_source_object_action_attribute
778 (
c19feb34 779 schema => $schema,
81393881 780 object_class => $object,
781 source_class => $source,
782 attribute_name => $attr_name
783 );
784 $meta->add_attribute( $attr_name => %$attr_params);
785 }
786
787 $meta->make_immutable if $make_immutable;
788 return $meta;
789};
790sub parameters_for_source_object_action_attribute {
791 my ($self, %opts) = @_;
792
793 my $object = delete $opts{object_class};
794 my $attr_name = delete $opts{attribute_name};
795 my $source_class = delete $opts{source_class};
c19feb34 796 my $schema = delete $opts{schema};
797 my $source = $schema->source($source_class);
81393881 798 confess("object_class and attribute_name are required parameters")
799 unless $attr_name && $object;
800
801 my $o_meta = $object->meta;
802 my $dm_name = $o_meta->find_attribute_by_name($attr_name)->domain_model;
803 $source_class ||= $o_meta->find_attribute_by_name($dm_name)->_isa_metadata;
804 my $from_attr = $source_class->meta->find_attribute_by_name($attr_name);
805
806 #print STDERR "$attr_name is type: " . $from_attr->meta->name . "\n";
807
808 confess("${attr_name} is not writeable and can not be reflected")
809 unless $from_attr->get_write_method;
810
811 my %attr_opts = (
812 is => 'rw',
813 isa => $from_attr->_isa_metadata,
814 required => $from_attr->is_required,
815 ($from_attr->is_required
816 ? () : (clearer => "clear_${attr_name}")),
817 predicate => "has_${attr_name}",
818 );
819
820 if ($attr_opts{required}) {
821 if($from_attr->has_default) {
822 $attr_opts{lazy} = 1;
823 $attr_opts{default} = $from_attr->default;
824 } else {
825 $attr_opts{lazy_fail} = 1;
7adfd53f 826 }
81393881 827 }
828
829
830 my $m2m_meta;
831 if(my $coderef = $source_class->result_class->can('_m2m_metadata')){
832 $m2m_meta = $source_class->result_class->$coderef;
833 }
834 #test for relationships
835 my $constraint_is_ArrayRef =
836 $from_attr->type_constraint->name eq 'ArrayRef' ||
837 $from_attr->type_constraint->is_subtype_of('ArrayRef');
838
81393881 839 if (my $rel_info = $source->relationship_info($attr_name)) {
840 my $rel_accessor = $rel_info->{attrs}->{accessor};
841
842 if($rel_accessor eq 'multi' && $constraint_is_ArrayRef) {
843 confess "${attr_name} is a rw has_many, this won't work.";
844 } elsif( $rel_accessor eq 'single' || $rel_accessor eq 'filter') {
df9a9c12 845 $attr_opts{valid_values} = sub {
81393881 846 shift->target_model->result_source->related_source($attr_name)->resultset;
df9a9c12 847 };
7adfd53f 848 }
81393881 849 } elsif ( $constraint_is_ArrayRef && $attr_name =~ m/^(.*)_list$/) {
850 my $mm_name = $1;
851 my $link_table = "links_to_${mm_name}_list";
852 $attr_opts{default} = sub { [] };
853 $attr_opts{valid_values} = sub {
854 shift->target_model->result_source->related_source($link_table)
855 ->related_source($mm_name)->resultset;
856 };
857 } elsif( $constraint_is_ArrayRef && defined $m2m_meta && exists $m2m_meta->{$attr_name} ){
858 #m2m if using introspectable m2m component
859 my $rel = $m2m_meta->{$attr_name}->{relation};
860 my $far_rel = $m2m_meta->{$attr_name}->{foreign_relation};
861 $attr_opts{default} = sub { [] };
862 $attr_opts{valid_values} = sub {
863 shift->target_model->result_source->related_source($rel)
864 ->related_source($far_rel)->resultset;
865 };
866 }
867 #use Data::Dumper;
868 #print STDERR "\n" .$attr_name ." - ". $object . "\n";
869 #print STDERR Dumper(\%attr_opts);
870 return \%attr_opts;
871};
27f4dfaa 872
81393881 873sub _load_or_create {
27f4dfaa 874 my ($self, $class, %options) = @_;
875
876 if( $self->_maybe_load_class($class) ){
877 return $class->meta;
878 }
879 my $base;
880 if( exists $options{superclasses} ){
881 ($base) = @{ $options{superclasses} };
882 } else {
883 $base = 'Reaction::InterfaceModel::Action';
884 }
885 return $base->meta->create($class, %options);
886}
887
81393881 888sub _maybe_load_class {
889 my ($self, $class) = @_;
890 my $file = $class . '.pm';
891 $file =~ s{::}{/}g;
892 my $ret = eval { Class::MOP::load_class($class) };
893 if ($INC{$file} && $@) {
894 confess "Error loading ${class}: $@";
895 }
896 return $ret;
27f4dfaa 897}
c11c77ee 898
81393881 899__PACKAGE__->meta->make_immutable;
c11c77ee 900
7adfd53f 901
9021;
0402136b 903
904#--------#---------#---------#---------#---------#---------#---------#---------#
905__END__;
906
907=head1 NAME
908
909Reaction::InterfaceModel::Reflector::DBIC -
910Automatically Generate InterfaceModels from DBIx::Class models
911
912=head1 DESCRIPTION
913
914The InterfaceModel reflectors are classes that are meant to aid you in easily
915generating Reaction::InterfaceModel classes that represent their underlying
916DBIx::Class domain models by introspecting your L<DBIx::Class::ResultSource>s
917and creating a collection of L<Reaction::InterfaceModel::Object> and
918L<Reaction::InterfaceModel::Collection> classes for you to use.
919
920The default base class of all Object classes will be
921 L<Reaction::InterfaceModel::Object> and the default Collection type will be
922L<Reaction::InterfaceModel::Collection::Virtual::ResultSet>.
923
924Additionally, the reflector can create InterfaceModel actions that interact
925with the supplied L<Reaction::UI::Controller::Collection::CRUD>, allowing you
926to easily set up a highly customizable CRUD interface in minimal time.
927
928At this time, supported collection actions consist of:
929
930=over 4
931
932=item B<> L<Reaction::INterfaceModel::Action::DBIC::ResultSet::Create>
933
934Creates a new item in the collection and underlying ResultSet.
935
936=item B<> L<Reaction::INterfaceModel::Action::DBIC::ResultSet::DeleteAll>
937
938Deletes all the items in a collection and it's underlying resultset using
939C<delete_all>
940
941=back
942
943And supported object actions are :
944
945=over 4
946
947=item B<Update> - via L<Reaction::INterfaceModel::Action::DBIC::Result::Update>
948
949Updates an existing object.
950
951=item B<Delete> - via L<Reaction::INterfaceModel::Action::DBIC::Result::Delete>
952
953Deletes an existing object.
954
955=back
956
957=head1 SYNOPSIS
958
959 package MyApp::IM::TestModel;
960 use base 'Reaction::InterfaceModel::Object';
961 use Reaction::Class;
962 use Reaction::InterfaceModel::Reflector::DBIC;
963 my $reflector = Reaction::InterfaceModel::Reflector::DBIC->new;
964
965 #Reflect everything
966 $reflector->reflect_schema
967 (
968 model_class => __PACKAGE__,
969 schema_class => 'MyApp::Schema',
970 );
971
972=head2 Selectively including and excluding sources
973
974 #reflect everything except for the FooBar and FooBaz classes
975 $reflector->reflect_schema
976 (
977 model_class => __PACKAGE__,
978 schema_class => 'MyApp::Schema',
979 sources => [-exclude => [qw/FooBar FooBaz/] ],
980 # you could also do:
981 sources => [-exclude => qr/(?:FooBar|FooBaz)/,
982 # or even
983 sources => [-exclude => [qr/FooBar/, qr/FooBaz/],
984 );
985
986 #reflect only the Foo family of sources
987 $reflector->reflect_schema
988 (
989 model_class => __PACKAGE__,
990 schema_class => 'MyApp::Schema',
991 sources => qr/^Foo/,
992 );
993
994=head2 Selectively including and excluding fields in sources
995
996 #Reflect Foo and Baz in their entirety and exclude the field 'avatar' in the Bar ResultSource
997 $reflector->reflect_schema
998 (
999 model_class => __PACKAGE__,
1000 schema_class => 'MyApp::Schema',
1001 sources => [qw/Foo Baz/,
1002 [ Bar => {attributes => [[-exclude => 'avatar']] } ],
1003 # or exclude by regex
1004 [ Bar => {attributes => [-exclude => qr/avatar/] } ],
1005 # or simply do not include it...
1006 [ Bar => {attributes => [qw/id name description/] } ],
1007 ],
1008 );
1009
1010=head1 ATTRIBUTES
1011
1012=head2 make_classes_immutable
1013
1014=head2 object_actions
1015
1016=head2 collection_actions
1017
1018=head2 default_object_actions
1019
1020=head2 default_collection_actions
1021
1022=head2 builtin_object_actions
1023
1024=head2 builtin_collection_actions
1025
1026=head1 METHODS
1027
1028=head2 new
1029
1030=head2 _all_object_actions
1031
1032=head2 _all_collection_actions
1033
1034=head2 dm_name_from_class_name
1035
1036=head2 dm_name_from_source_name
1037
1038=head2 class_name_from_source_name
1039
1040=head2 class_name_for_collection_of
1041
1042=head2 merge_hashes
1043
1044=head2 parse_reflect_rules
1045
1046=head2 merge_reflect_rules
1047
1048=head2 reflect_schema
1049
1050=head2 _compute_source_options
1051
1052=head2 add_source
1053
1054=head2 reflect_source
1055
1056=head2 reflect_source_collection
1057
1058=head2 reflect_source_object
1059
1060=head2 reflect_source_object_attribute
1061
1062=head2 parameters_for_source_object_attribute
1063
1064=head2 reflect_source_action
1065
1066=head2 parameters_for_source_object_action_attribute
1067
1068=head1 TODO
1069
1070Allow the reflector to dump the generated code out as files, eliminating the need to
1071reflect on startup every time. This will likely take quite a bit of work though. The
1072main work is already in place, but the grunt work is still left. At the moment there
1073is no closures that can't be dumped out as code with a little bit of work.
1074
1075=head1 AUTHORS
1076
1077See L<Reaction::Class> for authors.
1078
1079=head1 LICENSE
1080
1081See L<Reaction::Class> for the license.
1082
1083=cut