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';
18 has make_classes_immutable => (isa => "Bool", is => "rw", required => 1, default => sub{ 1 });
20 #user defined actions and prototypes
21 has object_actions => (isa => "HashRef", is => "rw", lazy_build => 1);
22 has collection_actions => (isa => "HashRef", is => "rw", lazy_build => 1);
24 #which actions to create by default
25 has default_object_actions => (isa => "ArrayRef", is => "rw", lazy_build => 1);
26 has default_collection_actions => (isa => "ArrayRef", is => "rw", lazy_build => 1);
28 #builtin actions and prototypes
29 has builtin_object_actions => (isa => "HashRef", is => "rw", lazy_build => 1);
30 has builtin_collection_actions => (isa => "HashRef", is => "rw", lazy_build => 1);
32 implements _build_object_actions => as { {} };
33 implements _build_collection_actions => as { {} };
35 implements _build_default_object_actions => as { [ qw/Update Delete/ ] };
36 implements _build_default_collection_actions => as { [ qw/Create DeleteAll/ ] };
38 implements _build_builtin_object_actions => as {
40 Update => { name => 'Update', base => Update },
41 Delete => { name => 'Delete', base => Delete, attributes => [] },
45 implements _build_builtin_collection_actions => as {
47 Create => {name => 'Create', base => Create },
48 DeleteAll => {name => 'DeleteAll', base => DeleteAll, attributes => [] }
52 implements _all_object_actions => as {
54 return $self->merge_hashes
55 ($self->builtin_object_actions, $self->object_actions);
58 implements _all_collection_actions => as {
60 return $self->merge_hashes
61 ($self->builtin_collection_actions, $self->collection_actions);
64 implements dm_name_from_class_name => as {
65 my($self, $class) = @_;
66 confess("wrong arguments") unless $class;
68 $class = "_" . $self->_class_to_attribute_name($class) . "_store";
72 implements dm_name_from_source_name => as {
73 my($self, $source) = @_;
74 confess("wrong arguments") unless $source;
75 $source =~ s/([a-z0-9])([A-Z])/${1}_${2}/g ;
76 $source = "_" . $self->_class_to_attribute_name($source) . "_store";
80 implements class_name_from_source_name => as {
81 my ($self, $model_class, $source_name) = @_;
82 confess("wrong arguments") unless $model_class && $source_name;
83 return join "::", $model_class, $source_name;
86 implements class_name_for_collection_of => as {
87 my ($self, $object_class) = @_;
88 confess("wrong arguments") unless $object_class;
89 return "${object_class}::Collection";
92 implements merge_hashes => as {
93 my($self, $left, $right) = @_;
94 return Catalyst::Utils::merge_hashes($left, $right);
97 implements parse_reflect_rules => as {
98 my ($self, $rules, $haystack) = @_;
99 confess('$rules must be an array reference') unless ref $rules eq 'ARRAY';
100 confess('$haystack must be an array reference') unless ref $haystack eq 'ARRAY';
103 my (@exclude, @include, $global_opts);
104 if(@$rules == 2 && $rules->[0] eq '-exclude'){
105 push(@exclude, (ref $rules->[1] eq 'ARRAY' ? @{$rules->[1]} : $rules->[1]));
107 for my $rule ( @$rules ){
108 if (ref $rule eq 'ARRAY' && $rule->[0] eq '-exclude'){
109 push(@exclude, (ref $rule->[1] eq 'ARRAY' ? @{$rule->[1]} : $rule->[1]));
110 } elsif( ref $rule eq 'HASH' ){
111 $global_opts = ref $global_opts eq 'HASH' ?
112 $self->merge_hashes($global_opts, $rule) : $rule;
114 push(@include, $rule);
118 my $check_exclude = sub{
119 for my $rule (@exclude){
120 return 1 if(ref $rule eq 'Regexp' ? $_[0] =~ /$rule/ : $_[0] eq $rule);
125 @$haystack = grep { !$check_exclude->($_) } @$haystack;
126 $self->merge_reflect_rules(\@include, $needles, $haystack, $global_opts);
130 implements merge_reflect_rules => as {
131 my ($self, $rules, $needles, $haystack, $local_opts) = @_;
132 for my $rule ( @$rules ){
133 if(!ref $rule && ( grep {$rule eq $_} @$haystack ) ){
134 $needles->{$rule} = defined $needles->{$rule} ?
135 $self->merge_hashes($needles->{$rule}, $local_opts) : $local_opts;
136 } elsif( ref $rule eq 'Regexp' ){
137 for my $match ( grep { /$rule/ } @$haystack ){
138 $needles->{$match} = defined $needles->{$match} ?
139 $self->merge_hashes($needles->{$match}, $local_opts) : $local_opts;
141 } elsif( ref $rule eq 'ARRAY' ){
143 $opts = pop(@$rule) if @$rule > 1 and ref $rule->[$#$rule] eq 'HASH';
144 $opts = $self->merge_hashes($local_opts, $opts) if defined $local_opts;
145 $self->merge_reflect_rules($rule, $needles, $haystack, $opts);
150 implements reflect_schema => as {
151 my ($self, %opts) = @_;
152 my $base = delete $opts{base} || Object;
153 my $model = delete $opts{model_class};
154 my $schema = delete $opts{schema_class};
155 my $dm_name = delete $opts{domain_model_name};
156 my $dm_args = delete $opts{domain_model_args} || {};
157 $dm_name ||= $self->dm_name_from_class_name($schema);
159 #load all necessary classes
160 confess("model_class and schema_class are required parameters")
161 unless($model && $schema);
162 Class::MOP::load_class( $base );
163 Class::MOP::load_class( $schema );
164 my $meta = $self->_load_or_create($model, $base);
166 # sources => undef, #default to qr/./
167 # sources => [], #default to nothing
168 # sources => qr//, #DWIM, treated as [qr//]
169 # sources => [{...}] #DWIM, treat as [qr/./, {...} ]
170 # sources => [[-exclude => ...]] #DWIM, treat as [qr/./, [-exclude => ...]]
171 my $haystack = [ $schema->sources ];
173 my $rules = delete $opts{sources};
176 } elsif( ref $rules eq 'Regexp'){
178 } elsif( ref $rules eq 'ARRAY' && @$rules){
179 #don't add a qr/./ rule if we have at least one match rule
180 push(@$rules, qr/./) unless grep {(ref $_ eq 'ARRAY' && $_->[0] ne '-exclude')
181 || !ref $_ || ref $_ eq 'Regexp'} @$rules;
184 my $sources = $self->parse_reflect_rules($rules, $haystack);
186 my $make_immutable = $meta->is_immutable || $self->make_classes_immutable;
187 $meta->make_mutable if $meta->is_immutable;
189 $meta->add_domain_model
190 ($dm_name, is => 'rw', isa => $schema, required => 1, %$dm_args);
192 for my $source_name (keys %$sources){
193 my $source_opts = $sources->{$source_name} || {};
194 $self->reflect_source(
195 source_name => $source_name,
196 parent_class => $model,
197 schema_class => $schema,
198 source_class => $schema->class($source_name),
199 parent_domain_model_name => $dm_name,
204 $meta->make_immutable if $make_immutable;
208 implements _compute_source_options => as {
209 my ($self, %opts) = @_;
210 my $schema = delete $opts{schema_class};
211 my $source_name = delete $opts{source_name};
212 my $source_class = delete $opts{source_class};
213 my $parent = delete $opts{parent_class};
214 my $parent_dm = delete $opts{parent_domain_model_name};
216 #this is the part where I hate my life for promissing all sorts of DWIMery
217 confess("parent_class and source_name or source_class are required parameters")
218 unless($parent && ($source_name || $source_class));
220 OUTER: until( $schema && $source_name && $source_class && $parent_dm ){
221 if( $schema && !$source_name){
222 next OUTER if $source_name = $source_class->result_source_instance->source_name;
223 } elsif( $schema && !$source_class){
224 next OUTER if $source_class = eval { $schema->class($source_name) };
227 if($source_class && (!$schema || !$source_name)){
229 $schema = $source_class->result_source_instance->schema;
230 next OUTER if $schema && Class::MOP::load_class($schema);
233 $source_name = $source_class->result_source_instance->source_name;
234 next OUTER if $source_name;
237 my @haystack = $parent_dm ?
238 $parent->meta->find_attribute_by_name($parent_dm) : $parent->domain_models;
240 #there's a lot of guessing going on, but it should work fine on most cases
241 INNER: for my $needle (@haystack){
242 my $isa = $needle->_isa_metadata;
243 next INNER unless Class::MOP::load_class( $isa->_isa_metadata );
244 next INNER unless $isa->isa('DBIx::Class::Schema');
245 if(!$parent_dm && $schema && $isa eq $schema){
246 $parent_dm = $needle->name;
251 my $src_class = eval{ $isa->class($source_name) };
252 next INNER unless $src_class;
253 next INNER if($source_class && $source_class ne $src_class);
255 $parent_dm = $needle->name;
256 $source_class = $src_class;
261 #do we even need to go this far?
262 if( !$parent_dm && $schema ){
263 my $tentative = $self->dm_name_from_class_name($schema);
264 $parent_dm = $tentative if grep{$_->name eq $tentative} @haystack;
267 confess("Could not determine options automatically from: schema " .
268 "'${schema}', source_name '${source_name}', source_class " .
269 "'${source_class}', parent_domain_model_name '${parent_dm}'");
273 source_name => $source_name,
274 schema_class => $schema,
275 source_class => $source_class,
276 parent_class => $parent,
277 parent_domain_model_name => $parent_dm,
281 implements _class_to_attribute_name => as {
282 my ( $self, $str ) = @_;
283 confess("wrong arguments passed for _class_to_attribute_name") unless $str;
284 return join('_', map lc, split(/::|(?<=[a-z0-9])(?=[A-Z])/, $str))
287 implements add_source => as {
288 my ($self, %opts) = @_;
290 my $model = delete $opts{model_class};
291 my $reader = delete $opts{reader};
292 my $source = delete $opts{source_name};
293 my $dm_name = delete $opts{domain_model_name};
294 my $collection = delete $opts{collection_class};
295 my $name = delete $opts{attribute_name} || $source;
297 confess("model_class and source_name are required parameters")
298 unless $model && $source;
299 my $meta = $model->meta;
301 unless( $collection ){
302 my $object = $self->class_name_from_source_name($model, $source);
303 $collection = $self->class_name_for_collection_of($object);
307 $reader =~ s/([a-z0-9])([A-Z])/${1}_${2}/g ;
308 $reader = $self->_class_to_attribute_name($reader) . "_collection";
311 my @haystack = $meta->domain_models;
313 @haystack = grep { $_->_isa_metadata->isa('DBIx::Class::Schema') } @haystack;
316 $dm_name = $haystack[0]->name;
317 } elsif(@haystack > 1){
318 confess("Failed to automatically determine domain_model_name. More than one " .
319 "possible match (".(join ", ", map{"'".$_->name."'"} @haystack).")");
321 confess("Failed to automatically determine domain_model_name. No matches.");
331 predicate => "has_" . $self->_class_to_attribute_name($name) ,
332 domain_model => $dm_name,
333 orig_attr_name => $source,
337 _source_resultset => $_[0]->$dm_name->resultset($source),
343 my $make_immutable = $meta->is_immutable;
344 $meta->make_mutable if $make_immutable;
345 my $attr = $meta->add_attribute($name, %attr_opts);
346 $meta->make_immutable if $make_immutable;
351 implements reflect_source => as {
352 my ($self, %opts) = @_;
353 my $collection = delete $opts{collection} || {};
354 %opts = %{ $self->merge_hashes(\%opts, $self->_compute_source_options(%opts)) };
356 my $obj_meta = $self->reflect_source_object(%opts);
357 my $col_meta = $self->reflect_source_collection
359 object_class => $obj_meta->name,
360 source_class => $opts{source_class},
366 model_class => delete $opts{parent_class},
367 domain_model_name => delete $opts{parent_domain_model_name},
368 collection_class => $col_meta->name,
372 implements reflect_source_collection => as {
373 my ($self, %opts) = @_;
374 my $base = delete $opts{base} || ResultSet;
375 my $class = delete $opts{class};
376 my $object = delete $opts{object_class};
377 my $source = delete $opts{source_class};
378 my $action_rules = delete $opts{actions};
380 confess('object_class and source_class are required parameters')
381 unless $object && $source;
382 $class ||= $self->class_name_for_collection_of($object);
384 Class::MOP::load_class( $base );
385 Class::MOP::load_class( $object );
386 my $meta = $self->_load_or_create($class, $base);
388 my $make_immutable = $meta->is_immutable || $self->make_classes_immutable;;
389 $meta->make_mutable if $meta->is_immutable;
390 $meta->add_method(_build_member_type => sub{ $object } );
391 #XXX as a default pass the domain model as a target_model until i come up with something
392 #better through the coercion method
393 my $def_act_args = sub {
395 return { (target_model => $_[0]->_source_resultset), %{ $super->(@_) } };
397 $meta->add_around_method_modifier('_default_action_args_for', $def_act_args);
401 my $all_actions = $self->_all_collection_actions;
402 my $action_haystack = [keys %$all_actions];
403 if(!defined $action_rules){
404 $action_rules = $self->default_collection_actions;
405 } elsif( (!ref $action_rules && $action_rules) || (ref $action_rules eq 'Regexp') ){
406 $action_rules = [ $action_rules ];
407 } elsif( ref $action_rules eq 'ARRAY' && @$action_rules){
408 #don't add a qr/./ rule if we have at least one match rule
409 push(@$action_rules, qr/./)
410 unless grep {(ref $_ eq 'ARRAY' && $_->[0] ne '-exclude')
411 || !ref $_ || ref $_ eq 'Regexp'} @$action_rules;
414 # XXX this is kind of a dirty hack to support custom actions that are not
415 # previously defined and still be able to use the parse_reflect_rules mechanism
416 my @custom_actions = grep {!exists $all_actions->{$_}}
417 map{ $_->[0] } grep {ref $_ eq 'ARRAY' && $_->[0] ne '-exclude'} @$action_rules;
418 push(@$action_haystack, @custom_actions);
419 my $actions = $self->parse_reflect_rules($action_rules, $action_haystack);
420 for my $action (keys %$actions){
421 my $action_opts = $self->merge_hashes
422 ($all_actions->{$action} || {}, $actions->{$action} || {});
424 #NOTE: If the name of the action is not specified in the prototype then use it's
425 #hash key as the name. I think this is sane beahvior, but I've actually been thinking
426 #of making Action prototypes their own separate objects
427 $self->reflect_source_action(
429 object_class => $object,
430 source_class => $source,
434 # XXX i will move this to use the coercion method soon. this will be
435 # GoodEnough until then. I still need to think a little about the type coercion
436 # thing so i don't make a mess of it
437 my $act_args = sub { #override target model for this action
439 return { %{ $super->(@_) },
440 ($_[1] eq $action ? (target_model => $_[0]->_source_resultset) : () ) };
442 $meta->add_around_method_modifier('_default_action_args_for', $act_args);
445 $meta->make_immutable if $make_immutable;
449 implements reflect_source_object => as {
450 my($self, %opts) = @_;
451 %opts = %{ $self->merge_hashes(\%opts, $self->_compute_source_options(%opts)) };
453 my $base = delete $opts{base} || Object;
454 my $class = delete $opts{class};
455 my $dm_name = delete $opts{domain_model_name};
456 my $dm_opts = delete $opts{domain_model_args} || {};
458 my $source_name = delete $opts{source_name};
459 my $schema = delete $opts{schema_class};
460 my $source_class = delete $opts{source_class};
461 my $parent = delete $opts{parent_class};
462 my $parent_dm = delete $opts{parent_domain_model_name};
464 my $action_rules = delete $opts{actions};
465 my $attr_rules = delete $opts{attributes};
467 $class ||= $self->class_name_from_source_name($parent, $source_name);
469 Class::MOP::load_class($parent);
470 Class::MOP::load_class($schema) if $schema;
471 Class::MOP::load_class($source_class);
473 my $meta = $self->_load_or_create($class, $base);
475 #create the domain model
476 $dm_name ||= $self->dm_name_from_source_name($source_name);
478 $dm_opts->{isa} = $source_class;
479 $dm_opts->{is} ||= 'rw';
480 $dm_opts->{required} ||= 1;
482 my $make_immutable = $meta->is_immutable || $self->make_classes_immutable;;
483 $meta->make_mutable if $meta->is_immutable;
485 my $dm_attr = $meta->add_domain_model($dm_name, %$dm_opts);
486 my $dm_reader = $dm_attr->get_read_method;
488 unless( $class->can('inflate_result') ){
489 my $inflate_method = sub {
490 my $class = shift; my ($src) = @_;
491 $src = $src->resolve if $src->isa('DBIx::Class::ResultSourceHandle');
492 $class->new($dm_name, $src->result_class->inflate_result(@_));
494 $meta->add_method('inflate_result', $inflate_method);
497 #XXX this is here to allow action prototypes to work with ListView
498 # maybe Collections hsould have this kind of thing too to allow you to reconstruct them?
499 #i like the possibility to be honest... as aset of key/value pairs they could be URId
500 #XXX move to using 'handles' for this?
501 $meta->add_method('__id', sub {shift->$dm_reader->id} )
502 unless $class->can('__id');
503 #XXX this one is for Action, ChooseOne and ChooseMany need this shit
504 $meta->add_method('__ident_condition', sub {shift->$dm_reader->ident_condition} )
505 unless $class->can('__ident_condition');
507 #XXX this is just a disaster
508 $meta->add_method('display_name', sub {shift->$dm_reader->display_name} )
509 if( $source_class->can('display_name') && !$class->can('display_name'));
511 #XXX as a default pass the domain model as a target_model until i come up with something
512 #better through the coercion method
513 my $def_act_args = sub {
515 confess "no dm reader: $dm_reader on $_[0]" unless $_[0]->can($dm_reader);
516 return { (target_model => $_[0]->$dm_reader), %{ $super->(@_) } };
518 $meta->add_around_method_modifier('_default_action_args_for', $def_act_args);
521 # attributes => undef, #default to qr/./
522 # attributes => [], #default to nothing
523 # attributes => qr//, #DWIM, treated as [qr//]
524 # attributes => [{...}] #DWIM, treat as [qr/./, {...} ]
525 # attributes => [[-exclude => ...]] #DWIM, treat as [qr/./, [-exclude => ...]]
527 [ map { $_->name } $source_class->meta->compute_all_applicable_attributes ];
529 if(!defined $attr_rules){
530 $attr_rules = [qr/./];
531 } elsif( (!ref $attr_rules && $attr_rules) || (ref $attr_rules eq 'Regexp') ){
532 $attr_rules = [ $attr_rules ];
533 } elsif( ref $attr_rules eq 'ARRAY' && @$attr_rules){
534 #don't add a qr/./ rule if we have at least one match rule
535 push(@$attr_rules, qr/./) unless
536 grep {(ref $_ eq 'ARRAY' && $_->[0] ne '-exclude')
537 || !ref $_ || ref $_ eq 'Regexp'} @$attr_rules;
540 my $attributes = $self->parse_reflect_rules($attr_rules, $attr_haystack);
541 for my $attr_name (keys %$attributes){
542 $self->reflect_source_object_attribute(
544 source_class => $source_class,
545 parent_class => $parent,
546 attribute_name => $attr_name,
547 domain_model_name => $dm_name,
548 %{ $attributes->{$attr_name} || {}},
554 my $all_actions = $self->_all_object_actions;
555 my $action_haystack = [keys %$all_actions];
556 if(!defined $action_rules){
557 $action_rules = $self->default_object_actions;
558 } elsif( (!ref $action_rules && $action_rules) || (ref $action_rules eq 'Regexp') ){
559 $action_rules = [ $action_rules ];
560 } elsif( ref $action_rules eq 'ARRAY' && @$action_rules){
561 #don't add a qr/./ rule if we have at least one match rule
562 push(@$action_rules, qr/./)
563 unless grep {(ref $_ eq 'ARRAY' && $_->[0] ne '-exclude')
564 || !ref $_ || ref $_ eq 'Regexp'} @$action_rules;
567 # XXX this is kind of a dirty hack to support custom actions that are not
568 # previously defined and still be able to use the parse_reflect_rules mechanism
569 my @custom_actions = grep {!exists $all_actions->{$_}} map{ $_->[0] }
570 grep {ref $_ eq 'ARRAY' && $_->[0] ne '-exclude'} @$action_rules;
571 push(@$action_haystack, @custom_actions);
572 my $actions = $self->parse_reflect_rules($action_rules, $action_haystack);
573 for my $action (keys %$actions){
574 my $action_opts = $self->merge_hashes
575 ($all_actions->{$action} || {}, $actions->{$action} || {});
577 #NOTE: If the name of the action is not specified in the prototype then use it's
578 #hash key as the name. I think this is sane beahvior, but I've actually been thinking
579 #of making Action prototypes their own separate objects
580 $self->reflect_source_action(
582 object_class => $class,
583 source_class => $source_class,
587 # XXX i will move this to use the coercion method soon. this will be
588 # GoodEnough until then. I still need to think a little about the type coercion
589 # thing so i don't make a mess of it
590 my $act_args = sub { #override target model for this action
592 confess "no dm reader: $dm_reader on $_[0]" unless $_[0]->can($dm_reader);
593 return { %{ $super->(@_) },
594 ($_[1] eq $action ? (target_model => $_[0]->$dm_reader) : () ) };
596 $meta->add_around_method_modifier('_default_action_args_for', $act_args);
600 $meta->make_immutable if $make_immutable;
604 # needs class, attribute_name domain_model_name
605 implements reflect_source_object_attribute => as {
606 my ($self, %opts) = @_;
607 unless( $opts{attribute_name} && $opts{class} && $opts{parent_class}
608 && ( $opts{source_class} || $opts{domain_model_name} ) ){
609 confess( "Error: class, parent_class, attribute_name, and either " .
610 "domain_model_name or source_class are required parameters" );
613 my $meta = $opts{class}->meta;
614 my $attr_opts = $self->parameters_for_source_object_attribute(%opts);
616 my $make_immutable = $meta->is_immutable;
617 $meta->make_mutable if $meta->is_immutable;
619 my $attr = $meta->add_attribute($opts{attribute_name}, %$attr_opts);
621 $meta->make_immutable if $make_immutable;
625 # needs class, attribute_name domain_model_name
626 implements parameters_for_source_object_attribute => as {
627 my ($self, %opts) = @_;
629 my $class = delete $opts{class};
630 my $attr_name = delete $opts{attribute_name};
631 my $dm_name = delete $opts{domain_model_name};
632 my $source_class = delete $opts{source_class};
633 my $parent_class = delete $opts{parent_class};
634 confess("parent_class is a required argument") unless $parent_class;
635 confess("You must supply at least one of domain_model_name and source_class")
636 unless $dm_name || $source_class;
639 $source = $source_class->result_source_instance if $source_class;
641 if( !$source_class ){
642 my $dm = $class->meta->find_attribute_by_name($dm_name);
643 $source_class = $dm->_isa_metadata;
644 $source = $source_class->result_source_instance;
645 } elsif( !$dm_name ){
646 ($dm_name) = map{$_->name} grep{$_->_isa_metadata eq $source_class}
647 $class->meta->domain_models;
648 if( !$dm_name ){ #last resort guess
649 my $tentative = $self->dm_name_from_source_name($source->source_name);
650 ($dm_name) = $tentative if grep{$_->name eq $tentative} $class->domain_models;
654 my $from_attr = $source_class->meta->find_attribute_by_name($attr_name);
655 my $reader = $from_attr->get_read_method;
657 #default options. lazy build but no outsider method
658 my %attr_opts = ( is => 'ro', lazy => 1, required => 1,
659 clearer => "_clear_${attr_name}",
661 "has_${attr_name}" =>
662 sub { defined(shift->$dm_name->$reader) }
664 domain_model => $dm_name,
665 orig_attr_name => $attr_name,
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 $attr_opts{default} = eval "sub{ shift->${dm_name}->${reader} }";
742 implements reflect_source_action => as{
743 my($self, %opts) = @_;
744 my $name = delete $opts{name};
745 my $class = delete $opts{class};
746 my $base = delete $opts{base} || Action;
747 my $object = delete $opts{object_class};
748 my $source = delete $opts{source_class};
750 confess("name, object_class and source_class are required arguments")
751 unless $source && $name && $object;
753 my $attr_rules = delete $opts{attributes};
754 $class ||= $object->_default_action_class_for($name);
756 Class::MOP::load_class( $base );
757 Class::MOP::load_class( $object );
758 Class::MOP::load_class( $source );
760 #print STDERR "\n\t", ref $attr_rules eq 'ARRAY' ? @$attr_rules : $attr_rules,"\n";
761 # attributes => undef, #default to qr/./
762 # attributes => [], #default to nothing
763 # attributes => qr//, #DWIM, treated as [qr//]
764 # attributes => [{...}] #DWIM, treat as [qr/./, {...} ]
765 # attributes => [[-exclude => ...]] #DWIM, treat as [qr/./, [-exclude => ...]]
766 my $attr_haystack = [ map { $_->name } $object->meta->parameter_attributes ];
767 if(!defined $attr_rules){
768 $attr_rules = [qr/./];
769 } elsif( (!ref $attr_rules && $attr_rules) || (ref $attr_rules eq 'Regexp') ){
770 $attr_rules = [ $attr_rules ];
771 } elsif( ref $attr_rules eq 'ARRAY' && @$attr_rules){
772 #don't add a qr/./ rule if we have at least one match rule
773 push(@$attr_rules, qr/./) unless
774 grep {(ref $_ eq 'ARRAY' && $_->[0] ne '-exclude')
775 || !ref $_ || ref $_ eq 'Regexp'} @$attr_rules;
778 #print STDERR "${name}\t${class}\t${base}\n";
779 #print STDERR "\t${object}\t${source}\n";
780 #print STDERR "\t",@$attr_rules,"\n";
782 my $o_meta = $object->meta;
783 my $s_meta = $source->meta;
784 my $attributes = $self->parse_reflect_rules($attr_rules, $attr_haystack);
787 my $meta = $self->_load_or_create($class, $base);
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;
814 implements parameters_for_source_object_action_attribute => as {
815 my ($self, %opts) = @_;
817 my $object = delete $opts{object_class};
818 my $attr_name = delete $opts{attribute_name};
819 my $source_class = delete $opts{source_class};
820 confess("object_class and attribute_name are required parameters")
821 unless $attr_name && $object;
823 my $o_meta = $object->meta;
824 my $dm_name = $o_meta->find_attribute_by_name($attr_name)->domain_model;
825 $source_class ||= $o_meta->find_attribute_by_name($dm_name)->_isa_metadata;
826 my $from_attr = $source_class->meta->find_attribute_by_name($attr_name);
828 #print STDERR "$attr_name is type: " . $from_attr->meta->name . "\n";
830 confess("${attr_name} is not writeable and can not be reflected")
831 unless $from_attr->get_write_method;
835 isa => $from_attr->_isa_metadata,
836 required => $from_attr->is_required,
837 ($from_attr->is_required
838 ? () : (clearer => "clear_${attr_name}")),
839 predicate => "has_${attr_name}",
842 if ($attr_opts{required}) {
843 if($from_attr->has_default) {
844 $attr_opts{lazy} = 1;
845 $attr_opts{default} = $from_attr->default;
847 $attr_opts{lazy_fail} = 1;
853 if(my $coderef = $source_class->result_class->can('_m2m_metadata')){
854 $m2m_meta = $source_class->result_class->$coderef;
856 #test for relationships
857 my $constraint_is_ArrayRef =
858 $from_attr->type_constraint->name eq 'ArrayRef' ||
859 $from_attr->type_constraint->is_subtype_of('ArrayRef');
861 my $source = $source_class->result_source_instance;
862 if (my $rel_info = $source->relationship_info($attr_name)) {
863 my $rel_accessor = $rel_info->{attrs}->{accessor};
865 if($rel_accessor eq 'multi' && $constraint_is_ArrayRef) {
866 confess "${attr_name} is a rw has_many, this won't work.";
867 } elsif( $rel_accessor eq 'single' || $rel_accessor eq 'filter') {
868 $attr_opts{valid_values} = sub {
869 shift->target_model->result_source->related_source($attr_name)->resultset;
872 } elsif ( $constraint_is_ArrayRef && $attr_name =~ m/^(.*)_list$/) {
874 my $link_table = "links_to_${mm_name}_list";
875 $attr_opts{default} = sub { [] };
876 $attr_opts{valid_values} = sub {
877 shift->target_model->result_source->related_source($link_table)
878 ->related_source($mm_name)->resultset;
880 } elsif( $constraint_is_ArrayRef && defined $m2m_meta && exists $m2m_meta->{$attr_name} ){
881 #m2m if using introspectable m2m component
882 my $rel = $m2m_meta->{$attr_name}->{relation};
883 my $far_rel = $m2m_meta->{$attr_name}->{foreign_relation};
884 $attr_opts{default} = sub { [] };
885 $attr_opts{valid_values} = sub {
886 shift->target_model->result_source->related_source($rel)
887 ->related_source($far_rel)->resultset;
891 #print STDERR "\n" .$attr_name ." - ". $object . "\n";
892 #print STDERR Dumper(\%attr_opts);
896 implements _load_or_create => as {
897 my ($self, $class, $base) = @_;
898 my $meta = $self->_maybe_load_class($class) ?
899 $class->meta : $base->meta->create($class, superclasses => [ $base ]);
903 implements _maybe_load_class => as {
904 my ($self, $class) = @_;
905 my $file = $class . '.pm';
907 my $ret = eval { Class::MOP::load_class($class) };
908 if ($INC{$file} && $@) {
909 confess "Error loading ${class}: $@";
918 #--------#---------#---------#---------#---------#---------#---------#---------#
923 Reaction::InterfaceModel::Reflector::DBIC -
924 Automatically Generate InterfaceModels from DBIx::Class models
928 The InterfaceModel reflectors are classes that are meant to aid you in easily
929 generating Reaction::InterfaceModel classes that represent their underlying
930 DBIx::Class domain models by introspecting your L<DBIx::Class::ResultSource>s
931 and creating a collection of L<Reaction::InterfaceModel::Object> and
932 L<Reaction::InterfaceModel::Collection> classes for you to use.
934 The default base class of all Object classes will be
935 L<Reaction::InterfaceModel::Object> and the default Collection type will be
936 L<Reaction::InterfaceModel::Collection::Virtual::ResultSet>.
938 Additionally, the reflector can create InterfaceModel actions that interact
939 with the supplied L<Reaction::UI::Controller::Collection::CRUD>, allowing you
940 to easily set up a highly customizable CRUD interface in minimal time.
942 At this time, supported collection actions consist of:
946 =item B<> L<Reaction::INterfaceModel::Action::DBIC::ResultSet::Create>
948 Creates a new item in the collection and underlying ResultSet.
950 =item B<> L<Reaction::INterfaceModel::Action::DBIC::ResultSet::DeleteAll>
952 Deletes all the items in a collection and it's underlying resultset using
957 And supported object actions are :
961 =item B<Update> - via L<Reaction::INterfaceModel::Action::DBIC::Result::Update>
963 Updates an existing object.
965 =item B<Delete> - via L<Reaction::INterfaceModel::Action::DBIC::Result::Delete>
967 Deletes an existing object.
973 package MyApp::IM::TestModel;
974 use base 'Reaction::InterfaceModel::Object';
976 use Reaction::InterfaceModel::Reflector::DBIC;
977 my $reflector = Reaction::InterfaceModel::Reflector::DBIC->new;
980 $reflector->reflect_schema
982 model_class => __PACKAGE__,
983 schema_class => 'MyApp::Schema',
986 =head2 Selectively including and excluding sources
988 #reflect everything except for the FooBar and FooBaz classes
989 $reflector->reflect_schema
991 model_class => __PACKAGE__,
992 schema_class => 'MyApp::Schema',
993 sources => [-exclude => [qw/FooBar FooBaz/] ],
995 sources => [-exclude => qr/(?:FooBar|FooBaz)/,
997 sources => [-exclude => [qr/FooBar/, qr/FooBaz/],
1000 #reflect only the Foo family of sources
1001 $reflector->reflect_schema
1003 model_class => __PACKAGE__,
1004 schema_class => 'MyApp::Schema',
1005 sources => qr/^Foo/,
1008 =head2 Selectively including and excluding fields in sources
1010 #Reflect Foo and Baz in their entirety and exclude the field 'avatar' in the Bar ResultSource
1011 $reflector->reflect_schema
1013 model_class => __PACKAGE__,
1014 schema_class => 'MyApp::Schema',
1015 sources => [qw/Foo Baz/,
1016 [ Bar => {attributes => [[-exclude => 'avatar']] } ],
1017 # or exclude by regex
1018 [ Bar => {attributes => [-exclude => qr/avatar/] } ],
1019 # or simply do not include it...
1020 [ Bar => {attributes => [qw/id name description/] } ],
1026 =head2 make_classes_immutable
1028 =head2 object_actions
1030 =head2 collection_actions
1032 =head2 default_object_actions
1034 =head2 default_collection_actions
1036 =head2 builtin_object_actions
1038 =head2 builtin_collection_actions
1044 =head2 _all_object_actions
1046 =head2 _all_collection_actions
1048 =head2 dm_name_from_class_name
1050 =head2 dm_name_from_source_name
1052 =head2 class_name_from_source_name
1054 =head2 class_name_for_collection_of
1058 =head2 parse_reflect_rules
1060 =head2 merge_reflect_rules
1062 =head2 reflect_schema
1064 =head2 _compute_source_options
1068 =head2 reflect_source
1070 =head2 reflect_source_collection
1072 =head2 reflect_source_object
1074 =head2 reflect_source_object_attribute
1076 =head2 parameters_for_source_object_attribute
1078 =head2 reflect_source_action
1080 =head2 parameters_for_source_object_action_attribute
1084 Allow the reflector to dump the generated code out as files, eliminating the need to
1085 reflect on startup every time. This will likely take quite a bit of work though. The
1086 main work is already in place, but the grunt work is still left. At the moment there
1087 is no closures that can't be dumped out as code with a little bit of work.
1091 See L<Reaction::Class> for authors.
1095 See L<Reaction::Class> for the license.