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 = "_" . lc($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 = "_" . lc($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 = eval { Class::MOP::load_class($model); } ?
165 $model->meta : $base->meta->create($model, superclasses => [ $base ]);
167 # sources => undef, #default to qr/./
168 # sources => [], #default to nothing
169 # sources => qr//, #DWIM, treated as [qr//]
170 # sources => [{...}] #DWIM, treat as [qr/./, {...} ]
171 # sources => [[-exclude => ...]] #DWIM, treat as [qr/./, [-exclude => ...]]
172 my $haystack = [ $schema->sources ];
174 my $rules = delete $opts{sources};
177 } elsif( ref $rules eq 'Regexp'){
179 } elsif( ref $rules eq 'ARRAY' && @$rules){
180 #don't add a qr/./ rule if we have at least one match rule
181 push(@$rules, qr/./) unless grep {(ref $_ eq 'ARRAY' && $_->[0] ne '-exclude')
182 || !ref $_ || ref $_ eq 'Regexp'} @$rules;
185 my $sources = $self->parse_reflect_rules($rules, $haystack);
187 my $make_immutable = $meta->is_immutable || $self->make_classes_immutable;
188 $meta->make_mutable if $meta->is_immutable;
190 $meta->add_domain_model
191 ($dm_name, is => 'rw', isa => $schema, required => 1, %$dm_args);
193 for my $source_name (keys %$sources){
194 my $source_opts = $sources->{$source_name} || {};
195 $self->reflect_source(
196 source_name => $source_name,
197 parent_class => $model,
198 schema_class => $schema,
199 source_class => $schema->class($source_name),
200 parent_domain_model_name => $dm_name,
205 $meta->make_immutable if $make_immutable;
209 implements _compute_source_options => as {
210 my ($self, %opts) = @_;
211 my $schema = delete $opts{schema_class};
212 my $source_name = delete $opts{source_name};
213 my $source_class = delete $opts{source_class};
214 my $parent = delete $opts{parent_class};
215 my $parent_dm = delete $opts{parent_domain_model_name};
217 #this is the part where I hate my life for promissing all sorts of DWIMery
218 confess("parent_class and source_name or source_class are required parameters")
219 unless($parent && ($source_name || $source_class));
221 OUTER: until( $schema && $source_name && $source_class && $parent_dm ){
222 if( $schema && !$source_name){
223 next OUTER if $source_name = $source_class->result_source_instance->source_name;
224 } elsif( $schema && !$source_class){
225 next OUTER if $source_class = eval { $schema->class($source_name) };
228 if($source_class && (!$schema || !$source_name)){
230 $schema = $source_class->result_source_instance->schema;
231 next OUTER if $schema && Class::MOP::load_class($schema);
234 $source_name = $source_class->result_source_instance->source_name;
235 next OUTER if $source_name;
238 my @haystack = $parent_dm ?
239 $parent->meta->find_attribute_by_name($parent_dm) : $parent->domain_models;
241 #there's a lot of guessing going on, but it should work fine on most cases
242 INNER: for my $needle (@haystack){
243 my $isa = $needle->_isa_metadata;
244 next INNER unless Class::MOP::load_class( $isa->_isa_metadata );
245 next INNER unless $isa->isa('DBIx::Class::Schema');
246 if(!$parent_dm && $schema && $isa eq $schema){
247 $parent_dm = $needle->name;
252 my $src_class = eval{ $isa->class($source_name) };
253 next INNER unless $src_class;
254 next INNER if($source_class && $source_class ne $src_class);
256 $parent_dm = $needle->name;
257 $source_class = $src_class;
262 #do we even need to go this far?
263 if( !$parent_dm && $schema ){
264 my $tentative = $self->dm_name_from_class_name($schema);
265 $parent_dm = $tentative if grep{$_->name eq $tentative} @haystack;
268 confess("Could not determine options automatically from: schema " .
269 "'${schema}', source_name '${source_name}', source_class " .
270 "'${source_class}', parent_domain_model_name '${parent_dm}'");
274 source_name => $source_name,
275 schema_class => $schema,
276 source_class => $source_class,
277 parent_class => $parent,
278 parent_domain_model_name => $parent_dm,
282 implements _class_to_attribute_name => as {
283 my ( $self, $str ) = @_;
284 confess("wrong arguments passed for _class_to_attribute_name") unless $str;
285 return join('_', map lc, split(/::|(?<=[a-z0-9])(?=[A-Z])/, $str))
288 implements add_source => as {
289 my ($self, %opts) = @_;
291 my $model = delete $opts{model_class};
292 my $reader = delete $opts{reader};
293 my $source = delete $opts{source_name};
294 my $dm_name = delete $opts{domain_model_name};
295 my $collection = delete $opts{collection_class};
296 my $name = delete $opts{attribute_name} || $source;
298 confess("model_class and source_name are required parameters")
299 unless $model && $source;
300 my $meta = $model->meta;
302 unless( $collection ){
303 my $object = $self->class_name_from_source_name($model, $source);
304 $collection = $self->class_name_for_collection_of($object);
308 $reader =~ s/([a-z0-9])([A-Z])/${1}_${2}/g ;
309 $reader = $self->_class_to_attribute_name($reader) . "_collection";
312 my @haystack = $meta->domain_models;
314 @haystack = grep { $_->_isa_metadata->isa('DBIx::Class::Schema') } @haystack;
317 $dm_name = $haystack[0]->name;
318 } elsif(@haystack > 1){
319 confess("Failed to automatically determine domain_model_name. More than one " .
320 "possible match (".(join ", ", map{"'".$_->name."'"} @haystack).")");
322 confess("Failed to automatically determine domain_model_name. No matches.");
332 predicate => "has_" . $self->_class_to_attribute_name($name) ,
333 domain_model => $dm_name,
334 orig_attr_name => $source,
336 $collection->new(_source_resultset => shift->$dm_name->resultset($source));
340 my $make_immutable = $meta->is_immutable;
341 $meta->make_mutable if $make_immutable;
342 my $attr = $meta->add_attribute($name, %attr_opts);
343 $meta->make_immutable if $make_immutable;
348 implements reflect_source => as {
349 my ($self, %opts) = @_;
350 my $collection = delete $opts{collection} || {};
351 %opts = %{ $self->merge_hashes(\%opts, $self->_compute_source_options(%opts)) };
353 my $obj_meta = $self->reflect_source_object(%opts);
354 my $col_meta = $self->reflect_source_collection
356 object_class => $obj_meta->name,
357 source_class => $opts{source_class},
362 model_class => $opts{parent_class},
363 source_name => $opts{source_name},
364 domain_model_name => $opts{parent_domain_model_name},
365 collection_class => $col_meta->name,
369 implements reflect_source_collection => as {
370 my ($self, %opts) = @_;
371 my $base = delete $opts{base} || ResultSet;
372 my $class = delete $opts{class};
373 my $object = delete $opts{object_class};
374 my $source = delete $opts{source_class};
375 my $action_rules = delete $opts{actions};
377 confess('object_class and source_class are required parameters')
378 unless $object && $source;
379 $class ||= $self->class_name_for_collection_of($object);
381 Class::MOP::load_class( $base );
382 Class::MOP::load_class( $object );
383 my $meta = eval { Class::MOP::load_class($class) } ?
384 $class->meta : $base->meta->create( $class, superclasses => [ $base ]);
386 my $make_immutable = $meta->is_immutable || $self->make_classes_immutable;;
387 $meta->make_mutable if $meta->is_immutable;
388 $meta->add_method(_build_member_type => sub{ $object } );
389 #XXX as a default pass the domain model as a target_model until i come up with something
390 #better through the coercion method
391 my $def_act_args = sub {
393 return { (target_model => $_[0]->_source_resultset), %{ $super->(@_) } };
395 $meta->add_around_method_modifier('_default_action_args_for', $def_act_args);
399 my $all_actions = $self->_all_collection_actions;
400 my $action_haystack = [keys %$all_actions];
401 if(!defined $action_rules){
402 $action_rules = $self->default_collection_actions;
403 } elsif( (!ref $action_rules && $action_rules) || (ref $action_rules eq 'Regexp') ){
404 $action_rules = [ $action_rules ];
405 } elsif( ref $action_rules eq 'ARRAY' && @$action_rules){
406 #don't add a qr/./ rule if we have at least one match rule
407 push(@$action_rules, qr/./)
408 unless grep {(ref $_ eq 'ARRAY' && $_->[0] ne '-exclude')
409 || !ref $_ || ref $_ eq 'Regexp'} @$action_rules;
412 # XXX this is kind of a dirty hack to support custom actions that are not
413 # previously defined and still be able to use the parse_reflect_rules mechanism
414 my @custom_actions = grep {!exists $all_actions->{$_}}
415 map{ $_->[0] } grep {ref $_ eq 'ARRAY' && $_->[0] ne '-exclude'} @$action_rules;
416 push(@$action_haystack, @custom_actions);
417 my $actions = $self->parse_reflect_rules($action_rules, $action_haystack);
418 for my $action (keys %$actions){
419 my $action_opts = $self->merge_hashes
420 ($all_actions->{$action} || {}, $actions->{$action} || {});
422 #NOTE: If the name of the action is not specified in the prototype then use it's
423 #hash key as the name. I think this is sane beahvior, but I've actually been thinking
424 #of making Action prototypes their own separate objects
425 $self->reflect_source_action(
427 object_class => $object,
428 source_class => $source,
432 # XXX i will move this to use the coercion method soon. this will be
433 # GoodEnough until then. I still need to think a little about the type coercion
434 # thing so i don't make a mess of it
435 my $act_args = sub { #override target model for this action
437 return { %{ $super->(@_) },
438 ($_[1] eq $action ? (target_model => $_[0]->_source_resultset) : () ) };
440 $meta->add_around_method_modifier('_default_action_args_for', $act_args);
443 $meta->make_immutable if $make_immutable;
447 implements reflect_source_object => as {
448 my($self, %opts) = @_;
449 %opts = %{ $self->merge_hashes(\%opts, $self->_compute_source_options(%opts)) };
451 my $base = delete $opts{base} || Object;
452 my $class = delete $opts{class};
453 my $dm_name = delete $opts{domain_model_name};
454 my $dm_opts = delete $opts{domain_model_args} || {};
456 my $source_name = delete $opts{source_name};
457 my $schema = delete $opts{schema_class};
458 my $source_class = delete $opts{source_class};
459 my $parent = delete $opts{parent_class};
460 my $parent_dm = delete $opts{parent_domain_model_name};
462 my $action_rules = delete $opts{actions};
463 my $attr_rules = delete $opts{attributes};
465 $class ||= $self->class_name_from_source_name($parent, $source_name);
467 Class::MOP::load_class($parent);
468 Class::MOP::load_class($schema) if $schema;
469 Class::MOP::load_class($source_class);
471 my $meta = eval { Class::MOP::load_class($class) } ?
472 $class->meta : $base->meta->create($class, superclasses => [ $base ]);
474 #create the domain model
475 $dm_name ||= $self->dm_name_from_source_name($source_name);
477 $dm_opts->{isa} = $source_class;
478 $dm_opts->{is} ||= 'rw';
479 $dm_opts->{required} ||= 1;
481 my $make_immutable = $meta->is_immutable || $self->make_classes_immutable;;
482 $meta->make_mutable if $meta->is_immutable;
484 my $dm_attr = $meta->add_domain_model($dm_name, %$dm_opts);
485 my $dm_reader = $dm_attr->get_read_method;
487 unless( $class->can('inflate_result') ){
488 my $inflate_method = sub {
489 my $class = shift; my ($src) = @_;
490 $src = $src->resolve if $src->isa('DBIx::Class::ResultSourceHandle');
491 $class->new($dm_name, $src->result_class->inflate_result(@_));
493 $meta->add_method('inflate_result', $inflate_method);
496 #XXX this is here to allow action prototypes to work with ListView
497 # maybe Collections hsould have this kind of thing too to allow you to reconstruct them?
498 #i like the possibility to be honest... as aset of key/value pairs they could be URId
499 #XXX move to using 'handles' for this?
500 $meta->add_method('__id', sub {shift->$dm_reader->id} )
501 unless $class->can('__id');
502 #XXX this one is for Action, ChooseOne and ChooseMany need this shit
503 $meta->add_method('__ident_condition', sub {shift->$dm_reader->ident_condition} )
504 unless $class->can('__ident_condition');
506 #XXX this is just a disaster
507 $meta->add_method('display_name', sub {shift->$dm_reader->display_name} )
508 if( $source_class->can('display_name') && !$class->can('display_name'));
510 #XXX as a default pass the domain model as a target_model until i come up with something
511 #better through the coercion method
512 my $def_act_args = sub {
514 confess "no dm reader: $dm_reader on $_[0]" unless $_[0]->can($dm_reader);
515 return { (target_model => $_[0]->$dm_reader), %{ $super->(@_) } };
517 $meta->add_around_method_modifier('_default_action_args_for', $def_act_args);
520 # attributes => undef, #default to qr/./
521 # attributes => [], #default to nothing
522 # attributes => qr//, #DWIM, treated as [qr//]
523 # attributes => [{...}] #DWIM, treat as [qr/./, {...} ]
524 # attributes => [[-exclude => ...]] #DWIM, treat as [qr/./, [-exclude => ...]]
526 [ map { $_->name } $source_class->meta->compute_all_applicable_attributes ];
528 if(!defined $attr_rules){
529 $attr_rules = [qr/./];
530 } elsif( (!ref $attr_rules && $attr_rules) || (ref $attr_rules eq 'Regexp') ){
531 $attr_rules = [ $attr_rules ];
532 } elsif( ref $attr_rules eq 'ARRAY' && @$attr_rules){
533 #don't add a qr/./ rule if we have at least one match rule
534 push(@$attr_rules, qr/./) unless
535 grep {(ref $_ eq 'ARRAY' && $_->[0] ne '-exclude')
536 || !ref $_ || ref $_ eq 'Regexp'} @$attr_rules;
539 my $attributes = $self->parse_reflect_rules($attr_rules, $attr_haystack);
540 for my $attr_name (keys %$attributes){
541 $self->reflect_source_object_attribute(
543 source_class => $source_class,
544 parent_class => $parent,
545 attribute_name => $attr_name,
546 domain_model_name => $dm_name,
547 %{ $attributes->{$attr_name} || {}},
553 my $all_actions = $self->_all_object_actions;
554 my $action_haystack = [keys %$all_actions];
555 if(!defined $action_rules){
556 $action_rules = $self->default_object_actions;
557 } elsif( (!ref $action_rules && $action_rules) || (ref $action_rules eq 'Regexp') ){
558 $action_rules = [ $action_rules ];
559 } elsif( ref $action_rules eq 'ARRAY' && @$action_rules){
560 #don't add a qr/./ rule if we have at least one match rule
561 push(@$action_rules, qr/./)
562 unless grep {(ref $_ eq 'ARRAY' && $_->[0] ne '-exclude')
563 || !ref $_ || ref $_ eq 'Regexp'} @$action_rules;
566 # XXX this is kind of a dirty hack to support custom actions that are not
567 # previously defined and still be able to use the parse_reflect_rules mechanism
568 my @custom_actions = grep {!exists $all_actions->{$_}} map{ $_->[0] }
569 grep {ref $_ eq 'ARRAY' && $_->[0] ne '-exclude'} @$action_rules;
570 push(@$action_haystack, @custom_actions);
571 my $actions = $self->parse_reflect_rules($action_rules, $action_haystack);
572 for my $action (keys %$actions){
573 my $action_opts = $self->merge_hashes
574 ($all_actions->{$action} || {}, $actions->{$action} || {});
576 #NOTE: If the name of the action is not specified in the prototype then use it's
577 #hash key as the name. I think this is sane beahvior, but I've actually been thinking
578 #of making Action prototypes their own separate objects
579 $self->reflect_source_action(
581 object_class => $class,
582 source_class => $source_class,
586 # XXX i will move this to use the coercion method soon. this will be
587 # GoodEnough until then. I still need to think a little about the type coercion
588 # thing so i don't make a mess of it
589 my $act_args = sub { #override target model for this action
591 confess "no dm reader: $dm_reader on $_[0]" unless $_[0]->can($dm_reader);
592 return { %{ $super->(@_) },
593 ($_[1] eq $action ? (target_model => $_[0]->$dm_reader) : () ) };
595 $meta->add_around_method_modifier('_default_action_args_for', $act_args);
599 $meta->make_immutable if $make_immutable;
603 # needs class, attribute_name domain_model_name
604 implements reflect_source_object_attribute => as {
605 my ($self, %opts) = @_;
606 unless( $opts{attribute_name} && $opts{class} && $opts{parent_class}
607 && ( $opts{source_class} || $opts{domain_model_name} ) ){
608 confess( "Error: class, parent_class, attribute_name, and either " .
609 "domain_model_name or source_class are required parameters" );
612 my $meta = $opts{class}->meta;
613 my $attr_opts = $self->parameters_for_source_object_attribute(%opts);
615 my $make_immutable = $meta->is_immutable;
616 $meta->make_mutable if $meta->is_immutable;
618 my $attr = $meta->add_attribute($opts{attribute_name}, %$attr_opts);
620 $meta->make_immutable if $make_immutable;
624 # needs class, attribute_name domain_model_name
625 implements parameters_for_source_object_attribute => as {
626 my ($self, %opts) = @_;
628 my $class = delete $opts{class};
629 my $attr_name = delete $opts{attribute_name};
630 my $dm_name = delete $opts{domain_model_name};
631 my $source_class = delete $opts{source_class};
632 my $parent_class = delete $opts{parent_class};
633 confess("parent_class is a required argument") unless $parent_class;
634 confess("You must supply at least one of domain_model_name and source_class")
635 unless $dm_name || $source_class;
638 $source = $source_class->result_source_instance if $source_class;
640 if( !$source_class ){
641 my $dm = $class->meta->find_attribute_by_name($dm_name);
642 $source_class = $dm->_isa_metadata;
643 $source = $source_class->result_source_instance;
644 } elsif( !$dm_name ){
645 ($dm_name) = map{$_->name} grep{$_->_isa_metadata eq $source_class}
646 $class->meta->domain_models;
647 if( !$dm_name ){ #last resort guess
648 my $tentative = $self->dm_name_from_source_name($source->source_name);
649 ($dm_name) = $tentative if grep{$_->name eq $tentative} $class->domain_models;
653 my $from_attr = $source_class->meta->find_attribute_by_name($attr_name);
655 #default options. lazy build but no outsider method
656 my %attr_opts = ( is => 'ro', lazy => 1, required => 1,
657 clearer => "_clear_${attr_name}",
658 predicate => "has_${attr_name}",
659 domain_model => $dm_name,
660 orig_attr_name => $attr_name,
664 my $constraint_is_ArrayRef =
665 $from_attr->type_constraint->name eq 'ArrayRef' ||
666 $from_attr->type_constraint->is_subtype_of('ArrayRef');
668 if( my $rel_info = $source->relationship_info($attr_name) ){
669 my $rel_accessor = $rel_info->{attrs}->{accessor};
670 my $rel_moniker = $rel_info->{class}->result_source_instance->source_name;
672 if($rel_accessor eq 'multi' && $constraint_is_ArrayRef) {
674 my $sm = $self->class_name_from_source_name($parent_class, $rel_moniker);
675 #type constraint is a collection, and default builds it
676 $attr_opts{isa} = $self->class_name_for_collection_of($sm);
677 $attr_opts{default} = sub {
678 my $rs = shift->$dm_name->related_resultset($attr_name);
679 return $attr_opts{isa}->new(_source_resultset => $rs);
681 } elsif( $rel_accessor eq 'single') {
683 #type constraint is the foreign IM object, default inflates it
684 $attr_opts{isa} = $self->class_name_from_source_name($parent_class, $rel_moniker);
685 $attr_opts{default} = sub {
687 ->find_related($attr_name, {},{result_class => $attr_opts{isa}});
690 } elsif( $constraint_is_ArrayRef && $attr_name =~ m/^(.*)_list$/ ) {
693 my $link_table = "links_to_${mm_name}_list";
694 my ($hm_source, $far_side);
695 eval { $hm_source = $source->related_source($link_table); }
696 || confess "Can't find ${link_table} has_many for ${mm_name}_list";
697 eval { $far_side = $hm_source->related_source($mm_name); }
698 || confess "Can't find ${mm_name} belongs_to on ".$hm_source->result_class
699 ." traversing many-many for ${mm_name}_list";
701 my $sm = $self->class_name_from_source_name($parent_class,$far_side->source_name);
702 $attr_opts{isa} = $self->class_name_for_collection_of($sm);
704 #proper collections will remove the result_class uglyness.
705 $attr_opts{default} = sub {
706 my $rs = shift->$dm_name->related_resultset($link_table)->related_resultset($mm_name);
707 return $attr_opts{isa}->new(_source_resultset => $rs);
711 my $reader = $from_attr->get_read_method;
712 $attr_opts{isa} = $from_attr->_isa_metadata;
713 $attr_opts{default} = sub{ shift->$dm_name->$reader };
719 implements reflect_source_action => as{
720 my($self, %opts) = @_;
721 my $name = delete $opts{name};
722 my $class = delete $opts{class};
723 my $base = delete $opts{base} || Action;
724 my $object = delete $opts{object_class};
725 my $source = delete $opts{source_class};
727 confess("name, object_class and source_class are required arguments")
728 unless $source && $name && $object;
730 my $attr_rules = delete $opts{attributes};
731 $class ||= $object->_default_action_class_for($name);
733 Class::MOP::load_class( $base );
734 Class::MOP::load_class( $object );
735 Class::MOP::load_class( $source );
737 #print STDERR "\n\t", ref $attr_rules eq 'ARRAY' ? @$attr_rules : $attr_rules,"\n";
738 # attributes => undef, #default to qr/./
739 # attributes => [], #default to nothing
740 # attributes => qr//, #DWIM, treated as [qr//]
741 # attributes => [{...}] #DWIM, treat as [qr/./, {...} ]
742 # attributes => [[-exclude => ...]] #DWIM, treat as [qr/./, [-exclude => ...]]
743 my $attr_haystack = [ map { $_->name } $object->meta->parameter_attributes ];
744 if(!defined $attr_rules){
745 $attr_rules = [qr/./];
746 } elsif( (!ref $attr_rules && $attr_rules) || (ref $attr_rules eq 'Regexp') ){
747 $attr_rules = [ $attr_rules ];
748 } elsif( ref $attr_rules eq 'ARRAY' && @$attr_rules){
749 #don't add a qr/./ rule if we have at least one match rule
750 push(@$attr_rules, qr/./) unless
751 grep {(ref $_ eq 'ARRAY' && $_->[0] ne '-exclude')
752 || !ref $_ || ref $_ eq 'Regexp'} @$attr_rules;
755 #print STDERR "${name}\t${class}\t${base}\n";
756 #print STDERR "\t${object}\t${source}\n";
757 #print STDERR "\t",@$attr_rules,"\n";
759 my $o_meta = $object->meta;
760 my $s_meta = $source->meta;
761 my $attributes = $self->parse_reflect_rules($attr_rules, $attr_haystack);
764 my $meta = eval { Class::MOP::load_class($class) } ?
765 $class->meta : $base->meta->create($class, superclasses => [$base]);
766 my $make_immutable = $meta->is_immutable || $self->make_classes_immutable;
767 $meta->make_mutable if $meta->is_immutable;
769 for my $attr_name (keys %$attributes){
770 my $attr_opts = $attributes->{$attr_name} || {};
771 my $o_attr = $o_meta->find_attribute_by_name($attr_name);
772 my $s_attr_name = $o_attr->orig_attr_name || $attr_name;
773 my $s_attr = $s_meta->find_attribute_by_name($s_attr_name);
774 confess("Unable to find attribute for '${s_attr_name}' via '${source}'")
775 unless defined $s_attr;
776 next unless $s_attr->get_write_method; #only rw attributes!
778 my $attr_params = $self->parameters_for_source_object_action_attribute
780 object_class => $object,
781 source_class => $source,
782 attribute_name => $attr_name
784 $meta->add_attribute( $attr_name => %$attr_params);
787 $meta->make_immutable if $make_immutable;
791 implements parameters_for_source_object_action_attribute => as {
792 my ($self, %opts) = @_;
794 my $object = delete $opts{object_class};
795 my $attr_name = delete $opts{attribute_name};
796 my $source_class = delete $opts{source_class};
797 confess("object_class and attribute_name are required parameters")
798 unless $attr_name && $object;
800 my $o_meta = $object->meta;
801 my $dm_name = $o_meta->find_attribute_by_name($attr_name)->domain_model;
802 $source_class ||= $o_meta->find_attribute_by_name($dm_name)->_isa_metadata;
803 my $from_attr = $source_class->meta->find_attribute_by_name($attr_name);
805 confess("${attr_name} is not writeable and can not be reflected")
806 unless $from_attr->get_write_method;
810 isa => $from_attr->_isa_metadata,
811 required => $from_attr->is_required,
812 predicate => "has_${attr_name}",
815 if ($attr_opts{required}) {
816 if($from_attr->has_default) {
817 $attr_opts{lazy} = 1;
818 $attr_opts{default} = $from_attr->default;
820 $attr_opts{lazy_fail} = 1;
824 #test for relationships
825 my $constraint_is_ArrayRef =
826 $from_attr->type_constraint->name eq 'ArrayRef' ||
827 $from_attr->type_constraint->is_subtype_of('ArrayRef');
829 my $source = $source_class->result_source_instance;
830 if (my $rel_info = $source->relationship_info($attr_name)) {
831 my $rel_accessor = $rel_info->{attrs}->{accessor};
833 if($rel_accessor eq 'multi' && $constraint_is_ArrayRef) {
834 confess "${attr_name} is a rw has_many, this won't work.";
835 } elsif( $rel_accessor eq 'single') {
836 $attr_opts{valid_values} = sub {
837 shift->target_model->result_source->related_source($attr_name)->resultset;
840 } elsif ( $constraint_is_ArrayRef && $attr_name =~ m/^(.*)_list$/) {
842 my $link_table = "links_to_${mm_name}_list";
843 my ($hm_source, $far_side);
844 eval { $hm_source = $source->related_source($link_table); }
845 || confess "Can't find ${link_table} has_many for ${mm_name}_list";
846 eval { $far_side = $hm_source->related_source($mm_name); }
847 || confess "Can't find ${mm_name} belongs_to on ".$hm_source->result_class
848 ." traversing many-many for ${mm_name}_list";
850 $attr_opts{default} = sub { [] };
851 $attr_opts{valid_values} = sub {
852 shift->target_model->result_source->related_source($link_table)
853 ->related_source($mm_name)->resultset;
857 #print STDERR "\n" .$attr_name ." - ". $object . "\n";
858 #print STDERR Dumper(\%attr_opts);
866 #--------#---------#---------#---------#---------#---------#---------#---------#
871 Reaction::InterfaceModel::Reflector::DBIC -
872 Automatically Generate InterfaceModels from DBIx::Class models
876 The InterfaceModel reflectors are classes that are meant to aid you in easily
877 generating Reaction::InterfaceModel classes that represent their underlying
878 DBIx::Class domain models by introspecting your L<DBIx::Class::ResultSource>s
879 and creating a collection of L<Reaction::InterfaceModel::Object> and
880 L<Reaction::InterfaceModel::Collection> classes for you to use.
882 The default base class of all Object classes will be
883 L<Reaction::InterfaceModel::Object> and the default Collection type will be
884 L<Reaction::InterfaceModel::Collection::Virtual::ResultSet>.
886 Additionally, the reflector can create InterfaceModel actions that interact
887 with the supplied L<Reaction::UI::Controller::Collection::CRUD>, allowing you
888 to easily set up a highly customizable CRUD interface in minimal time.
890 At this time, supported collection actions consist of:
894 =item B<> L<Reaction::INterfaceModel::Action::DBIC::ResultSet::Create>
896 Creates a new item in the collection and underlying ResultSet.
898 =item B<> L<Reaction::INterfaceModel::Action::DBIC::ResultSet::DeleteAll>
900 Deletes all the items in a collection and it's underlying resultset using
905 And supported object actions are :
909 =item B<Update> - via L<Reaction::INterfaceModel::Action::DBIC::Result::Update>
911 Updates an existing object.
913 =item B<Delete> - via L<Reaction::INterfaceModel::Action::DBIC::Result::Delete>
915 Deletes an existing object.
921 package MyApp::IM::TestModel;
922 use base 'Reaction::InterfaceModel::Object';
924 use Reaction::InterfaceModel::Reflector::DBIC;
925 my $reflector = Reaction::InterfaceModel::Reflector::DBIC->new;
928 $reflector->reflect_schema
930 model_class => __PACKAGE__,
931 schema_class => 'MyApp::Schema',
934 =head2 Selectively including and excluding sources
936 #reflect everything except for the FooBar and FooBaz classes
937 $reflector->reflect_schema
939 model_class => __PACKAGE__,
940 schema_class => 'MyApp::Schema',
941 sources => [-exclude => [qw/FooBar FooBaz/] ],
943 sources => [-exclude => qr/(?:FooBar|FooBaz)/,
945 sources => [-exclude => [qr/FooBar/, qr/FooBaz/],
948 #reflect only the Foo family of sources
949 $reflector->reflect_schema
951 model_class => __PACKAGE__,
952 schema_class => 'MyApp::Schema',
956 =head2 Selectively including and excluding fields in sources
958 #Reflect Foo and Baz in their entirety and exclude the field 'avatar' in the Bar ResultSource
959 $reflector->reflect_schema
961 model_class => __PACKAGE__,
962 schema_class => 'MyApp::Schema',
963 sources => [qw/Foo Baz/,
964 [ Bar => {attributes => [[-exclude => 'avatar']] } ],
965 # or exclude by regex
966 [ Bar => {attributes => [-exclude => qr/avatar/] } ],
967 # or simply do not include it...
968 [ Bar => {attributes => [qw/id name description/] } ],
974 =head2 make_classes_immutable
976 =head2 object_actions
978 =head2 collection_actions
980 =head2 default_object_actions
982 =head2 default_collection_actions
984 =head2 builtin_object_actions
986 =head2 builtin_collection_actions
992 =head2 _all_object_actions
994 =head2 _all_collection_actions
996 =head2 dm_name_from_class_name
998 =head2 dm_name_from_source_name
1000 =head2 class_name_from_source_name
1002 =head2 class_name_for_collection_of
1006 =head2 parse_reflect_rules
1008 =head2 merge_reflect_rules
1010 =head2 reflect_schema
1012 =head2 _compute_source_options
1016 =head2 reflect_source
1018 =head2 reflect_source_collection
1020 =head2 reflect_source_object
1022 =head2 reflect_source_object_attribute
1024 =head2 parameters_for_source_object_attribute
1026 =head2 reflect_source_action
1028 =head2 parameters_for_source_object_action_attribute
1032 Allow the reflector to dump the generated code out as files, eliminating the need to
1033 reflect on startup every time. This will likely take quite a bit of work though. The
1034 main work is already in place, but the grunt work is still left. At the moment there
1035 is no closures that can't be dumped out as code with a little bit of work.
1039 See L<Reaction::Class> for authors.
1043 See L<Reaction::Class> for the license.