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;
654 #default options. lazy build but no outsider method
655 my %attr_opts = ( is => 'ro', lazy => 1, required => 1,
656 clearer => "_clear_${attr_name}",
658 "has_${attr_name}" =>
659 sub { defined(shift->$dm_name->$reader) }
661 domain_model => $dm_name,
662 orig_attr_name => $attr_name,
667 if(my $coderef = $source->result_class->can('_m2m_metadata')){
668 $m2m_meta = $source->result_class->$coderef;
671 my $constraint_is_ArrayRef =
672 $from_attr->type_constraint->name eq 'ArrayRef' ||
673 $from_attr->type_constraint->is_subtype_of('ArrayRef');
675 if( my $rel_info = $source->relationship_info($attr_name) ){
676 my $rel_accessor = $rel_info->{attrs}->{accessor};
677 my $rel_moniker = $rel_info->{class}->result_source_instance->source_name;
679 if($rel_accessor eq 'multi' && $constraint_is_ArrayRef) {
681 my $sm = $self->class_name_from_source_name($parent_class, $rel_moniker);
682 #type constraint is a collection, and default builds it
683 my $isa = $attr_opts{isa} = $self->class_name_for_collection_of($sm);
684 $attr_opts{default} = eval "sub {
685 my \$rs = shift->${dm_name}->related_resultset('${attr_name}');
686 return ${isa}->new(_source_resultset => \$rs);
688 } elsif( $rel_accessor eq 'single' || $rel_accessor eq 'filter' ) {
690 #type constraint is the foreign IM object, default inflates it
691 my $isa = $attr_opts{isa} = $self->class_name_from_source_name($parent_class, $rel_moniker);
692 $attr_opts{default} = eval "sub {
693 if (defined(my \$o = shift->${dm_name}->${reader})) {
694 return ${isa}->inflate_result(\$o->result_source, { \$o->get_columns });
699 } elsif( $constraint_is_ArrayRef && $attr_name =~ m/^(.*)_list$/ ) {
702 my $link_table = "links_to_${mm_name}_list";
703 my ($hm_source, $far_side);
704 eval { $hm_source = $source->related_source($link_table); }
705 || confess "Can't find ${link_table} has_many for ${mm_name}_list";
706 eval { $far_side = $hm_source->related_source($mm_name); }
707 || confess "Can't find ${mm_name} belongs_to on ".$hm_source->result_class
708 ." traversing many-many for ${mm_name}_list";
710 my $sm = $self->class_name_from_source_name($parent_class,$far_side->source_name);
711 my $isa = $attr_opts{isa} = $self->class_name_for_collection_of($sm);
713 #proper collections will remove the result_class uglyness.
714 $attr_opts{default} = eval "sub {
715 my \$rs = shift->${dm_name}->related_resultset('${link_table}')->related_resultset('${mm_name}');
716 return ${isa}->new(_source_resultset => \$rs);
718 } elsif( $constraint_is_ArrayRef && defined $m2m_meta && exists $m2m_meta->{$attr_name} ){
719 #m2m if using introspectable m2m component
720 my $rel = $m2m_meta->{$attr_name}->{relation};
721 my $far_rel = $m2m_meta->{$attr_name}->{foreign_relation};
722 my $far_source = $source->related_source($rel)->related_source($far_rel);
723 my $sm = $self->class_name_from_source_name($parent_class, $far_source->source_name);
724 my $isa = $attr_opts{isa} = $self->class_name_for_collection_of($sm);
726 my $rs_meth = $m2m_meta->{$attr_name}->{rs_method};
727 $attr_opts{default} = eval "sub {
728 return ${isa}->new(_source_resultset => shift->${dm_name}->${rs_meth});
732 $attr_opts{isa} = $from_attr->_isa_metadata;
733 $attr_opts{default} = eval "sub{ shift->${dm_name}->${reader} }";
737 sub reflect_source_action {
738 my($self, %opts) = @_;
739 my $name = delete $opts{name};
740 my $base = delete $opts{base} || Action;
741 my $roles = delete $opts{roles} || [];
742 my $class = delete $opts{class};
743 my $object = delete $opts{object_class};
744 my $source = delete $opts{source_class};
746 confess("name, object_class and source_class are required arguments")
747 unless $source && $name && $object;
749 my $attr_rules = delete $opts{attributes};
750 $class ||= $object->_default_action_class_for($name);
752 Class::MOP::load_class( $base );
753 Class::MOP::load_class( $object );
754 Class::MOP::load_class( $source );
756 #print STDERR "\n\t", ref $attr_rules eq 'ARRAY' ? @$attr_rules : $attr_rules,"\n";
757 # attributes => undef, #default to qr/./
758 # attributes => [], #default to nothing
759 # attributes => qr//, #DWIM, treated as [qr//]
760 # attributes => [{...}] #DWIM, treat as [qr/./, {...} ]
761 # attributes => [[-exclude => ...]] #DWIM, treat as [qr/./, [-exclude => ...]]
762 my $attr_haystack = [ map { $_->name } $object->parameter_attributes ];
763 if(!defined $attr_rules){
764 $attr_rules = [qr/./];
765 } elsif( (!ref $attr_rules && $attr_rules) || (ref $attr_rules eq 'Regexp') ){
766 $attr_rules = [ $attr_rules ];
767 } elsif( ref $attr_rules eq 'ARRAY' && @$attr_rules){
768 #don't add a qr/./ rule if we have at least one match rule
769 push(@$attr_rules, qr/./) unless
770 grep {(ref $_ eq 'ARRAY' && $_->[0] ne '-exclude')
771 || !ref $_ || ref $_ eq 'Regexp'} @$attr_rules;
774 #print STDERR "${name}\t${class}\t${base}\n";
775 #print STDERR "\t${object}\t${source}\n";
776 #print STDERR "\t",@$attr_rules,"\n";
778 my $o_meta = $object->meta;
779 my $s_meta = $source->meta;
780 my $attributes = $self->parse_reflect_rules($attr_rules, $attr_haystack);
783 my $meta = $self->_load_or_create(
785 superclasses => [$base],
786 ( @$roles ? (roles => $roles) : ()),
788 my $make_immutable = $meta->is_immutable || $self->make_classes_immutable;
789 $meta->make_mutable if $meta->is_immutable;
791 for my $attr_name (keys %$attributes){
792 my $attr_opts = $attributes->{$attr_name} || {};
793 my $o_attr = $o_meta->find_attribute_by_name($attr_name);
794 my $s_attr_name = $o_attr->orig_attr_name || $attr_name;
795 my $s_attr = $s_meta->find_attribute_by_name($s_attr_name);
796 confess("Unable to find attribute for '${s_attr_name}' via '${source}'")
797 unless defined $s_attr;
798 next unless $s_attr->get_write_method
799 && $s_attr->get_write_method !~ /^_/; #only rw attributes!
801 my $attr_params = $self->parameters_for_source_object_action_attribute
803 object_class => $object,
804 source_class => $source,
805 attribute_name => $attr_name
807 $meta->add_attribute( $attr_name => %$attr_params);
810 $meta->make_immutable if $make_immutable;
813 sub parameters_for_source_object_action_attribute {
814 my ($self, %opts) = @_;
816 my $object = delete $opts{object_class};
817 my $attr_name = delete $opts{attribute_name};
818 my $source_class = delete $opts{source_class};
819 confess("object_class and attribute_name are required parameters")
820 unless $attr_name && $object;
822 my $o_meta = $object->meta;
823 my $dm_name = $o_meta->find_attribute_by_name($attr_name)->domain_model;
824 $source_class ||= $o_meta->find_attribute_by_name($dm_name)->_isa_metadata;
825 my $from_attr = $source_class->meta->find_attribute_by_name($attr_name);
827 #print STDERR "$attr_name is type: " . $from_attr->meta->name . "\n";
829 confess("${attr_name} is not writeable and can not be reflected")
830 unless $from_attr->get_write_method;
834 isa => $from_attr->_isa_metadata,
835 required => $from_attr->is_required,
836 ($from_attr->is_required
837 ? () : (clearer => "clear_${attr_name}")),
838 predicate => "has_${attr_name}",
841 if ($attr_opts{required}) {
842 if($from_attr->has_default) {
843 $attr_opts{lazy} = 1;
844 $attr_opts{default} = $from_attr->default;
846 $attr_opts{lazy_fail} = 1;
852 if(my $coderef = $source_class->result_class->can('_m2m_metadata')){
853 $m2m_meta = $source_class->result_class->$coderef;
855 #test for relationships
856 my $constraint_is_ArrayRef =
857 $from_attr->type_constraint->name eq 'ArrayRef' ||
858 $from_attr->type_constraint->is_subtype_of('ArrayRef');
860 my $source = $source_class->result_source_instance;
861 if (my $rel_info = $source->relationship_info($attr_name)) {
862 my $rel_accessor = $rel_info->{attrs}->{accessor};
864 if($rel_accessor eq 'multi' && $constraint_is_ArrayRef) {
865 confess "${attr_name} is a rw has_many, this won't work.";
866 } elsif( $rel_accessor eq 'single' || $rel_accessor eq 'filter') {
867 $attr_opts{valid_values} = sub {
868 shift->target_model->result_source->related_source($attr_name)->resultset;
871 } elsif ( $constraint_is_ArrayRef && $attr_name =~ m/^(.*)_list$/) {
873 my $link_table = "links_to_${mm_name}_list";
874 $attr_opts{default} = sub { [] };
875 $attr_opts{valid_values} = sub {
876 shift->target_model->result_source->related_source($link_table)
877 ->related_source($mm_name)->resultset;
879 } elsif( $constraint_is_ArrayRef && defined $m2m_meta && exists $m2m_meta->{$attr_name} ){
880 #m2m if using introspectable m2m component
881 my $rel = $m2m_meta->{$attr_name}->{relation};
882 my $far_rel = $m2m_meta->{$attr_name}->{foreign_relation};
883 $attr_opts{default} = sub { [] };
884 $attr_opts{valid_values} = sub {
885 shift->target_model->result_source->related_source($rel)
886 ->related_source($far_rel)->resultset;
890 #print STDERR "\n" .$attr_name ." - ". $object . "\n";
891 #print STDERR Dumper(\%attr_opts);
895 sub _load_or_create {
896 my ($self, $class, %options) = @_;
898 if( $self->_maybe_load_class($class) ){
902 if( exists $options{superclasses} ){
903 ($base) = @{ $options{superclasses} };
905 $base = 'Reaction::InterfaceModel::Action';
907 return $base->meta->create($class, %options);
910 sub _maybe_load_class {
911 my ($self, $class) = @_;
912 my $file = $class . '.pm';
914 my $ret = eval { Class::MOP::load_class($class) };
915 if ($INC{$file} && $@) {
916 confess "Error loading ${class}: $@";
921 __PACKAGE__->meta->make_immutable;
926 #--------#---------#---------#---------#---------#---------#---------#---------#
931 Reaction::InterfaceModel::Reflector::DBIC -
932 Automatically Generate InterfaceModels from DBIx::Class models
936 The InterfaceModel reflectors are classes that are meant to aid you in easily
937 generating Reaction::InterfaceModel classes that represent their underlying
938 DBIx::Class domain models by introspecting your L<DBIx::Class::ResultSource>s
939 and creating a collection of L<Reaction::InterfaceModel::Object> and
940 L<Reaction::InterfaceModel::Collection> classes for you to use.
942 The default base class of all Object classes will be
943 L<Reaction::InterfaceModel::Object> and the default Collection type will be
944 L<Reaction::InterfaceModel::Collection::Virtual::ResultSet>.
946 Additionally, the reflector can create InterfaceModel actions that interact
947 with the supplied L<Reaction::UI::Controller::Collection::CRUD>, allowing you
948 to easily set up a highly customizable CRUD interface in minimal time.
950 At this time, supported collection actions consist of:
954 =item B<> L<Reaction::INterfaceModel::Action::DBIC::ResultSet::Create>
956 Creates a new item in the collection and underlying ResultSet.
958 =item B<> L<Reaction::INterfaceModel::Action::DBIC::ResultSet::DeleteAll>
960 Deletes all the items in a collection and it's underlying resultset using
965 And supported object actions are :
969 =item B<Update> - via L<Reaction::INterfaceModel::Action::DBIC::Result::Update>
971 Updates an existing object.
973 =item B<Delete> - via L<Reaction::INterfaceModel::Action::DBIC::Result::Delete>
975 Deletes an existing object.
981 package MyApp::IM::TestModel;
982 use base 'Reaction::InterfaceModel::Object';
984 use Reaction::InterfaceModel::Reflector::DBIC;
985 my $reflector = Reaction::InterfaceModel::Reflector::DBIC->new;
988 $reflector->reflect_schema
990 model_class => __PACKAGE__,
991 schema_class => 'MyApp::Schema',
994 =head2 Selectively including and excluding sources
996 #reflect everything except for the FooBar and FooBaz classes
997 $reflector->reflect_schema
999 model_class => __PACKAGE__,
1000 schema_class => 'MyApp::Schema',
1001 sources => [-exclude => [qw/FooBar FooBaz/] ],
1002 # you could also do:
1003 sources => [-exclude => qr/(?:FooBar|FooBaz)/,
1005 sources => [-exclude => [qr/FooBar/, qr/FooBaz/],
1008 #reflect only the Foo family of sources
1009 $reflector->reflect_schema
1011 model_class => __PACKAGE__,
1012 schema_class => 'MyApp::Schema',
1013 sources => qr/^Foo/,
1016 =head2 Selectively including and excluding fields in sources
1018 #Reflect Foo and Baz in their entirety and exclude the field 'avatar' in the Bar ResultSource
1019 $reflector->reflect_schema
1021 model_class => __PACKAGE__,
1022 schema_class => 'MyApp::Schema',
1023 sources => [qw/Foo Baz/,
1024 [ Bar => {attributes => [[-exclude => 'avatar']] } ],
1025 # or exclude by regex
1026 [ Bar => {attributes => [-exclude => qr/avatar/] } ],
1027 # or simply do not include it...
1028 [ Bar => {attributes => [qw/id name description/] } ],
1034 =head2 make_classes_immutable
1036 =head2 object_actions
1038 =head2 collection_actions
1040 =head2 default_object_actions
1042 =head2 default_collection_actions
1044 =head2 builtin_object_actions
1046 =head2 builtin_collection_actions
1052 =head2 _all_object_actions
1054 =head2 _all_collection_actions
1056 =head2 dm_name_from_class_name
1058 =head2 dm_name_from_source_name
1060 =head2 class_name_from_source_name
1062 =head2 class_name_for_collection_of
1066 =head2 parse_reflect_rules
1068 =head2 merge_reflect_rules
1070 =head2 reflect_schema
1072 =head2 _compute_source_options
1076 =head2 reflect_source
1078 =head2 reflect_source_collection
1080 =head2 reflect_source_object
1082 =head2 reflect_source_object_attribute
1084 =head2 parameters_for_source_object_attribute
1086 =head2 reflect_source_action
1088 =head2 parameters_for_source_object_action_attribute
1092 Allow the reflector to dump the generated code out as files, eliminating the need to
1093 reflect on startup every time. This will likely take quite a bit of work though. The
1094 main work is already in place, but the grunt work is still left. At the moment there
1095 is no closures that can't be dumped out as code with a little bit of work.
1099 See L<Reaction::Class> for authors.
1103 See L<Reaction::Class> for the license.