avoid passwords going into <input> elements
[catagits/Reaction.git] / lib / Reaction / InterfaceModel / Reflector / DBIC.pm
CommitLineData
7adfd53f 1package Reaction::InterfaceModel::Reflector::DBIC;
2
3use aliased 'Reaction::InterfaceModel::Action::DBIC::ResultSet::Create';
b8faba69 4use aliased 'Reaction::InterfaceModel::Action::DBIC::ResultSet::DeleteAll';
7adfd53f 5use aliased 'Reaction::InterfaceModel::Action::DBIC::Result::Update';
6use aliased 'Reaction::InterfaceModel::Action::DBIC::Result::Delete';
7
8use aliased 'Reaction::InterfaceModel::Collection::Virtual::ResultSet';
9use aliased 'Reaction::InterfaceModel::Object';
10use aliased 'Reaction::InterfaceModel::Action';
11use Reaction::Class;
12use Class::MOP;
13
f670cfd0 14use Catalyst::Utils;
15
7adfd53f 16class DBIC, which {
17
de48f4e6 18 has make_classes_immutable => (isa => "Bool", is => "rw", required => 1, default => sub{ 1 });
19
f670cfd0 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);
23
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);
27
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
89939ff9 32 implements _build_object_actions => as { {} };
33 implements _build_collection_actions => as { {} };
f670cfd0 34
89939ff9 35 implements _build_default_object_actions => as { [ qw/Update Delete/ ] };
36 implements _build_default_collection_actions => as { [ qw/Create DeleteAll/ ] };
f670cfd0 37
89939ff9 38 implements _build_builtin_object_actions => as {
f670cfd0 39 {
7b78a39d 40 Update => { name => 'Update', base => Update },
41 Delete => { name => 'Delete', base => Delete, attributes => [] },
f670cfd0 42 };
43 };
44
89939ff9 45 implements _build_builtin_collection_actions => as {
a4f82080 46 {
47 Create => {name => 'Create', base => Create },
c689b58e 48 DeleteAll => {name => 'DeleteAll', base => DeleteAll, attributes => [] }
a4f82080 49 };
f670cfd0 50 };
51
52 implements _all_object_actions => as {
89939ff9 53 my $self = shift;
f670cfd0 54 return $self->merge_hashes
55 ($self->builtin_object_actions, $self->object_actions);
56 };
7adfd53f 57
f670cfd0 58 implements _all_collection_actions => as {
59 my $self = shift;
60 return $self->merge_hashes
61 ($self->builtin_collection_actions, $self->collection_actions);
62 };
63
64 implements dm_name_from_class_name => as {
65 my($self, $class) = @_;
66 confess("wrong arguments") unless $class;
67 $class =~ s/::/_/g;
68 $class = "_" . lc($class) . "_store";
69 return $class;
7adfd53f 70 };
71
f670cfd0 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";
77 return $source;
7adfd53f 78 };
79
f670cfd0 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;
84 };
85
86 implements class_name_for_collection_of => as {
7adfd53f 87 my ($self, $object_class) = @_;
f670cfd0 88 confess("wrong arguments") unless $object_class;
7adfd53f 89 return "${object_class}::Collection";
90 };
91
f670cfd0 92 implements merge_hashes => as {
93 my($self, $left, $right) = @_;
94 return Catalyst::Utils::merge_hashes($left, $right);
95 };
96
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';
101
102 my $needles = {};
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]));
106 } else {
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;
113 } else {
114 push(@include, $rule);
115 }
116 }
117 }
118 my $check_exclude = sub{
119 for my $rule (@exclude){
120 return 1 if(ref $rule eq 'Regexp' ? $_[0] =~ /$rule/ : $_[0] eq $rule);
121 }
122 return;
123 };
124
125 @$haystack = grep { !$check_exclude->($_) } @$haystack;
126 $self->merge_reflect_rules(\@include, $needles, $haystack, $global_opts);
127 return $needles;
128 };
129
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;
140 }
141 } elsif( ref $rule eq 'ARRAY' ){
142 my $opts;
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);
146 }
147 }
148 };
149
150 implements reflect_schema => as {
7adfd53f 151 my ($self, %opts) = @_;
f670cfd0 152 my $base = delete $opts{base} || Object;
153 my $model = delete $opts{model_class};
154 my $schema = delete $opts{schema_class};
7adfd53f 155 my $dm_name = delete $opts{domain_model_name};
156 my $dm_args = delete $opts{domain_model_args} || {};
f670cfd0 157 $dm_name ||= $self->dm_name_from_class_name($schema);
158
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 );
c11c77ee 164 my $meta = $self->_load_or_create($model, $base);
f670cfd0 165
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 ];
172
173 my $rules = delete $opts{sources};
174 if(!defined $rules){
175 $rules = [qr/./];
176 } elsif( ref $rules eq 'Regexp'){
177 $rules = [ $rules ];
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;
182 }
7adfd53f 183
f670cfd0 184 my $sources = $self->parse_reflect_rules($rules, $haystack);
7adfd53f 185
de48f4e6 186 my $make_immutable = $meta->is_immutable || $self->make_classes_immutable;
7adfd53f 187 $meta->make_mutable if $meta->is_immutable;
188
f670cfd0 189 $meta->add_domain_model
190 ($dm_name, is => 'rw', isa => $schema, required => 1, %$dm_args);
191
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,
200 %$source_opts
201 );
7adfd53f 202 }
203
204 $meta->make_immutable if $make_immutable;
205 return $meta;
206 };
207
f670cfd0 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};
215
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));
219
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) };
225 }
7adfd53f 226
f670cfd0 227 if($source_class && (!$schema || !$source_name)){
228 if(!$schema){
229 $schema = $source_class->result_source_instance->schema;
230 next OUTER if $schema && Class::MOP::load_class($schema);
231 }
232 if(!$source_name){
233 $source_name = $source_class->result_source_instance->source_name;
234 next OUTER if $source_name;
235 }
236 }
237 my @haystack = $parent_dm ?
238 $parent->meta->find_attribute_by_name($parent_dm) : $parent->domain_models;
239
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;
247 next OUTER;
248 }
249
250 if( $source_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);
254 $schema = $isa;
255 $parent_dm = $needle->name;
256 $source_class = $src_class;
257 next OUTER;
258 }
259 }
260
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;
265 }
266
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}'");
270 }
271
272 return {
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,
278 };
279 };
7adfd53f 280
46937531 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))
285 };
7adfd53f 286
f670cfd0 287 implements add_source => as {
288 my ($self, %opts) = @_;
289
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;
296
297 confess("model_class and source_name are required parameters")
298 unless $model && $source;
299 my $meta = $model->meta;
300
301 unless( $collection ){
302 my $object = $self->class_name_from_source_name($model, $source);
303 $collection = $self->class_name_for_collection_of($object);
304 }
305 unless( $reader ){
306 $reader = $source;
7adfd53f 307 $reader =~ s/([a-z0-9])([A-Z])/${1}_${2}/g ;
46937531 308 $reader = $self->_class_to_attribute_name($reader) . "_collection";
7adfd53f 309 }
f670cfd0 310 unless( $dm_name ){
311 my @haystack = $meta->domain_models;
312 if( @haystack > 1 ){
313 @haystack = grep { $_->_isa_metadata->isa('DBIx::Class::Schema') } @haystack;
314 }
315 if(@haystack == 1){
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).")");
320 } else {
321 confess("Failed to automatically determine domain_model_name. No matches.");
322 }
323 }
7adfd53f 324
325 my %attr_opts =
326 (
327 lazy => 1,
7adfd53f 328 required => 1,
f670cfd0 329 isa => $collection,
7adfd53f 330 reader => $reader,
46937531 331 predicate => "has_" . $self->_class_to_attribute_name($name) ,
7adfd53f 332 domain_model => $dm_name,
f670cfd0 333 orig_attr_name => $source,
7adfd53f 334 default => sub {
a945c8a0 335 my $self = $_[0];
336 return $collection->new(
337 _source_resultset => $self->$dm_name->resultset($source),
338 _parent => $self,
339 );
7adfd53f 340 },
341 );
7adfd53f 342
f670cfd0 343 my $make_immutable = $meta->is_immutable;
344 $meta->make_mutable if $make_immutable;
345 my $attr = $meta->add_attribute($name, %attr_opts);
7adfd53f 346 $meta->make_immutable if $make_immutable;
f670cfd0 347
7adfd53f 348 return $attr;
349 };
350
f670cfd0 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)) };
355
356 my $obj_meta = $self->reflect_source_object(%opts);
357 my $col_meta = $self->reflect_source_collection
358 (
359 object_class => $obj_meta->name,
360 source_class => $opts{source_class},
361 %$collection
362 );
363
364 $self->add_source(
365 model_class => $opts{parent_class},
366 source_name => $opts{source_name},
367 domain_model_name => $opts{parent_domain_model_name},
368 collection_class => $col_meta->name,
369 );
370 };
371
372 implements reflect_source_collection => as {
7adfd53f 373 my ($self, %opts) = @_;
7adfd53f 374 my $base = delete $opts{base} || ResultSet;
f670cfd0 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};
379
380 confess('object_class and source_class are required parameters')
381 unless $object && $source;
382 $class ||= $self->class_name_for_collection_of($object);
7adfd53f 383
f670cfd0 384 Class::MOP::load_class( $base );
385 Class::MOP::load_class( $object );
c11c77ee 386 my $meta = $self->_load_or_create($class, $base);
7adfd53f 387
de48f4e6 388 my $make_immutable = $meta->is_immutable || $self->make_classes_immutable;;
f670cfd0 389 $meta->make_mutable if $meta->is_immutable;
c8fbb8ad 390 $meta->add_method(_build_member_type => sub{ $object } );
f670cfd0 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 {
394 my $super = shift;
395 return { (target_model => $_[0]->_source_resultset), %{ $super->(@_) } };
396 };
397 $meta->add_around_method_modifier('_default_action_args_for', $def_act_args);
398
399
400 {
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;
7adfd53f 412 }
7adfd53f 413
f670cfd0 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} || {});
7b78a39d 423
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
f670cfd0 427 $self->reflect_source_action(
428 name => $action,
429 object_class => $object,
430 source_class => $source,
431 %$action_opts,
432 );
433
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
438 my $super = shift;
439 return { %{ $super->(@_) },
440 ($_[1] eq $action ? (target_model => $_[0]->_source_resultset) : () ) };
441 };
442 $meta->add_around_method_modifier('_default_action_args_for', $act_args);
443 }
444 }
7adfd53f 445 $meta->make_immutable if $make_immutable;
446 return $meta;
447 };
448
f670cfd0 449 implements reflect_source_object => as {
450 my($self, %opts) = @_;
451 %opts = %{ $self->merge_hashes(\%opts, $self->_compute_source_options(%opts)) };
452
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} || {};
457
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};
463
464 my $action_rules = delete $opts{actions};
465 my $attr_rules = delete $opts{attributes};
466
467 $class ||= $self->class_name_from_source_name($parent, $source_name);
468
469 Class::MOP::load_class($parent);
470 Class::MOP::load_class($schema) if $schema;
471 Class::MOP::load_class($source_class);
472
c11c77ee 473 my $meta = $self->_load_or_create($class, $base);
7adfd53f 474
475 #create the domain model
f670cfd0 476 $dm_name ||= $self->dm_name_from_source_name($source_name);
7adfd53f 477
f670cfd0 478 $dm_opts->{isa} = $source_class;
7adfd53f 479 $dm_opts->{is} ||= 'rw';
480 $dm_opts->{required} ||= 1;
7adfd53f 481
de48f4e6 482 my $make_immutable = $meta->is_immutable || $self->make_classes_immutable;;
f670cfd0 483 $meta->make_mutable if $meta->is_immutable;
484
485 my $dm_attr = $meta->add_domain_model($dm_name, %$dm_opts);
486 my $dm_reader = $dm_attr->get_read_method;
487
488 unless( $class->can('inflate_result') ){
7adfd53f 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(@_));
493 };
494 $meta->add_method('inflate_result', $inflate_method);
495 }
496
f670cfd0 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');
5ee24b95 503 #XXX this one is for Action, ChooseOne and ChooseMany need this shit
f670cfd0 504 $meta->add_method('__ident_condition', sub {shift->$dm_reader->ident_condition} )
505 unless $class->can('__ident_condition');
506
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'));
510
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 {
514 my $super = shift;
515 confess "no dm reader: $dm_reader on $_[0]" unless $_[0]->can($dm_reader);
516 return { (target_model => $_[0]->$dm_reader), %{ $super->(@_) } };
517 };
518 $meta->add_around_method_modifier('_default_action_args_for', $def_act_args);
519
520 {
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 => ...]]
526 my $attr_haystack =
527 [ map { $_->name } $source_class->meta->compute_all_applicable_attributes ];
528
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;
538 }
539
540 my $attributes = $self->parse_reflect_rules($attr_rules, $attr_haystack);
541 for my $attr_name (keys %$attributes){
542 $self->reflect_source_object_attribute(
543 class => $class,
544 source_class => $source_class,
545 parent_class => $parent,
546 attribute_name => $attr_name,
547 domain_model_name => $dm_name,
548 %{ $attributes->{$attr_name} || {}},
549 );
550 }
7adfd53f 551 }
552
f670cfd0 553 {
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;
565 }
566
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} || {});
7b78a39d 576
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
f670cfd0 580 $self->reflect_source_action(
581 name => $action,
582 object_class => $class,
583 source_class => $source_class,
584 %$action_opts,
585 );
586
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
591 my $super = shift;
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) : () ) };
595 };
596 $meta->add_around_method_modifier('_default_action_args_for', $act_args);
7adfd53f 597 }
7adfd53f 598 }
599
600 $meta->make_immutable if $make_immutable;
601 return $meta;
602 };
603
604 # needs class, attribute_name domain_model_name
f670cfd0 605 implements reflect_source_object_attribute => as {
7adfd53f 606 my ($self, %opts) = @_;
f670cfd0 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" );
611 }
612
7adfd53f 613 my $meta = $opts{class}->meta;
f670cfd0 614 my $attr_opts = $self->parameters_for_source_object_attribute(%opts);
7adfd53f 615
616 my $make_immutable = $meta->is_immutable;
617 $meta->make_mutable if $meta->is_immutable;
f670cfd0 618
7adfd53f 619 my $attr = $meta->add_attribute($opts{attribute_name}, %$attr_opts);
7adfd53f 620
f670cfd0 621 $meta->make_immutable if $make_immutable;
7adfd53f 622 return $attr;
623 };
624
625 # needs class, attribute_name domain_model_name
f670cfd0 626 implements parameters_for_source_object_attribute => as {
7adfd53f 627 my ($self, %opts) = @_;
628
f670cfd0 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;
637
638 my $source;
639 $source = $source_class->result_source_instance if $source_class;
640 #puke! dwimery
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;
651 }
652 }
653
654 my $from_attr = $source_class->meta->find_attribute_by_name($attr_name);
7adfd53f 655
656 #default options. lazy build but no outsider method
657 my %attr_opts = ( is => 'ro', lazy => 1, required => 1,
658 clearer => "_clear_${attr_name}",
659 predicate => "has_${attr_name}",
660 domain_model => $dm_name,
661 orig_attr_name => $attr_name,
662 );
663
664 #m2m / has_many
665 my $constraint_is_ArrayRef =
666 $from_attr->type_constraint->name eq 'ArrayRef' ||
667 $from_attr->type_constraint->is_subtype_of('ArrayRef');
668
669 if( my $rel_info = $source->relationship_info($attr_name) ){
670 my $rel_accessor = $rel_info->{attrs}->{accessor};
f670cfd0 671 my $rel_moniker = $rel_info->{class}->result_source_instance->source_name;
7adfd53f 672
673 if($rel_accessor eq 'multi' && $constraint_is_ArrayRef) {
674 #has_many
f670cfd0 675 my $sm = $self->class_name_from_source_name($parent_class, $rel_moniker);
7adfd53f 676 #type constraint is a collection, and default builds it
f670cfd0 677 $attr_opts{isa} = $self->class_name_for_collection_of($sm);
7adfd53f 678 $attr_opts{default} = sub {
679 my $rs = shift->$dm_name->related_resultset($attr_name);
680 return $attr_opts{isa}->new(_source_resultset => $rs);
681 };
682 } elsif( $rel_accessor eq 'single') {
683 #belongs_to
684 #type constraint is the foreign IM object, default inflates it
f670cfd0 685 $attr_opts{isa} = $self->class_name_from_source_name($parent_class, $rel_moniker);
7adfd53f 686 $attr_opts{default} = sub {
687 shift->$dm_name
688 ->find_related($attr_name, {},{result_class => $attr_opts{isa}});
689 };
690 }
f670cfd0 691 } elsif( $constraint_is_ArrayRef && $attr_name =~ m/^(.*)_list$/ ) {
7adfd53f 692 #m2m magic
693 my $mm_name = $1;
694 my $link_table = "links_to_${mm_name}_list";
695 my ($hm_source, $far_side);
696 eval { $hm_source = $source->related_source($link_table); }
697 || confess "Can't find ${link_table} has_many for ${mm_name}_list";
698 eval { $far_side = $hm_source->related_source($mm_name); }
699 || confess "Can't find ${mm_name} belongs_to on ".$hm_source->result_class
700 ." traversing many-many for ${mm_name}_list";
701
f670cfd0 702 my $sm = $self->class_name_from_source_name($parent_class,$far_side->source_name);
703 $attr_opts{isa} = $self->class_name_for_collection_of($sm);
7adfd53f 704
705 #proper collections will remove the result_class uglyness.
706 $attr_opts{default} = sub {
2e2afc48 707 my $rs = shift->$dm_name->related_resultset($link_table)->related_resultset($mm_name);
7adfd53f 708 return $attr_opts{isa}->new(_source_resultset => $rs);
709 };
710 } else {
711 #no rel
712 my $reader = $from_attr->get_read_method;
713 $attr_opts{isa} = $from_attr->_isa_metadata;
714 $attr_opts{default} = sub{ shift->$dm_name->$reader };
715 }
716 return \%attr_opts;
717 };
718
719
f670cfd0 720 implements reflect_source_action => as{
7adfd53f 721 my($self, %opts) = @_;
f670cfd0 722 my $name = delete $opts{name};
723 my $class = delete $opts{class};
724 my $base = delete $opts{base} || Action;
725 my $object = delete $opts{object_class};
726 my $source = delete $opts{source_class};
727
728 confess("name, object_class and source_class are required arguments")
729 unless $source && $name && $object;
730
731 my $attr_rules = delete $opts{attributes};
732 $class ||= $object->_default_action_class_for($name);
733
734 Class::MOP::load_class( $base );
735 Class::MOP::load_class( $object );
736 Class::MOP::load_class( $source );
737
738 #print STDERR "\n\t", ref $attr_rules eq 'ARRAY' ? @$attr_rules : $attr_rules,"\n";
739 # attributes => undef, #default to qr/./
740 # attributes => [], #default to nothing
741 # attributes => qr//, #DWIM, treated as [qr//]
742 # attributes => [{...}] #DWIM, treat as [qr/./, {...} ]
743 # attributes => [[-exclude => ...]] #DWIM, treat as [qr/./, [-exclude => ...]]
32afff5d 744 my $attr_haystack = [ map { $_->name } $object->meta->parameter_attributes ];
f670cfd0 745 if(!defined $attr_rules){
746 $attr_rules = [qr/./];
747 } elsif( (!ref $attr_rules && $attr_rules) || (ref $attr_rules eq 'Regexp') ){
748 $attr_rules = [ $attr_rules ];
749 } elsif( ref $attr_rules eq 'ARRAY' && @$attr_rules){
750 #don't add a qr/./ rule if we have at least one match rule
751 push(@$attr_rules, qr/./) unless
752 grep {(ref $_ eq 'ARRAY' && $_->[0] ne '-exclude')
753 || !ref $_ || ref $_ eq 'Regexp'} @$attr_rules;
754 }
7adfd53f 755
f670cfd0 756 #print STDERR "${name}\t${class}\t${base}\n";
757 #print STDERR "\t${object}\t${source}\n";
758 #print STDERR "\t",@$attr_rules,"\n";
7adfd53f 759
f670cfd0 760 my $o_meta = $object->meta;
761 my $s_meta = $source->meta;
762 my $attributes = $self->parse_reflect_rules($attr_rules, $attr_haystack);
7adfd53f 763
764 #create the class
c11c77ee 765 my $meta = $self->_load_or_create($class, $base);
de48f4e6 766 my $make_immutable = $meta->is_immutable || $self->make_classes_immutable;
7adfd53f 767 $meta->make_mutable if $meta->is_immutable;
768
f670cfd0 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);
32afff5d 774 confess("Unable to find attribute for '${s_attr_name}' via '${source}'")
775 unless defined $s_attr;
f670cfd0 776 next unless $s_attr->get_write_method; #only rw attributes!
777
778 my $attr_params = $self->parameters_for_source_object_action_attribute
779 (
780 object_class => $object,
781 source_class => $source,
782 attribute_name => $attr_name
783 );
7adfd53f 784 $meta->add_attribute( $attr_name => %$attr_params);
785 }
786
787 $meta->make_immutable if $make_immutable;
788 return $meta;
789 };
790
f670cfd0 791 implements parameters_for_source_object_action_attribute => as {
7adfd53f 792 my ($self, %opts) = @_;
793
f670cfd0 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;
799
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);
7adfd53f 804
805 confess("${attr_name} is not writeable and can not be reflected")
806 unless $from_attr->get_write_method;
807
808 my %attr_opts = (
809 is => 'rw',
810 isa => $from_attr->_isa_metadata,
811 required => $from_attr->is_required,
e44959e8 812 ($from_attr->is_required
813 ? () : (clearer => "clear_$attr_name}")),
7adfd53f 814 predicate => "has_${attr_name}",
815 );
816
817 if ($attr_opts{required}) {
3fad510b 818 if($from_attr->has_default) {
819 $attr_opts{lazy} = 1;
820 $attr_opts{default} = $from_attr->default;
821 } else {
822 $attr_opts{lazy_fail} = 1;
823 }
7adfd53f 824 }
825
826 #test for relationships
827 my $constraint_is_ArrayRef =
828 $from_attr->type_constraint->name eq 'ArrayRef' ||
829 $from_attr->type_constraint->is_subtype_of('ArrayRef');
830
f670cfd0 831 my $source = $source_class->result_source_instance;
7adfd53f 832 if (my $rel_info = $source->relationship_info($attr_name)) {
833 my $rel_accessor = $rel_info->{attrs}->{accessor};
834
835 if($rel_accessor eq 'multi' && $constraint_is_ArrayRef) {
836 confess "${attr_name} is a rw has_many, this won't work.";
837 } elsif( $rel_accessor eq 'single') {
838 $attr_opts{valid_values} = sub {
839 shift->target_model->result_source->related_source($attr_name)->resultset;
840 };
841 }
842 } elsif ( $constraint_is_ArrayRef && $attr_name =~ m/^(.*)_list$/) {
843 my $mm_name = $1;
844 my $link_table = "links_to_${mm_name}_list";
845 my ($hm_source, $far_side);
846 eval { $hm_source = $source->related_source($link_table); }
847 || confess "Can't find ${link_table} has_many for ${mm_name}_list";
848 eval { $far_side = $hm_source->related_source($mm_name); }
849 || confess "Can't find ${mm_name} belongs_to on ".$hm_source->result_class
850 ." traversing many-many for ${mm_name}_list";
851
852 $attr_opts{default} = sub { [] };
853 $attr_opts{valid_values} = sub {
f670cfd0 854 shift->target_model->result_source->related_source($link_table)
7adfd53f 855 ->related_source($mm_name)->resultset;
856 };
857 }
f670cfd0 858 #use Data::Dumper;
89939ff9 859 #print STDERR "\n" .$attr_name ." - ". $object . "\n";
f670cfd0 860 #print STDERR Dumper(\%attr_opts);
7adfd53f 861 return \%attr_opts;
862 };
863
c11c77ee 864 implements _load_or_create => as {
865 my ($self, $class, $base) = @_;
866 my $meta = $self->_maybe_load_class($class) ?
867 $class->meta : $base->meta->create($class, superclasses => [ $base ]);
868 return $meta;
869 };
870
871 implements _maybe_load_class => as {
872 my ($self, $class) = @_;
873 my $file = $class . '.pm';
874 $file =~ s{::}{/}g;
875 my $ret = eval { Class::MOP::load_class($class) };
876 if ($INC{$file} && $@) {
877 confess "Error loading ${class}: $@";
878 }
879 return $ret;
880 };
881
7adfd53f 882};
883
8841;
0402136b 885
886#--------#---------#---------#---------#---------#---------#---------#---------#
887__END__;
888
889=head1 NAME
890
891Reaction::InterfaceModel::Reflector::DBIC -
892Automatically Generate InterfaceModels from DBIx::Class models
893
894=head1 DESCRIPTION
895
896The InterfaceModel reflectors are classes that are meant to aid you in easily
897generating Reaction::InterfaceModel classes that represent their underlying
898DBIx::Class domain models by introspecting your L<DBIx::Class::ResultSource>s
899and creating a collection of L<Reaction::InterfaceModel::Object> and
900L<Reaction::InterfaceModel::Collection> classes for you to use.
901
902The default base class of all Object classes will be
903 L<Reaction::InterfaceModel::Object> and the default Collection type will be
904L<Reaction::InterfaceModel::Collection::Virtual::ResultSet>.
905
906Additionally, the reflector can create InterfaceModel actions that interact
907with the supplied L<Reaction::UI::Controller::Collection::CRUD>, allowing you
908to easily set up a highly customizable CRUD interface in minimal time.
909
910At this time, supported collection actions consist of:
911
912=over 4
913
914=item B<> L<Reaction::INterfaceModel::Action::DBIC::ResultSet::Create>
915
916Creates a new item in the collection and underlying ResultSet.
917
918=item B<> L<Reaction::INterfaceModel::Action::DBIC::ResultSet::DeleteAll>
919
920Deletes all the items in a collection and it's underlying resultset using
921C<delete_all>
922
923=back
924
925And supported object actions are :
926
927=over 4
928
929=item B<Update> - via L<Reaction::INterfaceModel::Action::DBIC::Result::Update>
930
931Updates an existing object.
932
933=item B<Delete> - via L<Reaction::INterfaceModel::Action::DBIC::Result::Delete>
934
935Deletes an existing object.
936
937=back
938
939=head1 SYNOPSIS
940
941 package MyApp::IM::TestModel;
942 use base 'Reaction::InterfaceModel::Object';
943 use Reaction::Class;
944 use Reaction::InterfaceModel::Reflector::DBIC;
945 my $reflector = Reaction::InterfaceModel::Reflector::DBIC->new;
946
947 #Reflect everything
948 $reflector->reflect_schema
949 (
950 model_class => __PACKAGE__,
951 schema_class => 'MyApp::Schema',
952 );
953
954=head2 Selectively including and excluding sources
955
956 #reflect everything except for the FooBar and FooBaz classes
957 $reflector->reflect_schema
958 (
959 model_class => __PACKAGE__,
960 schema_class => 'MyApp::Schema',
961 sources => [-exclude => [qw/FooBar FooBaz/] ],
962 # you could also do:
963 sources => [-exclude => qr/(?:FooBar|FooBaz)/,
964 # or even
965 sources => [-exclude => [qr/FooBar/, qr/FooBaz/],
966 );
967
968 #reflect only the Foo family of sources
969 $reflector->reflect_schema
970 (
971 model_class => __PACKAGE__,
972 schema_class => 'MyApp::Schema',
973 sources => qr/^Foo/,
974 );
975
976=head2 Selectively including and excluding fields in sources
977
978 #Reflect Foo and Baz in their entirety and exclude the field 'avatar' in the Bar ResultSource
979 $reflector->reflect_schema
980 (
981 model_class => __PACKAGE__,
982 schema_class => 'MyApp::Schema',
983 sources => [qw/Foo Baz/,
984 [ Bar => {attributes => [[-exclude => 'avatar']] } ],
985 # or exclude by regex
986 [ Bar => {attributes => [-exclude => qr/avatar/] } ],
987 # or simply do not include it...
988 [ Bar => {attributes => [qw/id name description/] } ],
989 ],
990 );
991
992=head1 ATTRIBUTES
993
994=head2 make_classes_immutable
995
996=head2 object_actions
997
998=head2 collection_actions
999
1000=head2 default_object_actions
1001
1002=head2 default_collection_actions
1003
1004=head2 builtin_object_actions
1005
1006=head2 builtin_collection_actions
1007
1008=head1 METHODS
1009
1010=head2 new
1011
1012=head2 _all_object_actions
1013
1014=head2 _all_collection_actions
1015
1016=head2 dm_name_from_class_name
1017
1018=head2 dm_name_from_source_name
1019
1020=head2 class_name_from_source_name
1021
1022=head2 class_name_for_collection_of
1023
1024=head2 merge_hashes
1025
1026=head2 parse_reflect_rules
1027
1028=head2 merge_reflect_rules
1029
1030=head2 reflect_schema
1031
1032=head2 _compute_source_options
1033
1034=head2 add_source
1035
1036=head2 reflect_source
1037
1038=head2 reflect_source_collection
1039
1040=head2 reflect_source_object
1041
1042=head2 reflect_source_object_attribute
1043
1044=head2 parameters_for_source_object_attribute
1045
1046=head2 reflect_source_action
1047
1048=head2 parameters_for_source_object_action_attribute
1049
1050=head1 TODO
1051
1052Allow the reflector to dump the generated code out as files, eliminating the need to
1053reflect on startup every time. This will likely take quite a bit of work though. The
1054main work is already in place, but the grunt work is still left. At the moment there
1055is no closures that can't be dumped out as code with a little bit of work.
1056
1057=head1 AUTHORS
1058
1059See L<Reaction::Class> for authors.
1060
1061=head1 LICENSE
1062
1063See L<Reaction::Class> for the license.
1064
1065=cut