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) ];
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);
31 sub _build_object_actions { {} };
32 sub _build_collection_actions { {} };
33 sub _build_default_object_actions { [ qw/Update Delete/ ] };
34 sub _build_default_collection_actions { [ qw/Create DeleteAll/ ] };
35 sub _build_builtin_object_actions {
37 Update => { name => 'Update', base => Update },
38 Delete => { name => 'Delete', base => Delete, attributes => [] },
41 sub _build_builtin_collection_actions {
43 Create => {name => 'Create', base => Create },
44 DeleteAll => {name => 'DeleteAll', base => DeleteAll, attributes => [] }
47 sub _all_object_actions {
49 return $self->merge_hashes
50 ($self->builtin_object_actions, $self->object_actions);
52 sub _all_collection_actions {
54 return $self->merge_hashes
55 ($self->builtin_collection_actions, $self->collection_actions);
57 sub dm_name_from_class_name {
58 my($self, $class) = @_;
59 confess("wrong arguments") unless $class;
61 $class = "_" . $self->_class_to_attribute_name($class) . "_store";
64 sub dm_name_from_source_name {
65 my($self, $source) = @_;
66 confess("wrong arguments") unless $source;
67 $source =~ s/([a-z0-9])([A-Z])/${1}_${2}/g ;
68 $source = "_" . $self->_class_to_attribute_name($source) . "_store";
71 sub class_name_from_source_name {
72 my ($self, $model_class, $source_name) = @_;
73 confess("wrong arguments") unless $model_class && $source_name;
74 return join "::", $model_class, $source_name;
76 sub class_name_for_collection_of {
77 my ($self, $object_class) = @_;
78 confess("wrong arguments") unless $object_class;
79 return "${object_class}::Collection";
82 my($self, $left, $right) = @_;
83 return Catalyst::Utils::merge_hashes($left, $right);
85 sub parse_reflect_rules {
86 my ($self, $rules, $haystack) = @_;
87 confess('$rules must be an array reference') unless ref $rules eq 'ARRAY';
88 confess('$haystack must be an array reference') unless ref $haystack eq 'ARRAY';
91 my (@exclude, @include, $global_opts);
92 if(@$rules == 2 && $rules->[0] eq '-exclude'){
93 push(@exclude, (ref $rules->[1] eq 'ARRAY' ? @{$rules->[1]} : $rules->[1]));
95 for my $rule ( @$rules ){
96 if (ref $rule eq 'ARRAY' && $rule->[0] eq '-exclude'){
97 push(@exclude, (ref $rule->[1] eq 'ARRAY' ? @{$rule->[1]} : $rule->[1]));
98 } elsif( ref $rule eq 'HASH' ){
99 $global_opts = ref $global_opts eq 'HASH' ?
100 $self->merge_hashes($global_opts, $rule) : $rule;
102 push(@include, $rule);
106 my $check_exclude = sub{
107 for my $rule (@exclude){
108 return 1 if(ref $rule eq 'Regexp' ? $_[0] =~ /$rule/ : $_[0] eq $rule);
113 @$haystack = grep { !$check_exclude->($_) } @$haystack;
114 $self->merge_reflect_rules(\@include, $needles, $haystack, $global_opts);
117 sub merge_reflect_rules {
118 my ($self, $rules, $needles, $haystack, $local_opts) = @_;
119 for my $rule ( @$rules ){
120 if(!ref $rule && ( grep {$rule eq $_} @$haystack ) ){
121 $needles->{$rule} = defined $needles->{$rule} ?
122 $self->merge_hashes($needles->{$rule}, $local_opts) : $local_opts;
123 } elsif( ref $rule eq 'Regexp' ){
124 for my $match ( grep { /$rule/ } @$haystack ){
125 $needles->{$match} = defined $needles->{$match} ?
126 $self->merge_hashes($needles->{$match}, $local_opts) : $local_opts;
128 } elsif( ref $rule eq 'ARRAY' ){
130 $opts = pop(@$rule) if @$rule > 1 and ref $rule->[$#$rule] eq 'HASH';
131 $opts = $self->merge_hashes($local_opts, $opts) if defined $local_opts;
132 $self->merge_reflect_rules($rule, $needles, $haystack, $opts);
137 my ($self, %opts) = @_;
138 my $base = delete $opts{base} || Object;
139 my $roles = delete $opts{roles} || [];
140 my $model = delete $opts{model_class};
141 my $schema = delete $opts{schema_class};
142 my $dm_name = delete $opts{domain_model_name};
143 my $dm_args = delete $opts{domain_model_args} || {};
144 $dm_name ||= $self->dm_name_from_class_name($schema);
146 #load all necessary classes
147 confess("model_class and schema_class are required parameters")
148 unless($model && $schema);
149 Class::MOP::load_class( $base );
150 Class::MOP::load_class( $schema );
151 my $meta = $self->_load_or_create(
153 superclasses => [$base],
154 ( @$roles ? (roles => $roles) : ()),
157 # sources => undef, #default to qr/./
158 # sources => [], #default to nothing
159 # sources => qr//, #DWIM, treated as [qr//]
160 # sources => [{...}] #DWIM, treat as [qr/./, {...} ]
161 # sources => [[-exclude => ...]] #DWIM, treat as [qr/./, [-exclude => ...]]
162 my $haystack = [ $schema->sources ];
164 my $rules = delete $opts{sources};
167 } elsif( ref $rules eq 'Regexp'){
169 } elsif( ref $rules eq 'ARRAY' && @$rules){
170 #don't add a qr/./ rule if we have at least one match rule
171 push(@$rules, qr/./) unless grep {(ref $_ eq 'ARRAY' && $_->[0] ne '-exclude')
172 || !ref $_ || ref $_ eq 'Regexp'} @$rules;
175 my $sources = $self->parse_reflect_rules($rules, $haystack);
177 my $make_immutable = $meta->is_immutable || $self->make_classes_immutable;
178 $meta->make_mutable if $meta->is_immutable;
180 $meta->add_domain_model
181 ($dm_name, is => 'rw', isa => $schema, required => 1, %$dm_args);
183 for my $source_name (keys %$sources){
184 my $source_opts = $sources->{$source_name} || {};
185 $self->reflect_source(
186 source_name => $source_name,
187 parent_class => $model,
188 schema_class => $schema,
189 source_class => $schema->class($source_name),
190 parent_domain_model_name => $dm_name,
195 $meta->make_immutable if $make_immutable;
198 sub _compute_source_options {
199 my ($self, %opts) = @_;
200 my $schema = delete $opts{schema_class};
201 my $source_name = delete $opts{source_name};
202 my $source_class = delete $opts{source_class};
203 my $parent = delete $opts{parent_class};
204 my $parent_dm = delete $opts{parent_domain_model_name};
206 #this is the part where I hate my life for promissing all sorts of DWIMery
207 confess("parent_class and source_name or source_class are required parameters")
208 unless($parent && ($source_name || $source_class));
210 OUTER: until( $schema && $source_name && $source_class && $parent_dm ){
211 if( $schema && !$source_name){
212 next OUTER if $source_name = $schema->source($source_class)->source_name;
213 } elsif( $schema && !$source_class){
214 next OUTER if $source_class = eval { $schema->class($source_name) };
217 my @haystack = $parent_dm ? $parent->meta->find_attribute_by_name($parent_dm) : ();
219 #there's a lot of guessing going on, but it should work fine on most cases
220 INNER: for my $needle (@haystack){
221 my $isa = $needle->_isa_metadata;
222 next INNER unless Class::MOP::load_class( $isa->_isa_metadata );
223 next INNER unless $isa->isa('DBIx::Class::Schema');
226 my $src_class = eval{ $isa->class($source_name) };
227 next INNER unless $src_class;
228 next INNER if($source_class && $source_class ne $src_class);
230 $parent_dm = $needle->name;
231 $source_class = $src_class;
237 confess("Could not determine options automatically from: schema " .
238 "'${schema}', source_name '${source_name}', source_class " .
239 "'${source_class}', parent_domain_model_name '${parent_dm}'");
243 source_name => $source_name,
244 schema_class => $schema,
245 source_class => $source_class,
246 parent_class => $parent,
247 parent_domain_model_name => $parent_dm,
250 sub _class_to_attribute_name {
251 my ( $self, $str ) = @_;
252 confess("wrong arguments passed for _class_to_attribute_name") unless $str;
253 return join('_', map lc, split(/::|(?<=[a-z0-9])(?=[A-Z])/, $str))
256 my ($self, %opts) = @_;
258 my $model = delete $opts{model_class};
259 my $reader = delete $opts{reader};
260 my $source = delete $opts{source_name};
261 my $dm_name = delete $opts{domain_model_name};
262 my $collection = delete $opts{collection_class};
263 my $name = delete $opts{attribute_name} || $source;
265 confess("model_class and source_name are required parameters")
266 unless $model && $source;
267 my $meta = $model->meta;
269 unless( $collection ){
270 my $object = $self->class_name_from_source_name($model, $source);
271 $collection = $self->class_name_for_collection_of($object);
275 $reader =~ s/([a-z0-9])([A-Z])/${1}_${2}/g ;
276 $reader = $self->_class_to_attribute_name($reader) . "_collection";
279 my @haystack = $meta->domain_models;
281 @haystack = grep { $_->_isa_metadata->isa('DBIx::Class::Schema') } @haystack;
284 $dm_name = $haystack[0]->name;
285 } elsif(@haystack > 1){
286 confess("Failed to automatically determine domain_model_name. More than one " .
287 "possible match (".(join ", ", map{"'".$_->name."'"} @haystack).")");
289 confess("Failed to automatically determine domain_model_name. No matches.");
299 predicate => "has_" . $self->_class_to_attribute_name($name) ,
300 domain_model => $dm_name,
301 orig_attr_name => $source,
305 _source_resultset => $_[0]->$dm_name->resultset($source),
311 my $make_immutable = $meta->is_immutable;
312 $meta->make_mutable if $make_immutable;
313 my $attr = $meta->add_attribute($name, %attr_opts);
314 $meta->make_immutable if $make_immutable;
319 my ($self, %opts) = @_;
320 my $collection = delete $opts{collection} || {};
321 %opts = %{ $self->merge_hashes(\%opts, $self->_compute_source_options(%opts)) };
323 my $obj_meta = $self->reflect_source_object(%opts);
324 my $col_meta = $self->reflect_source_collection
326 object_class => $obj_meta->name,
327 source_class => $opts{source_class},
328 schema => $opts{schema_class},
334 model_class => delete $opts{parent_class},
335 domain_model_name => delete $opts{parent_domain_model_name},
336 collection_class => $col_meta->name,
339 sub reflect_source_collection {
340 my ($self, %opts) = @_;
341 my $base = delete $opts{base} || ResultSet;
342 my $roles = delete $opts{roles} || [];
343 my $class = delete $opts{class};
344 my $object = delete $opts{object_class};
345 my $source = delete $opts{source_class};
346 my $action_rules = delete $opts{actions};
347 my $schema = delete $opts{schema};
349 confess('object_class and source_class are required parameters')
350 unless $object && $source;
351 $class ||= $self->class_name_for_collection_of($object);
353 Class::MOP::load_class( $base );
354 Class::MOP::load_class( $object );
356 my $meta = $self->_load_or_create(
358 superclasses => [$base],
359 ( @$roles ? (roles => $roles) : ()),
362 my $make_immutable = $meta->is_immutable || $self->make_classes_immutable;;
363 $meta->make_mutable if $meta->is_immutable;
364 $meta->add_method(_build_member_type => sub{ $object } );
365 #XXX as a default pass the domain model as a target_model until i come up with something
366 #better through the coercion method
367 my $def_act_args = sub {
369 return { (target_model => $_[0]->_source_resultset), %{ $super->(@_) } };
371 $meta->add_around_method_modifier('_default_action_args_for', $def_act_args);
375 my $all_actions = $self->_all_collection_actions;
376 my $action_haystack = [keys %$all_actions];
377 if(!defined $action_rules){
378 $action_rules = $self->default_collection_actions;
379 } elsif( (!ref $action_rules && $action_rules) || (ref $action_rules eq 'Regexp') ){
380 $action_rules = [ $action_rules ];
381 } elsif( ref $action_rules eq 'ARRAY' && @$action_rules){
382 #don't add a qr/./ rule if we have at least one match rule
383 push(@$action_rules, qr/./)
384 unless grep {(ref $_ eq 'ARRAY' && $_->[0] ne '-exclude')
385 || !ref $_ || ref $_ eq 'Regexp'} @$action_rules;
388 # XXX this is kind of a dirty hack to support custom actions that are not
389 # previously defined and still be able to use the parse_reflect_rules mechanism
390 my @custom_actions = grep {!exists $all_actions->{$_}}
391 map{ $_->[0] } grep {ref $_ eq 'ARRAY' && $_->[0] ne '-exclude'} @$action_rules;
392 push(@$action_haystack, @custom_actions);
393 my $actions = $self->parse_reflect_rules($action_rules, $action_haystack);
394 for my $action (keys %$actions){
395 my $action_opts = $self->merge_hashes
396 ($all_actions->{$action} || {}, $actions->{$action} || {});
398 #NOTE: If the name of the action is not specified in the prototype then use it's
399 #hash key as the name. I think this is sane beahvior, but I've actually been thinking
400 #of making Action prototypes their own separate objects
401 $self->reflect_source_action(
404 object_class => $object,
405 source_class => $source,
409 # XXX i will move this to use the coercion method soon. this will be
410 # GoodEnough until then. I still need to think a little about the type coercion
411 # thing so i don't make a mess of it
412 my $act_args = sub { #override target model for this action
414 return { %{ $super->(@_) },
415 ($_[1] eq $action ? (target_model => $_[0]->_source_resultset) : () ) };
417 $meta->add_around_method_modifier('_default_action_args_for', $act_args);
420 $meta->make_immutable if $make_immutable;
423 sub reflect_source_object {
424 my($self, %opts) = @_;
425 %opts = %{ $self->merge_hashes(\%opts, $self->_compute_source_options(%opts)) };
427 my $base = delete $opts{base} || Object;
428 my $roles = delete $opts{roles} || [];
429 my $class = delete $opts{class};
430 my $dm_name = delete $opts{domain_model_name};
431 my $dm_opts = delete $opts{domain_model_args} || {};
433 my $source_name = delete $opts{source_name};
434 my $schema = delete $opts{schema_class};
435 my $source_class = delete $opts{source_class};
436 my $parent = delete $opts{parent_class};
437 my $parent_dm = delete $opts{parent_domain_model_name};
439 my $action_rules = delete $opts{actions};
440 my $attr_rules = delete $opts{attributes};
442 $class ||= $self->class_name_from_source_name($parent, $source_name);
444 Class::MOP::load_class($parent);
445 Class::MOP::load_class($schema) if $schema;
446 Class::MOP::load_class($source_class);
448 my $meta = $self->_load_or_create(
450 superclasses => [$base],
451 ( @$roles ? (roles => $roles) : ()),
454 #create the domain model
455 $dm_name ||= $self->dm_name_from_source_name($source_name);
457 $dm_opts->{isa} = $source_class;
458 $dm_opts->{is} ||= 'rw';
459 $dm_opts->{required} ||= 1;
461 my $make_immutable = $meta->is_immutable || $self->make_classes_immutable;;
462 $meta->make_mutable if $meta->is_immutable;
464 my $dm_attr = $meta->add_domain_model($dm_name, %$dm_opts);
465 my $dm_reader = $dm_attr->get_read_method;
467 unless( $class->can('inflate_result') ){
468 my $inflate_method = sub {
469 my $class = shift; my ($src) = @_;
470 $src = $src->resolve if $src->isa('DBIx::Class::ResultSourceHandle');
471 $class->new($dm_name, $src->result_class->inflate_result(@_));
473 $meta->add_method('inflate_result', $inflate_method);
476 #XXX this is here to allow action prototypes to work with ListView
477 # maybe Collections hsould have this kind of thing too to allow you to reconstruct them?
478 #i like the possibility to be honest... as aset of key/value pairs they could be URId
479 #XXX move to using 'handles' for this?
480 $meta->add_method('__id', sub {shift->$dm_reader->id} )
481 unless $class->can('__id');
482 #XXX this one is for Action, ChooseOne and ChooseMany need this shit
483 $meta->add_method('__ident_condition', sub {shift->$dm_reader->ident_condition} )
484 unless $class->can('__ident_condition');
486 #XXX this is just a disaster
487 $meta->add_method('display_name', sub {shift->$dm_reader->display_name} )
488 if( $source_class->can('display_name') && !$class->can('display_name'));
490 #XXX as a default pass the domain model as a target_model until i come up with something
491 #better through the coercion method
492 my $def_act_args = sub {
494 confess "no dm reader: $dm_reader on $_[0]" unless $_[0]->can($dm_reader);
495 return { (target_model => $_[0]->$dm_reader), %{ $super->(@_) } };
497 $meta->add_around_method_modifier('_default_action_args_for', $def_act_args);
500 # attributes => undef, #default to qr/./
501 # attributes => [], #default to nothing
502 # attributes => qr//, #DWIM, treated as [qr//]
503 # attributes => [{...}] #DWIM, treat as [qr/./, {...} ]
504 # attributes => [[-exclude => ...]] #DWIM, treat as [qr/./, [-exclude => ...]]
506 [ map { $_->name } $source_class->meta->get_all_attributes ];
508 if(!defined $attr_rules){
509 $attr_rules = [qr/./];
510 } elsif( (!ref $attr_rules && $attr_rules) || (ref $attr_rules eq 'Regexp') ){
511 $attr_rules = [ $attr_rules ];
512 } elsif( ref $attr_rules eq 'ARRAY' && @$attr_rules){
513 #don't add a qr/./ rule if we have at least one match rule
514 push(@$attr_rules, qr/./) unless
515 grep {(ref $_ eq 'ARRAY' && $_->[0] ne '-exclude')
516 || !ref $_ || ref $_ eq 'Regexp'} @$attr_rules;
519 my $attributes = $self->parse_reflect_rules($attr_rules, $attr_haystack);
520 for my $attr_name (keys %$attributes){
521 $self->reflect_source_object_attribute(
524 source_class => $source_class,
525 parent_class => $parent,
526 attribute_name => $attr_name,
527 domain_model_name => $dm_name,
528 %{ $attributes->{$attr_name} || {}},
534 my $all_actions = $self->_all_object_actions;
535 my $action_haystack = [keys %$all_actions];
536 if(!defined $action_rules){
537 $action_rules = $self->default_object_actions;
538 } elsif( (!ref $action_rules && $action_rules) || (ref $action_rules eq 'Regexp') ){
539 $action_rules = [ $action_rules ];
540 } elsif( ref $action_rules eq 'ARRAY' && @$action_rules){
541 #don't add a qr/./ rule if we have at least one match rule
542 push(@$action_rules, qr/./)
543 unless grep {(ref $_ eq 'ARRAY' && $_->[0] ne '-exclude')
544 || !ref $_ || ref $_ eq 'Regexp'} @$action_rules;
547 # XXX this is kind of a dirty hack to support custom actions that are not
548 # previously defined and still be able to use the parse_reflect_rules mechanism
549 my @custom_actions = grep {!exists $all_actions->{$_}} map{ $_->[0] }
550 grep {ref $_ eq 'ARRAY' && $_->[0] ne '-exclude'} @$action_rules;
551 push(@$action_haystack, @custom_actions);
552 my $actions = $self->parse_reflect_rules($action_rules, $action_haystack);
553 for my $action (keys %$actions){
554 my $action_opts = $self->merge_hashes
555 ($all_actions->{$action} || {}, $actions->{$action} || {});
557 #NOTE: If the name of the action is not specified in the prototype then use it's
558 #hash key as the name. I think this is sane beahvior, but I've actually been thinking
559 #of making Action prototypes their own separate objects
560 $self->reflect_source_action(
563 object_class => $class,
564 source_class => $source_class,
568 # XXX i will move this to use the coercion method soon. this will be
569 # GoodEnough until then. I still need to think a little about the type coercion
570 # thing so i don't make a mess of it
571 my $act_args = sub { #override target model for this action
573 confess "no dm reader: $dm_reader on $_[0]" unless $_[0]->can($dm_reader);
574 return { %{ $super->(@_) },
575 ($_[1] eq $action ? (target_model => $_[0]->$dm_reader) : () ) };
577 $meta->add_around_method_modifier('_default_action_args_for', $act_args);
581 $meta->make_immutable if $make_immutable;
585 # needs class, attribute_name domain_model_name
586 sub reflect_source_object_attribute {
587 my ($self, %opts) = @_;
588 unless( $opts{attribute_name} && $opts{class} && $opts{parent_class}
589 && ( $opts{source_class} || $opts{domain_model_name} ) ){
590 confess( "Error: class, parent_class, attribute_name, and either " .
591 "domain_model_name or source_class are required parameters" );
594 my $meta = $opts{class}->meta;
595 my $attr_opts = $self->parameters_for_source_object_attribute(%opts);
597 my $make_immutable = $meta->is_immutable;
598 $meta->make_mutable if $meta->is_immutable;
600 my $attr = $meta->add_attribute($opts{attribute_name}, %$attr_opts);
602 $meta->make_immutable if $make_immutable;
606 # needs class, attribute_name domain_model_name
607 sub parameters_for_source_object_attribute {
608 my ($self, %opts) = @_;
610 my $class = delete $opts{class};
611 my $attr_name = delete $opts{attribute_name};
612 my $dm_name = delete $opts{domain_model_name};
613 my $source_class = delete $opts{source_class};
614 my $parent_class = delete $opts{parent_class};
615 my $schema = $opts{schema};
616 confess("parent_class is a required argument") unless $parent_class;
617 confess("You must supply at least one of domain_model_name and source_class")
618 unless $dm_name || $source_class;
620 my $source = $schema->source($source_class);
621 my $from_attr = $source_class->meta->find_attribute_by_name($attr_name);
622 my $reader = $from_attr->get_read_method;
623 die("Could not find reader for attribute '$attr_name' on $source_class")
626 #default options. lazy build but no outsider method
627 my %attr_opts = ( is => 'ro', lazy => 1, required => 1,
628 clearer => "_clear_${attr_name}",
630 "has_${attr_name}" =>
631 sub { defined(shift->$dm_name->$reader) }
633 domain_model => $dm_name,
634 orig_attr_name => $attr_name,
636 $attr_opts{coerce} = 1 if $from_attr->should_coerce;
640 if(my $coderef = $source->result_class->can('_m2m_metadata')){
641 $m2m_meta = $source->result_class->$coderef;
644 my $constraint_is_ArrayRef =
645 $from_attr->type_constraint->name eq 'ArrayRef' ||
646 $from_attr->type_constraint->is_subtype_of('ArrayRef');
648 if( my $rel_info = $source->relationship_info($attr_name) ){
649 my $rel_accessor = $rel_info->{attrs}->{accessor};
650 my $rel_moniker = $schema->source($rel_info->{class})->source_name;
652 if($rel_accessor eq 'multi' && $constraint_is_ArrayRef) {
654 my $sm = $self->class_name_from_source_name($parent_class, $rel_moniker);
655 #type constraint is a collection, and default builds it
656 my $isa = $attr_opts{isa} = $self->class_name_for_collection_of($sm);
657 $attr_opts{default} = eval "sub {
658 my \$rs = shift->${dm_name}->related_resultset('${attr_name}');
659 return ${isa}->new(_source_resultset => \$rs);
661 } elsif( $rel_accessor eq 'single' || $rel_accessor eq 'filter' ) {
663 #type constraint is the foreign IM object, default inflates it
664 my $isa = $attr_opts{isa} = $self->class_name_from_source_name($parent_class, $rel_moniker);
665 $attr_opts{default} = eval "sub {
666 if (defined(my \$o = shift->${dm_name}->${reader})) {
667 return ${isa}->inflate_result(\$o->result_source, { \$o->get_columns });
672 } elsif( $constraint_is_ArrayRef && $attr_name =~ m/^(.*)_list$/ ) {
675 my $link_table = "links_to_${mm_name}_list";
676 my ($hm_source, $far_side);
677 eval { $hm_source = $source->related_source($link_table); }
678 || confess "Can't find ${link_table} has_many for ${mm_name}_list";
679 eval { $far_side = $hm_source->related_source($mm_name); }
680 || confess "Can't find ${mm_name} belongs_to on ".$hm_source->result_class
681 ." traversing many-many for ${mm_name}_list";
683 my $sm = $self->class_name_from_source_name($parent_class,$far_side->source_name);
684 my $isa = $attr_opts{isa} = $self->class_name_for_collection_of($sm);
686 #proper collections will remove the result_class uglyness.
687 $attr_opts{default} = eval "sub {
688 my \$rs = shift->${dm_name}->related_resultset('${link_table}')->related_resultset('${mm_name}');
689 return ${isa}->new(_source_resultset => \$rs);
691 } elsif( $constraint_is_ArrayRef && defined $m2m_meta && exists $m2m_meta->{$attr_name} ){
692 #m2m if using introspectable m2m component
693 my $rel = $m2m_meta->{$attr_name}->{relation};
694 my $far_rel = $m2m_meta->{$attr_name}->{foreign_relation};
695 my $far_source = $source->related_source($rel)->related_source($far_rel);
696 my $sm = $self->class_name_from_source_name($parent_class, $far_source->source_name);
697 my $isa = $attr_opts{isa} = $self->class_name_for_collection_of($sm);
699 my $rs_meth = $m2m_meta->{$attr_name}->{rs_method};
700 $attr_opts{default} = eval "sub {
701 return ${isa}->new(_source_resultset => shift->${dm_name}->${rs_meth});
705 $attr_opts{isa} = $from_attr->_isa_metadata;
706 my $default_code = "sub{ shift->${dm_name}->${reader} }";
707 $attr_opts{default} = eval $default_code;
708 die "Could not generate default for attribute, code '$default_code' did not compile with: $@" if $@;
712 sub reflect_source_action {
713 my($self, %opts) = @_;
714 my $name = delete $opts{name};
715 my $base = delete $opts{base} || Action;
716 my $roles = delete $opts{roles} || [];
717 my $class = delete $opts{class};
718 my $object = delete $opts{object_class};
719 my $source = delete $opts{source_class};
720 my $schema = delete $opts{schema};
722 confess("name, object_class and source_class are required arguments")
723 unless $source && $name && $object;
725 my $attr_rules = delete $opts{attributes};
726 $class ||= $object->_default_action_class_for($name);
728 Class::MOP::load_class( $base );
729 Class::MOP::load_class( $object );
730 Class::MOP::load_class( $source );
732 #print STDERR "\n\t", ref $attr_rules eq 'ARRAY' ? @$attr_rules : $attr_rules,"\n";
733 # attributes => undef, #default to qr/./
734 # attributes => [], #default to nothing
735 # attributes => qr//, #DWIM, treated as [qr//]
736 # attributes => [{...}] #DWIM, treat as [qr/./, {...} ]
737 # attributes => [[-exclude => ...]] #DWIM, treat as [qr/./, [-exclude => ...]]
738 my $attr_haystack = [ map { $_->name } $object->parameter_attributes ];
739 if(!defined $attr_rules){
740 $attr_rules = [qr/./];
741 } elsif( (!ref $attr_rules && $attr_rules) || (ref $attr_rules eq 'Regexp') ){
742 $attr_rules = [ $attr_rules ];
743 } elsif( ref $attr_rules eq 'ARRAY' && @$attr_rules){
744 #don't add a qr/./ rule if we have at least one match rule
745 push(@$attr_rules, qr/./) unless
746 grep {(ref $_ eq 'ARRAY' && $_->[0] ne '-exclude')
747 || !ref $_ || ref $_ eq 'Regexp'} @$attr_rules;
750 #print STDERR "${name}\t${class}\t${base}\n";
751 #print STDERR "\t${object}\t${source}\n";
752 #print STDERR "\t",@$attr_rules,"\n";
754 my $o_meta = $object->meta;
755 my $s_meta = $source->meta;
756 my $attributes = $self->parse_reflect_rules($attr_rules, $attr_haystack);
759 my $meta = $self->_load_or_create(
761 superclasses => [$base],
762 ( @$roles ? (roles => $roles) : ()),
764 my $make_immutable = $meta->is_immutable || $self->make_classes_immutable;
765 $meta->make_mutable if $meta->is_immutable;
767 for my $attr_name (keys %$attributes){
768 my $attr_opts = $attributes->{$attr_name} || {};
769 my $o_attr = $o_meta->find_attribute_by_name($attr_name);
770 my $s_attr_name = $o_attr->orig_attr_name || $attr_name;
771 my $s_attr = $s_meta->find_attribute_by_name($s_attr_name);
772 confess("Unable to find attribute for '${s_attr_name}' via '${source}'")
773 unless defined $s_attr;
774 next unless $s_attr->get_write_method
775 && $s_attr->get_write_method !~ /^_/; #only rw attributes!
777 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;
790 sub parameters_for_source_object_action_attribute {
791 my ($self, %opts) = @_;
793 my $object = delete $opts{object_class};
794 my $attr_name = delete $opts{attribute_name};
795 my $source_class = delete $opts{source_class};
796 my $schema = delete $opts{schema};
797 my $source = $schema->source($source_class);
798 confess("object_class and attribute_name are required parameters")
799 unless $attr_name && $object;
801 my $o_meta = $object->meta;
802 my $dm_name = $o_meta->find_attribute_by_name($attr_name)->domain_model;
803 $source_class ||= $o_meta->find_attribute_by_name($dm_name)->_isa_metadata;
804 my $from_attr = $source_class->meta->find_attribute_by_name($attr_name);
806 #print STDERR "$attr_name is type: " . $from_attr->meta->name . "\n";
808 confess("${attr_name} is not writeable and can not be reflected")
809 unless $from_attr->get_write_method;
813 isa => $from_attr->_isa_metadata,
814 required => $from_attr->is_required,
815 ($from_attr->is_required
816 ? () : (clearer => "clear_${attr_name}")),
817 predicate => "has_${attr_name}",
820 if ($attr_opts{required}) {
821 if($from_attr->has_default) {
822 $attr_opts{lazy} = 1;
823 $attr_opts{default} = $from_attr->default;
825 $attr_opts{lazy_fail} = 1;
831 if(my $coderef = $source_class->result_class->can('_m2m_metadata')){
832 $m2m_meta = $source_class->result_class->$coderef;
834 #test for relationships
835 my $constraint_is_ArrayRef =
836 $from_attr->type_constraint->name eq 'ArrayRef' ||
837 $from_attr->type_constraint->is_subtype_of('ArrayRef');
839 if (my $rel_info = $source->relationship_info($attr_name)) {
840 my $rel_accessor = $rel_info->{attrs}->{accessor};
842 if($rel_accessor eq 'multi' && $constraint_is_ArrayRef) {
843 confess "${attr_name} is a rw has_many, this won't work.";
844 } elsif( $rel_accessor eq 'single' || $rel_accessor eq 'filter') {
845 $attr_opts{valid_values} = sub {
846 shift->target_model->result_source->related_source($attr_name)->resultset;
849 } elsif ( $constraint_is_ArrayRef && $attr_name =~ m/^(.*)_list$/) {
851 my $link_table = "links_to_${mm_name}_list";
852 $attr_opts{default} = sub { [] };
853 $attr_opts{valid_values} = sub {
854 shift->target_model->result_source->related_source($link_table)
855 ->related_source($mm_name)->resultset;
857 } elsif( $constraint_is_ArrayRef && defined $m2m_meta && exists $m2m_meta->{$attr_name} ){
858 #m2m if using introspectable m2m component
859 my $rel = $m2m_meta->{$attr_name}->{relation};
860 my $far_rel = $m2m_meta->{$attr_name}->{foreign_relation};
861 $attr_opts{default} = sub { [] };
862 $attr_opts{valid_values} = sub {
863 shift->target_model->result_source->related_source($rel)
864 ->related_source($far_rel)->resultset;
868 #print STDERR "\n" .$attr_name ." - ". $object . "\n";
869 #print STDERR Dumper(\%attr_opts);
873 sub _load_or_create {
874 my ($self, $class, %options) = @_;
876 if( $self->_maybe_load_class($class) ){
880 if( exists $options{superclasses} ){
881 ($base) = @{ $options{superclasses} };
883 $base = 'Reaction::InterfaceModel::Action';
885 return $base->meta->create($class, %options);
888 sub _maybe_load_class {
889 my ($self, $class) = @_;
890 my $file = $class . '.pm';
892 my $ret = eval { Class::MOP::load_class($class) };
893 if ($INC{$file} && $@) {
894 confess "Error loading ${class}: $@";
899 __PACKAGE__->meta->make_immutable;
904 #--------#---------#---------#---------#---------#---------#---------#---------#
909 Reaction::InterfaceModel::Reflector::DBIC -
910 Automatically Generate InterfaceModels from DBIx::Class models
914 The InterfaceModel reflectors are classes that are meant to aid you in easily
915 generating Reaction::InterfaceModel classes that represent their underlying
916 DBIx::Class domain models by introspecting your L<DBIx::Class::ResultSource>s
917 and creating a collection of L<Reaction::InterfaceModel::Object> and
918 L<Reaction::InterfaceModel::Collection> classes for you to use.
920 The default base class of all Object classes will be
921 L<Reaction::InterfaceModel::Object> and the default Collection type will be
922 L<Reaction::InterfaceModel::Collection::Virtual::ResultSet>.
924 Additionally, the reflector can create InterfaceModel actions that interact
925 with the supplied L<Reaction::UI::Controller::Collection::CRUD>, allowing you
926 to easily set up a highly customizable CRUD interface in minimal time.
928 At this time, supported collection actions consist of:
932 =item B<> L<Reaction::INterfaceModel::Action::DBIC::ResultSet::Create>
934 Creates a new item in the collection and underlying ResultSet.
936 =item B<> L<Reaction::INterfaceModel::Action::DBIC::ResultSet::DeleteAll>
938 Deletes all the items in a collection and it's underlying resultset using
943 And supported object actions are :
947 =item B<Update> - via L<Reaction::INterfaceModel::Action::DBIC::Result::Update>
949 Updates an existing object.
951 =item B<Delete> - via L<Reaction::INterfaceModel::Action::DBIC::Result::Delete>
953 Deletes an existing object.
959 package MyApp::IM::TestModel;
960 use base 'Reaction::InterfaceModel::Object';
962 use Reaction::InterfaceModel::Reflector::DBIC;
963 my $reflector = Reaction::InterfaceModel::Reflector::DBIC->new;
966 $reflector->reflect_schema
968 model_class => __PACKAGE__,
969 schema_class => 'MyApp::Schema',
972 =head2 Selectively including and excluding sources
974 #reflect everything except for the FooBar and FooBaz classes
975 $reflector->reflect_schema
977 model_class => __PACKAGE__,
978 schema_class => 'MyApp::Schema',
979 sources => [-exclude => [qw/FooBar FooBaz/] ],
981 sources => [-exclude => qr/(?:FooBar|FooBaz)/,
983 sources => [-exclude => [qr/FooBar/, qr/FooBaz/],
986 #reflect only the Foo family of sources
987 $reflector->reflect_schema
989 model_class => __PACKAGE__,
990 schema_class => 'MyApp::Schema',
994 =head2 Selectively including and excluding fields in sources
996 #Reflect Foo and Baz in their entirety and exclude the field 'avatar' in the Bar ResultSource
997 $reflector->reflect_schema
999 model_class => __PACKAGE__,
1000 schema_class => 'MyApp::Schema',
1001 sources => [qw/Foo Baz/,
1002 [ Bar => {attributes => [[-exclude => 'avatar']] } ],
1003 # or exclude by regex
1004 [ Bar => {attributes => [-exclude => qr/avatar/] } ],
1005 # or simply do not include it...
1006 [ Bar => {attributes => [qw/id name description/] } ],
1012 =head2 make_classes_immutable
1014 =head2 object_actions
1016 =head2 collection_actions
1018 =head2 default_object_actions
1020 =head2 default_collection_actions
1022 =head2 builtin_object_actions
1024 =head2 builtin_collection_actions
1030 =head2 _all_object_actions
1032 =head2 _all_collection_actions
1034 =head2 dm_name_from_class_name
1036 =head2 dm_name_from_source_name
1038 =head2 class_name_from_source_name
1040 =head2 class_name_for_collection_of
1044 =head2 parse_reflect_rules
1046 =head2 merge_reflect_rules
1048 =head2 reflect_schema
1050 =head2 _compute_source_options
1054 =head2 reflect_source
1056 =head2 reflect_source_collection
1058 =head2 reflect_source_object
1060 =head2 reflect_source_object_attribute
1062 =head2 parameters_for_source_object_attribute
1064 =head2 reflect_source_action
1066 =head2 parameters_for_source_object_action_attribute
1070 Allow the reflector to dump the generated code out as files, eliminating the need to
1071 reflect on startup every time. This will likely take quite a bit of work though. The
1072 main work is already in place, but the grunt work is still left. At the moment there
1073 is no closures that can't be dumped out as code with a little bit of work.
1077 See L<Reaction::Class> for authors.
1081 See L<Reaction::Class> for the license.