1 package Reaction::InterfaceModel::Reflector::DBIC;
3 use aliased 'Reaction::InterfaceModel::Action::DBIC::ResultSet::Create';
4 use aliased 'Reaction::InterfaceModel::Action::DBIC::ResultSet::DeleteAll';
5 use aliased 'Reaction::InterfaceModel::Action::DBIC::Result::Update';
6 use aliased 'Reaction::InterfaceModel::Action::DBIC::Result::Delete';
8 use aliased 'Reaction::InterfaceModel::Collection::Virtual::ResultSet';
9 use aliased 'Reaction::InterfaceModel::Object';
10 use aliased 'Reaction::InterfaceModel::Action';
16 use namespace::clean -except => [ qw(meta) ];
19 has make_classes_immutable => (isa => "Bool", is => "rw", required => 1, default => sub{ 1 });
21 #user defined actions and prototypes
22 has object_actions => (isa => "HashRef", is => "rw", lazy_build => 1);
23 has collection_actions => (isa => "HashRef", is => "rw", lazy_build => 1);
25 #which actions to create by default
26 has default_object_actions => (isa => "ArrayRef", is => "rw", lazy_build => 1);
27 has default_collection_actions => (isa => "ArrayRef", is => "rw", lazy_build => 1);
29 #builtin actions and prototypes
30 has builtin_object_actions => (isa => "HashRef", is => "rw", lazy_build => 1);
31 has builtin_collection_actions => (isa => "HashRef", is => "rw", lazy_build => 1);
32 sub _build_object_actions { {} };
33 sub _build_collection_actions { {} };
34 sub _build_default_object_actions { [ qw/Update Delete/ ] };
35 sub _build_default_collection_actions { [ qw/Create DeleteAll/ ] };
36 sub _build_builtin_object_actions {
38 Update => { name => 'Update', base => Update },
39 Delete => { name => 'Delete', base => Delete, attributes => [] },
42 sub _build_builtin_collection_actions {
44 Create => {name => 'Create', base => Create },
45 DeleteAll => {name => 'DeleteAll', base => DeleteAll, attributes => [] }
48 sub _all_object_actions {
50 return $self->merge_hashes
51 ($self->builtin_object_actions, $self->object_actions);
53 sub _all_collection_actions {
55 return $self->merge_hashes
56 ($self->builtin_collection_actions, $self->collection_actions);
58 sub dm_name_from_class_name {
59 my($self, $class) = @_;
60 confess("wrong arguments") unless $class;
62 $class = "_" . $self->_class_to_attribute_name($class) . "_store";
65 sub dm_name_from_source_name {
66 my($self, $source) = @_;
67 confess("wrong arguments") unless $source;
68 $source =~ s/([a-z0-9])([A-Z])/${1}_${2}/g ;
69 $source = "_" . $self->_class_to_attribute_name($source) . "_store";
72 sub class_name_from_source_name {
73 my ($self, $model_class, $source_name) = @_;
74 confess("wrong arguments") unless $model_class && $source_name;
75 return join "::", $model_class, $source_name;
77 sub class_name_for_collection_of {
78 my ($self, $object_class) = @_;
79 confess("wrong arguments") unless $object_class;
80 return "${object_class}::Collection";
83 my($self, $left, $right) = @_;
84 return Catalyst::Utils::merge_hashes($left, $right);
86 sub parse_reflect_rules {
87 my ($self, $rules, $haystack) = @_;
88 confess('$rules must be an array reference') unless ref $rules eq 'ARRAY';
89 confess('$haystack must be an array reference') unless ref $haystack eq 'ARRAY';
92 my (@exclude, @include, $global_opts);
93 if(@$rules == 2 && $rules->[0] eq '-exclude'){
94 push(@exclude, (ref $rules->[1] eq 'ARRAY' ? @{$rules->[1]} : $rules->[1]));
96 for my $rule ( @$rules ){
97 if (ref $rule eq 'ARRAY' && $rule->[0] eq '-exclude'){
98 push(@exclude, (ref $rule->[1] eq 'ARRAY' ? @{$rule->[1]} : $rule->[1]));
99 } elsif( ref $rule eq 'HASH' ){
100 $global_opts = ref $global_opts eq 'HASH' ?
101 $self->merge_hashes($global_opts, $rule) : $rule;
103 push(@include, $rule);
107 my $check_exclude = sub{
108 for my $rule (@exclude){
109 return 1 if(ref $rule eq 'Regexp' ? $_[0] =~ /$rule/ : $_[0] eq $rule);
114 @$haystack = grep { !$check_exclude->($_) } @$haystack;
115 $self->merge_reflect_rules(\@include, $needles, $haystack, $global_opts);
118 sub merge_reflect_rules {
119 my ($self, $rules, $needles, $haystack, $local_opts) = @_;
120 for my $rule ( @$rules ){
121 if(!ref $rule && ( grep {$rule eq $_} @$haystack ) ){
122 $needles->{$rule} = defined $needles->{$rule} ?
123 $self->merge_hashes($needles->{$rule}, $local_opts) : $local_opts;
124 } elsif( ref $rule eq 'Regexp' ){
125 for my $match ( grep { /$rule/ } @$haystack ){
126 $needles->{$match} = defined $needles->{$match} ?
127 $self->merge_hashes($needles->{$match}, $local_opts) : $local_opts;
129 } elsif( ref $rule eq 'ARRAY' ){
131 $opts = pop(@$rule) if @$rule > 1 and ref $rule->[$#$rule] eq 'HASH';
132 $opts = $self->merge_hashes($local_opts, $opts) if defined $local_opts;
133 $self->merge_reflect_rules($rule, $needles, $haystack, $opts);
138 my ($self, %opts) = @_;
139 my $base = delete $opts{base} || Object;
140 my $roles = delete $opts{roles} || [];
141 my $model = delete $opts{model_class};
142 my $schema = delete $opts{schema_class};
143 my $dm_name = delete $opts{domain_model_name};
144 my $dm_args = delete $opts{domain_model_args} || {};
145 $dm_name ||= $self->dm_name_from_class_name($schema);
147 #load all necessary classes
148 confess("model_class and schema_class are required parameters")
149 unless($model && $schema);
150 Class::MOP::load_class( $base );
151 Class::MOP::load_class( $schema );
152 my $meta = $self->_load_or_create(
154 superclasses => [$base],
155 ( @$roles ? (roles => $roles) : ()),
158 # sources => undef, #default to qr/./
159 # sources => [], #default to nothing
160 # sources => qr//, #DWIM, treated as [qr//]
161 # sources => [{...}] #DWIM, treat as [qr/./, {...} ]
162 # sources => [[-exclude => ...]] #DWIM, treat as [qr/./, [-exclude => ...]]
163 my $haystack = [ $schema->sources ];
165 my $rules = delete $opts{sources};
168 } elsif( ref $rules eq 'Regexp'){
170 } elsif( ref $rules eq 'ARRAY' && @$rules){
171 #don't add a qr/./ rule if we have at least one match rule
172 push(@$rules, qr/./) unless grep {(ref $_ eq 'ARRAY' && $_->[0] ne '-exclude')
173 || !ref $_ || ref $_ eq 'Regexp'} @$rules;
176 my $sources = $self->parse_reflect_rules($rules, $haystack);
178 my $make_immutable = $meta->is_immutable || $self->make_classes_immutable;
179 $meta->make_mutable if $meta->is_immutable;
181 $meta->add_domain_model
182 ($dm_name, is => 'rw', isa => $schema, required => 1, %$dm_args);
184 for my $source_name (keys %$sources){
185 my $source_opts = $sources->{$source_name} || {};
186 $self->reflect_source(
187 source_name => $source_name,
188 parent_class => $model,
189 schema_class => $schema,
190 source_class => $schema->class($source_name),
191 parent_domain_model_name => $dm_name,
196 $meta->make_immutable if $make_immutable;
199 sub _compute_source_options {
200 my ($self, %opts) = @_;
201 my $schema = delete $opts{schema_class};
202 my $source_name = delete $opts{source_name};
203 my $source_class = delete $opts{source_class};
204 my $parent = delete $opts{parent_class};
205 my $parent_dm = delete $opts{parent_domain_model_name};
207 #this is the part where I hate my life for promissing all sorts of DWIMery
208 confess("parent_class and source_name or source_class are required parameters")
209 unless($parent && ($source_name || $source_class));
211 OUTER: until( $schema && $source_name && $source_class && $parent_dm ){
212 if( $schema && !$source_name){
213 next OUTER if $source_name = $source_class->result_source_instance->source_name;
214 } elsif( $schema && !$source_class){
215 next OUTER if $source_class = eval { $schema->class($source_name) };
218 if($source_class && (!$schema || !$source_name)){
220 $schema = $source_class->result_source_instance->schema;
221 next OUTER if $schema && Class::MOP::load_class($schema);
224 $source_name = $source_class->result_source_instance->source_name;
225 next OUTER if $source_name;
228 my @haystack = $parent_dm ?
229 $parent->meta->find_attribute_by_name($parent_dm) : $parent->domain_models;
231 #there's a lot of guessing going on, but it should work fine on most cases
232 INNER: for my $needle (@haystack){
233 my $isa = $needle->_isa_metadata;
234 next INNER unless Class::MOP::load_class( $isa->_isa_metadata );
235 next INNER unless $isa->isa('DBIx::Class::Schema');
236 if(!$parent_dm && $schema && $isa eq $schema){
237 $parent_dm = $needle->name;
242 my $src_class = eval{ $isa->class($source_name) };
243 next INNER unless $src_class;
244 next INNER if($source_class && $source_class ne $src_class);
246 $parent_dm = $needle->name;
247 $source_class = $src_class;
252 #do we even need to go this far?
253 if( !$parent_dm && $schema ){
254 my $tentative = $self->dm_name_from_class_name($schema);
255 $parent_dm = $tentative if grep{$_->name eq $tentative} @haystack;
258 confess("Could not determine options automatically from: schema " .
259 "'${schema}', source_name '${source_name}', source_class " .
260 "'${source_class}', parent_domain_model_name '${parent_dm}'");
264 source_name => $source_name,
265 schema_class => $schema,
266 source_class => $source_class,
267 parent_class => $parent,
268 parent_domain_model_name => $parent_dm,
271 sub _class_to_attribute_name {
272 my ( $self, $str ) = @_;
273 confess("wrong arguments passed for _class_to_attribute_name") unless $str;
274 return join('_', map lc, split(/::|(?<=[a-z0-9])(?=[A-Z])/, $str))
277 my ($self, %opts) = @_;
279 my $model = delete $opts{model_class};
280 my $reader = delete $opts{reader};
281 my $source = delete $opts{source_name};
282 my $dm_name = delete $opts{domain_model_name};
283 my $collection = delete $opts{collection_class};
284 my $name = delete $opts{attribute_name} || $source;
286 confess("model_class and source_name are required parameters")
287 unless $model && $source;
288 my $meta = $model->meta;
290 unless( $collection ){
291 my $object = $self->class_name_from_source_name($model, $source);
292 $collection = $self->class_name_for_collection_of($object);
296 $reader =~ s/([a-z0-9])([A-Z])/${1}_${2}/g ;
297 $reader = $self->_class_to_attribute_name($reader) . "_collection";
300 my @haystack = $meta->domain_models;
302 @haystack = grep { $_->_isa_metadata->isa('DBIx::Class::Schema') } @haystack;
305 $dm_name = $haystack[0]->name;
306 } elsif(@haystack > 1){
307 confess("Failed to automatically determine domain_model_name. More than one " .
308 "possible match (".(join ", ", map{"'".$_->name."'"} @haystack).")");
310 confess("Failed to automatically determine domain_model_name. No matches.");
320 predicate => "has_" . $self->_class_to_attribute_name($name) ,
321 domain_model => $dm_name,
322 orig_attr_name => $source,
326 _source_resultset => $_[0]->$dm_name->resultset($source),
332 my $make_immutable = $meta->is_immutable;
333 $meta->make_mutable if $make_immutable;
334 my $attr = $meta->add_attribute($name, %attr_opts);
335 $meta->make_immutable if $make_immutable;
340 my ($self, %opts) = @_;
341 my $collection = delete $opts{collection} || {};
342 %opts = %{ $self->merge_hashes(\%opts, $self->_compute_source_options(%opts)) };
344 my $obj_meta = $self->reflect_source_object(%opts);
345 my $col_meta = $self->reflect_source_collection
347 object_class => $obj_meta->name,
348 source_class => $opts{source_class},
354 model_class => delete $opts{parent_class},
355 domain_model_name => delete $opts{parent_domain_model_name},
356 collection_class => $col_meta->name,
359 sub reflect_source_collection {
360 my ($self, %opts) = @_;
361 my $base = delete $opts{base} || ResultSet;
362 my $roles = delete $opts{roles} || [];
363 my $class = delete $opts{class};
364 my $object = delete $opts{object_class};
365 my $source = delete $opts{source_class};
366 my $action_rules = delete $opts{actions};
368 confess('object_class and source_class are required parameters')
369 unless $object && $source;
370 $class ||= $self->class_name_for_collection_of($object);
372 Class::MOP::load_class( $base );
373 Class::MOP::load_class( $object );
375 my $meta = $self->_load_or_create(
377 superclasses => [$base],
378 ( @$roles ? (roles => $roles) : ()),
381 my $make_immutable = $meta->is_immutable || $self->make_classes_immutable;;
382 $meta->make_mutable if $meta->is_immutable;
383 $meta->add_method(_build_member_type => sub{ $object } );
384 #XXX as a default pass the domain model as a target_model until i come up with something
385 #better through the coercion method
386 my $def_act_args = sub {
388 return { (target_model => $_[0]->_source_resultset), %{ $super->(@_) } };
390 $meta->add_around_method_modifier('_default_action_args_for', $def_act_args);
394 my $all_actions = $self->_all_collection_actions;
395 my $action_haystack = [keys %$all_actions];
396 if(!defined $action_rules){
397 $action_rules = $self->default_collection_actions;
398 } elsif( (!ref $action_rules && $action_rules) || (ref $action_rules eq 'Regexp') ){
399 $action_rules = [ $action_rules ];
400 } elsif( ref $action_rules eq 'ARRAY' && @$action_rules){
401 #don't add a qr/./ rule if we have at least one match rule
402 push(@$action_rules, qr/./)
403 unless grep {(ref $_ eq 'ARRAY' && $_->[0] ne '-exclude')
404 || !ref $_ || ref $_ eq 'Regexp'} @$action_rules;
407 # XXX this is kind of a dirty hack to support custom actions that are not
408 # previously defined and still be able to use the parse_reflect_rules mechanism
409 my @custom_actions = grep {!exists $all_actions->{$_}}
410 map{ $_->[0] } grep {ref $_ eq 'ARRAY' && $_->[0] ne '-exclude'} @$action_rules;
411 push(@$action_haystack, @custom_actions);
412 my $actions = $self->parse_reflect_rules($action_rules, $action_haystack);
413 for my $action (keys %$actions){
414 my $action_opts = $self->merge_hashes
415 ($all_actions->{$action} || {}, $actions->{$action} || {});
417 #NOTE: If the name of the action is not specified in the prototype then use it's
418 #hash key as the name. I think this is sane beahvior, but I've actually been thinking
419 #of making Action prototypes their own separate objects
420 $self->reflect_source_action(
422 object_class => $object,
423 source_class => $source,
427 # XXX i will move this to use the coercion method soon. this will be
428 # GoodEnough until then. I still need to think a little about the type coercion
429 # thing so i don't make a mess of it
430 my $act_args = sub { #override target model for this action
432 return { %{ $super->(@_) },
433 ($_[1] eq $action ? (target_model => $_[0]->_source_resultset) : () ) };
435 $meta->add_around_method_modifier('_default_action_args_for', $act_args);
438 $meta->make_immutable if $make_immutable;
441 sub reflect_source_object {
442 my($self, %opts) = @_;
443 %opts = %{ $self->merge_hashes(\%opts, $self->_compute_source_options(%opts)) };
445 my $base = delete $opts{base} || Object;
446 my $roles = delete $opts{roles} || [];
447 my $class = delete $opts{class};
448 my $dm_name = delete $opts{domain_model_name};
449 my $dm_opts = delete $opts{domain_model_args} || {};
451 my $source_name = delete $opts{source_name};
452 my $schema = delete $opts{schema_class};
453 my $source_class = delete $opts{source_class};
454 my $parent = delete $opts{parent_class};
455 my $parent_dm = delete $opts{parent_domain_model_name};
457 my $action_rules = delete $opts{actions};
458 my $attr_rules = delete $opts{attributes};
460 $class ||= $self->class_name_from_source_name($parent, $source_name);
462 Class::MOP::load_class($parent);
463 Class::MOP::load_class($schema) if $schema;
464 Class::MOP::load_class($source_class);
466 my $meta = $self->_load_or_create(
468 superclasses => [$base],
469 ( @$roles ? (roles => $roles) : ()),
472 #create the domain model
473 $dm_name ||= $self->dm_name_from_source_name($source_name);
475 $dm_opts->{isa} = $source_class;
476 $dm_opts->{is} ||= 'rw';
477 $dm_opts->{required} ||= 1;
479 my $make_immutable = $meta->is_immutable || $self->make_classes_immutable;;
480 $meta->make_mutable if $meta->is_immutable;
482 my $dm_attr = $meta->add_domain_model($dm_name, %$dm_opts);
483 my $dm_reader = $dm_attr->get_read_method;
485 unless( $class->can('inflate_result') ){
486 my $inflate_method = sub {
487 my $class = shift; my ($src) = @_;
488 $src = $src->resolve if $src->isa('DBIx::Class::ResultSourceHandle');
489 $class->new($dm_name, $src->result_class->inflate_result(@_));
491 $meta->add_method('inflate_result', $inflate_method);
494 #XXX this is here to allow action prototypes to work with ListView
495 # maybe Collections hsould have this kind of thing too to allow you to reconstruct them?
496 #i like the possibility to be honest... as aset of key/value pairs they could be URId
497 #XXX move to using 'handles' for this?
498 $meta->add_method('__id', sub {shift->$dm_reader->id} )
499 unless $class->can('__id');
500 #XXX this one is for Action, ChooseOne and ChooseMany need this shit
501 $meta->add_method('__ident_condition', sub {shift->$dm_reader->ident_condition} )
502 unless $class->can('__ident_condition');
504 #XXX this is just a disaster
505 $meta->add_method('display_name', sub {shift->$dm_reader->display_name} )
506 if( $source_class->can('display_name') && !$class->can('display_name'));
508 #XXX as a default pass the domain model as a target_model until i come up with something
509 #better through the coercion method
510 my $def_act_args = sub {
512 confess "no dm reader: $dm_reader on $_[0]" unless $_[0]->can($dm_reader);
513 return { (target_model => $_[0]->$dm_reader), %{ $super->(@_) } };
515 $meta->add_around_method_modifier('_default_action_args_for', $def_act_args);
518 # attributes => undef, #default to qr/./
519 # attributes => [], #default to nothing
520 # attributes => qr//, #DWIM, treated as [qr//]
521 # attributes => [{...}] #DWIM, treat as [qr/./, {...} ]
522 # attributes => [[-exclude => ...]] #DWIM, treat as [qr/./, [-exclude => ...]]
524 [ map { $_->name } $source_class->meta->compute_all_applicable_attributes ];
526 if(!defined $attr_rules){
527 $attr_rules = [qr/./];
528 } elsif( (!ref $attr_rules && $attr_rules) || (ref $attr_rules eq 'Regexp') ){
529 $attr_rules = [ $attr_rules ];
530 } elsif( ref $attr_rules eq 'ARRAY' && @$attr_rules){
531 #don't add a qr/./ rule if we have at least one match rule
532 push(@$attr_rules, qr/./) unless
533 grep {(ref $_ eq 'ARRAY' && $_->[0] ne '-exclude')
534 || !ref $_ || ref $_ eq 'Regexp'} @$attr_rules;
537 my $attributes = $self->parse_reflect_rules($attr_rules, $attr_haystack);
538 for my $attr_name (keys %$attributes){
539 $self->reflect_source_object_attribute(
541 source_class => $source_class,
542 parent_class => $parent,
543 attribute_name => $attr_name,
544 domain_model_name => $dm_name,
545 %{ $attributes->{$attr_name} || {}},
551 my $all_actions = $self->_all_object_actions;
552 my $action_haystack = [keys %$all_actions];
553 if(!defined $action_rules){
554 $action_rules = $self->default_object_actions;
555 } elsif( (!ref $action_rules && $action_rules) || (ref $action_rules eq 'Regexp') ){
556 $action_rules = [ $action_rules ];
557 } elsif( ref $action_rules eq 'ARRAY' && @$action_rules){
558 #don't add a qr/./ rule if we have at least one match rule
559 push(@$action_rules, qr/./)
560 unless grep {(ref $_ eq 'ARRAY' && $_->[0] ne '-exclude')
561 || !ref $_ || ref $_ eq 'Regexp'} @$action_rules;
564 # XXX this is kind of a dirty hack to support custom actions that are not
565 # previously defined and still be able to use the parse_reflect_rules mechanism
566 my @custom_actions = grep {!exists $all_actions->{$_}} map{ $_->[0] }
567 grep {ref $_ eq 'ARRAY' && $_->[0] ne '-exclude'} @$action_rules;
568 push(@$action_haystack, @custom_actions);
569 my $actions = $self->parse_reflect_rules($action_rules, $action_haystack);
570 for my $action (keys %$actions){
571 my $action_opts = $self->merge_hashes
572 ($all_actions->{$action} || {}, $actions->{$action} || {});
574 #NOTE: If the name of the action is not specified in the prototype then use it's
575 #hash key as the name. I think this is sane beahvior, but I've actually been thinking
576 #of making Action prototypes their own separate objects
577 $self->reflect_source_action(
579 object_class => $class,
580 source_class => $source_class,
584 # XXX i will move this to use the coercion method soon. this will be
585 # GoodEnough until then. I still need to think a little about the type coercion
586 # thing so i don't make a mess of it
587 my $act_args = sub { #override target model for this action
589 confess "no dm reader: $dm_reader on $_[0]" unless $_[0]->can($dm_reader);
590 return { %{ $super->(@_) },
591 ($_[1] eq $action ? (target_model => $_[0]->$dm_reader) : () ) };
593 $meta->add_around_method_modifier('_default_action_args_for', $act_args);
597 $meta->make_immutable if $make_immutable;
601 # needs class, attribute_name domain_model_name
602 sub reflect_source_object_attribute {
603 my ($self, %opts) = @_;
604 unless( $opts{attribute_name} && $opts{class} && $opts{parent_class}
605 && ( $opts{source_class} || $opts{domain_model_name} ) ){
606 confess( "Error: class, parent_class, attribute_name, and either " .
607 "domain_model_name or source_class are required parameters" );
610 my $meta = $opts{class}->meta;
611 my $attr_opts = $self->parameters_for_source_object_attribute(%opts);
613 my $make_immutable = $meta->is_immutable;
614 $meta->make_mutable if $meta->is_immutable;
616 my $attr = $meta->add_attribute($opts{attribute_name}, %$attr_opts);
618 $meta->make_immutable if $make_immutable;
622 # needs class, attribute_name domain_model_name
623 sub parameters_for_source_object_attribute {
624 my ($self, %opts) = @_;
626 my $class = delete $opts{class};
627 my $attr_name = delete $opts{attribute_name};
628 my $dm_name = delete $opts{domain_model_name};
629 my $source_class = delete $opts{source_class};
630 my $parent_class = delete $opts{parent_class};
631 confess("parent_class is a required argument") unless $parent_class;
632 confess("You must supply at least one of domain_model_name and source_class")
633 unless $dm_name || $source_class;
636 $source = $source_class->result_source_instance if $source_class;
638 if( !$source_class ){
639 my $dm = $class->meta->find_attribute_by_name($dm_name);
640 $source_class = $dm->_isa_metadata;
641 $source = $source_class->result_source_instance;
642 } elsif( !$dm_name ){
643 ($dm_name) = map{$_->name} grep{$_->_isa_metadata eq $source_class}
644 $class->meta->domain_models;
645 if( !$dm_name ){ #last resort guess
646 my $tentative = $self->dm_name_from_source_name($source->source_name);
647 ($dm_name) = $tentative if grep{$_->name eq $tentative} $class->domain_models;
651 my $from_attr = $source_class->meta->find_attribute_by_name($attr_name);
652 my $reader = $from_attr->get_read_method;
653 die("Could not find reader for attribute '$attr_name' on $source_class")
656 #default options. lazy build but no outsider method
657 my %attr_opts = ( is => 'ro', lazy => 1, required => 1,
658 clearer => "_clear_${attr_name}",
660 "has_${attr_name}" =>
661 sub { defined(shift->$dm_name->$reader) }
663 domain_model => $dm_name,
664 orig_attr_name => $attr_name,
666 $attr_opts{coerce} = 1 if $from_attr->should_coerce;
670 if(my $coderef = $source->result_class->can('_m2m_metadata')){
671 $m2m_meta = $source->result_class->$coderef;
674 my $constraint_is_ArrayRef =
675 $from_attr->type_constraint->name eq 'ArrayRef' ||
676 $from_attr->type_constraint->is_subtype_of('ArrayRef');
678 if( my $rel_info = $source->relationship_info($attr_name) ){
679 my $rel_accessor = $rel_info->{attrs}->{accessor};
680 my $rel_moniker = $rel_info->{class}->result_source_instance->source_name;
682 if($rel_accessor eq 'multi' && $constraint_is_ArrayRef) {
684 my $sm = $self->class_name_from_source_name($parent_class, $rel_moniker);
685 #type constraint is a collection, and default builds it
686 my $isa = $attr_opts{isa} = $self->class_name_for_collection_of($sm);
687 $attr_opts{default} = eval "sub {
688 my \$rs = shift->${dm_name}->related_resultset('${attr_name}');
689 return ${isa}->new(_source_resultset => \$rs);
691 } elsif( $rel_accessor eq 'single' || $rel_accessor eq 'filter' ) {
693 #type constraint is the foreign IM object, default inflates it
694 my $isa = $attr_opts{isa} = $self->class_name_from_source_name($parent_class, $rel_moniker);
695 $attr_opts{default} = eval "sub {
696 if (defined(my \$o = shift->${dm_name}->${reader})) {
697 return ${isa}->inflate_result(\$o->result_source, { \$o->get_columns });
702 } elsif( $constraint_is_ArrayRef && $attr_name =~ m/^(.*)_list$/ ) {
705 my $link_table = "links_to_${mm_name}_list";
706 my ($hm_source, $far_side);
707 eval { $hm_source = $source->related_source($link_table); }
708 || confess "Can't find ${link_table} has_many for ${mm_name}_list";
709 eval { $far_side = $hm_source->related_source($mm_name); }
710 || confess "Can't find ${mm_name} belongs_to on ".$hm_source->result_class
711 ." traversing many-many for ${mm_name}_list";
713 my $sm = $self->class_name_from_source_name($parent_class,$far_side->source_name);
714 my $isa = $attr_opts{isa} = $self->class_name_for_collection_of($sm);
716 #proper collections will remove the result_class uglyness.
717 $attr_opts{default} = eval "sub {
718 my \$rs = shift->${dm_name}->related_resultset('${link_table}')->related_resultset('${mm_name}');
719 return ${isa}->new(_source_resultset => \$rs);
721 } elsif( $constraint_is_ArrayRef && defined $m2m_meta && exists $m2m_meta->{$attr_name} ){
722 #m2m if using introspectable m2m component
723 my $rel = $m2m_meta->{$attr_name}->{relation};
724 my $far_rel = $m2m_meta->{$attr_name}->{foreign_relation};
725 my $far_source = $source->related_source($rel)->related_source($far_rel);
726 my $sm = $self->class_name_from_source_name($parent_class, $far_source->source_name);
727 my $isa = $attr_opts{isa} = $self->class_name_for_collection_of($sm);
729 my $rs_meth = $m2m_meta->{$attr_name}->{rs_method};
730 $attr_opts{default} = eval "sub {
731 return ${isa}->new(_source_resultset => shift->${dm_name}->${rs_meth});
735 $attr_opts{isa} = $from_attr->_isa_metadata;
736 my $default_code = "sub{ shift->${dm_name}->${reader} }";
737 $attr_opts{default} = eval $default_code;
738 die "Could not generate default for attribute, code '$default_code' did not compile with: $@" if $@;
742 sub reflect_source_action {
743 my($self, %opts) = @_;
744 my $name = delete $opts{name};
745 my $base = delete $opts{base} || Action;
746 my $roles = delete $opts{roles} || [];
747 my $class = delete $opts{class};
748 my $object = delete $opts{object_class};
749 my $source = delete $opts{source_class};
751 confess("name, object_class and source_class are required arguments")
752 unless $source && $name && $object;
754 my $attr_rules = delete $opts{attributes};
755 $class ||= $object->_default_action_class_for($name);
757 Class::MOP::load_class( $base );
758 Class::MOP::load_class( $object );
759 Class::MOP::load_class( $source );
761 #print STDERR "\n\t", ref $attr_rules eq 'ARRAY' ? @$attr_rules : $attr_rules,"\n";
762 # attributes => undef, #default to qr/./
763 # attributes => [], #default to nothing
764 # attributes => qr//, #DWIM, treated as [qr//]
765 # attributes => [{...}] #DWIM, treat as [qr/./, {...} ]
766 # attributes => [[-exclude => ...]] #DWIM, treat as [qr/./, [-exclude => ...]]
767 my $attr_haystack = [ map { $_->name } $object->parameter_attributes ];
768 if(!defined $attr_rules){
769 $attr_rules = [qr/./];
770 } elsif( (!ref $attr_rules && $attr_rules) || (ref $attr_rules eq 'Regexp') ){
771 $attr_rules = [ $attr_rules ];
772 } elsif( ref $attr_rules eq 'ARRAY' && @$attr_rules){
773 #don't add a qr/./ rule if we have at least one match rule
774 push(@$attr_rules, qr/./) unless
775 grep {(ref $_ eq 'ARRAY' && $_->[0] ne '-exclude')
776 || !ref $_ || ref $_ eq 'Regexp'} @$attr_rules;
779 #print STDERR "${name}\t${class}\t${base}\n";
780 #print STDERR "\t${object}\t${source}\n";
781 #print STDERR "\t",@$attr_rules,"\n";
783 my $o_meta = $object->meta;
784 my $s_meta = $source->meta;
785 my $attributes = $self->parse_reflect_rules($attr_rules, $attr_haystack);
788 my $meta = $self->_load_or_create(
790 superclasses => [$base],
791 ( @$roles ? (roles => $roles) : ()),
793 my $make_immutable = $meta->is_immutable || $self->make_classes_immutable;
794 $meta->make_mutable if $meta->is_immutable;
796 for my $attr_name (keys %$attributes){
797 my $attr_opts = $attributes->{$attr_name} || {};
798 my $o_attr = $o_meta->find_attribute_by_name($attr_name);
799 my $s_attr_name = $o_attr->orig_attr_name || $attr_name;
800 my $s_attr = $s_meta->find_attribute_by_name($s_attr_name);
801 confess("Unable to find attribute for '${s_attr_name}' via '${source}'")
802 unless defined $s_attr;
803 next unless $s_attr->get_write_method
804 && $s_attr->get_write_method !~ /^_/; #only rw attributes!
806 my $attr_params = $self->parameters_for_source_object_action_attribute
808 object_class => $object,
809 source_class => $source,
810 attribute_name => $attr_name
812 $meta->add_attribute( $attr_name => %$attr_params);
815 $meta->make_immutable if $make_immutable;
818 sub parameters_for_source_object_action_attribute {
819 my ($self, %opts) = @_;
821 my $object = delete $opts{object_class};
822 my $attr_name = delete $opts{attribute_name};
823 my $source_class = delete $opts{source_class};
824 confess("object_class and attribute_name are required parameters")
825 unless $attr_name && $object;
827 my $o_meta = $object->meta;
828 my $dm_name = $o_meta->find_attribute_by_name($attr_name)->domain_model;
829 $source_class ||= $o_meta->find_attribute_by_name($dm_name)->_isa_metadata;
830 my $from_attr = $source_class->meta->find_attribute_by_name($attr_name);
832 #print STDERR "$attr_name is type: " . $from_attr->meta->name . "\n";
834 confess("${attr_name} is not writeable and can not be reflected")
835 unless $from_attr->get_write_method;
839 isa => $from_attr->_isa_metadata,
840 required => $from_attr->is_required,
841 ($from_attr->is_required
842 ? () : (clearer => "clear_${attr_name}")),
843 predicate => "has_${attr_name}",
846 if ($attr_opts{required}) {
847 if($from_attr->has_default) {
848 $attr_opts{lazy} = 1;
849 $attr_opts{default} = $from_attr->default;
851 $attr_opts{lazy_fail} = 1;
857 if(my $coderef = $source_class->result_class->can('_m2m_metadata')){
858 $m2m_meta = $source_class->result_class->$coderef;
860 #test for relationships
861 my $constraint_is_ArrayRef =
862 $from_attr->type_constraint->name eq 'ArrayRef' ||
863 $from_attr->type_constraint->is_subtype_of('ArrayRef');
865 my $source = $source_class->result_source_instance;
866 if (my $rel_info = $source->relationship_info($attr_name)) {
867 my $rel_accessor = $rel_info->{attrs}->{accessor};
869 if($rel_accessor eq 'multi' && $constraint_is_ArrayRef) {
870 confess "${attr_name} is a rw has_many, this won't work.";
871 } elsif( $rel_accessor eq 'single' || $rel_accessor eq 'filter') {
872 $attr_opts{valid_values} = sub {
873 shift->target_model->result_source->related_source($attr_name)->resultset;
876 } elsif ( $constraint_is_ArrayRef && $attr_name =~ m/^(.*)_list$/) {
878 my $link_table = "links_to_${mm_name}_list";
879 $attr_opts{default} = sub { [] };
880 $attr_opts{valid_values} = sub {
881 shift->target_model->result_source->related_source($link_table)
882 ->related_source($mm_name)->resultset;
884 } elsif( $constraint_is_ArrayRef && defined $m2m_meta && exists $m2m_meta->{$attr_name} ){
885 #m2m if using introspectable m2m component
886 my $rel = $m2m_meta->{$attr_name}->{relation};
887 my $far_rel = $m2m_meta->{$attr_name}->{foreign_relation};
888 $attr_opts{default} = sub { [] };
889 $attr_opts{valid_values} = sub {
890 shift->target_model->result_source->related_source($rel)
891 ->related_source($far_rel)->resultset;
895 #print STDERR "\n" .$attr_name ." - ". $object . "\n";
896 #print STDERR Dumper(\%attr_opts);
900 sub _load_or_create {
901 my ($self, $class, %options) = @_;
903 if( $self->_maybe_load_class($class) ){
907 if( exists $options{superclasses} ){
908 ($base) = @{ $options{superclasses} };
910 $base = 'Reaction::InterfaceModel::Action';
912 return $base->meta->create($class, %options);
915 sub _maybe_load_class {
916 my ($self, $class) = @_;
917 my $file = $class . '.pm';
919 my $ret = eval { Class::MOP::load_class($class) };
920 if ($INC{$file} && $@) {
921 confess "Error loading ${class}: $@";
926 __PACKAGE__->meta->make_immutable;
931 #--------#---------#---------#---------#---------#---------#---------#---------#
936 Reaction::InterfaceModel::Reflector::DBIC -
937 Automatically Generate InterfaceModels from DBIx::Class models
941 The InterfaceModel reflectors are classes that are meant to aid you in easily
942 generating Reaction::InterfaceModel classes that represent their underlying
943 DBIx::Class domain models by introspecting your L<DBIx::Class::ResultSource>s
944 and creating a collection of L<Reaction::InterfaceModel::Object> and
945 L<Reaction::InterfaceModel::Collection> classes for you to use.
947 The default base class of all Object classes will be
948 L<Reaction::InterfaceModel::Object> and the default Collection type will be
949 L<Reaction::InterfaceModel::Collection::Virtual::ResultSet>.
951 Additionally, the reflector can create InterfaceModel actions that interact
952 with the supplied L<Reaction::UI::Controller::Collection::CRUD>, allowing you
953 to easily set up a highly customizable CRUD interface in minimal time.
955 At this time, supported collection actions consist of:
959 =item B<> L<Reaction::INterfaceModel::Action::DBIC::ResultSet::Create>
961 Creates a new item in the collection and underlying ResultSet.
963 =item B<> L<Reaction::INterfaceModel::Action::DBIC::ResultSet::DeleteAll>
965 Deletes all the items in a collection and it's underlying resultset using
970 And supported object actions are :
974 =item B<Update> - via L<Reaction::INterfaceModel::Action::DBIC::Result::Update>
976 Updates an existing object.
978 =item B<Delete> - via L<Reaction::INterfaceModel::Action::DBIC::Result::Delete>
980 Deletes an existing object.
986 package MyApp::IM::TestModel;
987 use base 'Reaction::InterfaceModel::Object';
989 use Reaction::InterfaceModel::Reflector::DBIC;
990 my $reflector = Reaction::InterfaceModel::Reflector::DBIC->new;
993 $reflector->reflect_schema
995 model_class => __PACKAGE__,
996 schema_class => 'MyApp::Schema',
999 =head2 Selectively including and excluding sources
1001 #reflect everything except for the FooBar and FooBaz classes
1002 $reflector->reflect_schema
1004 model_class => __PACKAGE__,
1005 schema_class => 'MyApp::Schema',
1006 sources => [-exclude => [qw/FooBar FooBaz/] ],
1007 # you could also do:
1008 sources => [-exclude => qr/(?:FooBar|FooBaz)/,
1010 sources => [-exclude => [qr/FooBar/, qr/FooBaz/],
1013 #reflect only the Foo family of sources
1014 $reflector->reflect_schema
1016 model_class => __PACKAGE__,
1017 schema_class => 'MyApp::Schema',
1018 sources => qr/^Foo/,
1021 =head2 Selectively including and excluding fields in sources
1023 #Reflect Foo and Baz in their entirety and exclude the field 'avatar' in the Bar ResultSource
1024 $reflector->reflect_schema
1026 model_class => __PACKAGE__,
1027 schema_class => 'MyApp::Schema',
1028 sources => [qw/Foo Baz/,
1029 [ Bar => {attributes => [[-exclude => 'avatar']] } ],
1030 # or exclude by regex
1031 [ Bar => {attributes => [-exclude => qr/avatar/] } ],
1032 # or simply do not include it...
1033 [ Bar => {attributes => [qw/id name description/] } ],
1039 =head2 make_classes_immutable
1041 =head2 object_actions
1043 =head2 collection_actions
1045 =head2 default_object_actions
1047 =head2 default_collection_actions
1049 =head2 builtin_object_actions
1051 =head2 builtin_collection_actions
1057 =head2 _all_object_actions
1059 =head2 _all_collection_actions
1061 =head2 dm_name_from_class_name
1063 =head2 dm_name_from_source_name
1065 =head2 class_name_from_source_name
1067 =head2 class_name_for_collection_of
1071 =head2 parse_reflect_rules
1073 =head2 merge_reflect_rules
1075 =head2 reflect_schema
1077 =head2 _compute_source_options
1081 =head2 reflect_source
1083 =head2 reflect_source_collection
1085 =head2 reflect_source_object
1087 =head2 reflect_source_object_attribute
1089 =head2 parameters_for_source_object_attribute
1091 =head2 reflect_source_action
1093 =head2 parameters_for_source_object_action_attribute
1097 Allow the reflector to dump the generated code out as files, eliminating the need to
1098 reflect on startup every time. This will likely take quite a bit of work though. The
1099 main work is already in place, but the grunt work is still left. At the moment there
1100 is no closures that can't be dumped out as code with a little bit of work.
1104 See L<Reaction::Class> for authors.
1108 See L<Reaction::Class> for the license.