X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?a=blobdiff_plain;f=lib%2FReaction%2FInterfaceModel%2FReflector%2FDBIC.pm;h=3643bd1f37eee10c2c5d2744fba5313ae9c71618;hb=7517cfe59c66f16fa08ce171b997f6bd7ff32ba5;hp=429ab4a91630d8a72b432fe7e9ea19b1e98be139;hpb=f670cfd0d1ce4753a2c76b27cdc01e8471e4cc4a;p=catagits%2FReaction.git diff --git a/lib/Reaction/InterfaceModel/Reflector/DBIC.pm b/lib/Reaction/InterfaceModel/Reflector/DBIC.pm index 429ab4a..3643bd1 100644 --- a/lib/Reaction/InterfaceModel/Reflector/DBIC.pm +++ b/lib/Reaction/InterfaceModel/Reflector/DBIC.pm @@ -1,6 +1,7 @@ package Reaction::InterfaceModel::Reflector::DBIC; use aliased 'Reaction::InterfaceModel::Action::DBIC::ResultSet::Create'; +use aliased 'Reaction::InterfaceModel::Action::DBIC::ResultSet::DeleteAll'; use aliased 'Reaction::InterfaceModel::Action::DBIC::Result::Update'; use aliased 'Reaction::InterfaceModel::Action::DBIC::Result::Delete'; @@ -14,6 +15,8 @@ use Catalyst::Utils; class DBIC, which { + has make_classes_immutable => (isa => "Bool", is => "rw", required => 1, default => sub{ 1 }); + #user defined actions and prototypes has object_actions => (isa => "HashRef", is => "rw", lazy_build => 1); has collection_actions => (isa => "HashRef", is => "rw", lazy_build => 1); @@ -26,25 +29,28 @@ class DBIC, which { has builtin_object_actions => (isa => "HashRef", is => "rw", lazy_build => 1); has builtin_collection_actions => (isa => "HashRef", is => "rw", lazy_build => 1); - implements build_object_actions => as { {} }; - implements build_collection_actions => as { {} }; + implements _build_object_actions => as { {} }; + implements _build_collection_actions => as { {} }; - implements build_default_object_actions => as { [ qw/Update Delete/ ] }; - implements build_default_collection_actions => as { [ 'Create' ] }; + implements _build_default_object_actions => as { [ qw/Update Delete/ ] }; + implements _build_default_collection_actions => as { [ qw/Create DeleteAll/ ] }; - implements build_builtin_object_actions => as { + implements _build_builtin_object_actions => as { { - Update => { base => Update }, - Delete => { base => Delete, attributes => [] }, + Update => { name => 'Update', base => Update }, + Delete => { name => 'Delete', base => Delete, attributes => [] }, }; }; - implements build_builtin_collection_actions => as { - { Create => {base => Create } }; + implements _build_builtin_collection_actions => as { + { + Create => {name => 'Create', base => Create }, + DeleteAll => {name => 'DeleteAll', base => DeleteAll, attributes => [] } + }; }; implements _all_object_actions => as { - my $self = shift; + my $self = shift; return $self->merge_hashes ($self->builtin_object_actions, $self->object_actions); }; @@ -155,8 +161,7 @@ class DBIC, which { unless($model && $schema); Class::MOP::load_class( $base ); Class::MOP::load_class( $schema ); - my $meta = eval {Class::MOP::load_class($model); } ? - $model->meta : $base->meta->create($model, superclasses => [ $base ]); + my $meta = $self->_load_or_create($model, $base); # sources => undef, #default to qr/./ # sources => [], #default to nothing @@ -178,7 +183,7 @@ class DBIC, which { my $sources = $self->parse_reflect_rules($rules, $haystack); - my $make_immutable = $meta->is_immutable; + my $make_immutable = $meta->is_immutable || $self->make_classes_immutable; $meta->make_mutable if $meta->is_immutable; $meta->add_domain_model @@ -273,6 +278,11 @@ class DBIC, which { }; }; + implements _class_to_attribute_name => as { + my ( $self, $str ) = @_; + confess("wrong arguments passed for _class_to_attribute_name") unless $str; + return join('_', map lc, split(/::|(?<=[a-z0-9])(?=[A-Z])/, $str)) + }; implements add_source => as { my ($self, %opts) = @_; @@ -295,7 +305,7 @@ class DBIC, which { unless( $reader ){ $reader = $source; $reader =~ s/([a-z0-9])([A-Z])/${1}_${2}/g ; - $reader = lc($reader) . "_collection"; + $reader = $self->_class_to_attribute_name($reader) . "_collection"; } unless( $dm_name ){ my @haystack = $meta->domain_models; @@ -318,14 +328,38 @@ class DBIC, which { required => 1, isa => $collection, reader => $reader, - predicate => "has_${name}", + predicate => "has_" . $self->_class_to_attribute_name($name) , domain_model => $dm_name, orig_attr_name => $source, default => sub { - $collection->new(_source_resultset => shift->$dm_name->resultset($source)); + my $self = $_[0]; + return $collection->new( + _source_resultset => $self->$dm_name->resultset($source), + _parent => $self, + ); }, ); +# my %debug_attr_opts = +# ( +# lazy => 1, +# required => 1, +# isa => $collection, +# reader => $reader, +# predicate => "has_" . $self->_class_to_attribute_name($name) , +# domain_model => $dm_name, +# orig_attr_name => $source, +# default => qq^sub { +# my \$self = \$_[0]; +# return $collection->new( +# _source_resultset => \$self->$dm_name->resultset("$source"), +# _parent => \$self, +# ); +# }, ^, +# ); + + + my $make_immutable = $meta->is_immutable; $meta->make_mutable if $make_immutable; my $attr = $meta->add_attribute($name, %attr_opts); @@ -369,12 +403,11 @@ class DBIC, which { Class::MOP::load_class( $base ); Class::MOP::load_class( $object ); - my $meta = eval { Class::MOP::load_class($class) } ? - $class->meta : $base->meta->create( $class, superclasses => [ $base ]); + my $meta = $self->_load_or_create($class, $base); - my $make_immutable = $meta->is_immutable; + my $make_immutable = $meta->is_immutable || $self->make_classes_immutable;; $meta->make_mutable if $meta->is_immutable; - $meta->add_method(_build_im_class => sub{ $object } ); + $meta->add_method(_build_member_type => sub{ $object } ); #XXX as a default pass the domain model as a target_model until i come up with something #better through the coercion method my $def_act_args = sub { @@ -407,6 +440,10 @@ class DBIC, which { for my $action (keys %$actions){ my $action_opts = $self->merge_hashes ($all_actions->{$action} || {}, $actions->{$action} || {}); + + #NOTE: If the name of the action is not specified in the prototype then use it's + #hash key as the name. I think this is sane beahvior, but I've actually been thinking + #of making Action prototypes their own separate objects $self->reflect_source_action( name => $action, object_class => $object, @@ -453,8 +490,7 @@ class DBIC, which { Class::MOP::load_class($schema) if $schema; Class::MOP::load_class($source_class); - my $meta = eval { Class::MOP::load_class($class) } ? - $class->meta : $base->meta->create($class, superclasses => [ $base ]); + my $meta = $self->_load_or_create($class, $base); #create the domain model $dm_name ||= $self->dm_name_from_source_name($source_name); @@ -463,7 +499,7 @@ class DBIC, which { $dm_opts->{is} ||= 'rw'; $dm_opts->{required} ||= 1; - my $make_immutable = $meta->is_immutable; + my $make_immutable = $meta->is_immutable || $self->make_classes_immutable;; $meta->make_mutable if $meta->is_immutable; my $dm_attr = $meta->add_domain_model($dm_name, %$dm_opts); @@ -484,7 +520,7 @@ class DBIC, which { #XXX move to using 'handles' for this? $meta->add_method('__id', sub {shift->$dm_reader->id} ) unless $class->can('__id'); - #XXX this one is for ActionForm, ChooseOne and ChooseMany need this shit + #XXX this one is for Action, ChooseOne and ChooseMany need this shit $meta->add_method('__ident_condition', sub {shift->$dm_reader->ident_condition} ) unless $class->can('__ident_condition'); @@ -557,6 +593,10 @@ class DBIC, which { for my $action (keys %$actions){ my $action_opts = $self->merge_hashes ($all_actions->{$action} || {}, $actions->{$action} || {}); + + #NOTE: If the name of the action is not specified in the prototype then use it's + #hash key as the name. I think this is sane beahvior, but I've actually been thinking + #of making Action prototypes their own separate objects $self->reflect_source_action( name => $action, object_class => $class, @@ -646,6 +686,8 @@ class DBIC, which { $from_attr->type_constraint->name eq 'ArrayRef' || $from_attr->type_constraint->is_subtype_of('ArrayRef'); + + if( my $rel_info = $source->relationship_info($attr_name) ){ my $rel_accessor = $rel_info->{attrs}->{accessor}; my $rel_moniker = $rel_info->{class}->result_source_instance->source_name; @@ -684,10 +726,17 @@ class DBIC, which { #proper collections will remove the result_class uglyness. $attr_opts{default} = sub { - my $rs = shift->$dm_name->result_source->related_source($link_table) - ->related_source($mm_name)->resultset; + my $rs = shift->$dm_name->related_resultset($link_table)->related_resultset($mm_name); return $attr_opts{isa}->new(_source_resultset => $rs); }; + #} elsif( $constraint_is_ArrayRef ){ + #test these to see if rel is m2m + #my $meth = $attr_name; + #if( $source->can("set_${meth}") && $source->can("add_to_${meth}") && + # $source->can("${meth}_rs") && $source->can("remove_from_${meth}") ){ + + + #} } else { #no rel my $reader = $from_attr->get_read_method; @@ -722,7 +771,7 @@ class DBIC, which { # attributes => qr//, #DWIM, treated as [qr//] # attributes => [{...}] #DWIM, treat as [qr/./, {...} ] # attributes => [[-exclude => ...]] #DWIM, treat as [qr/./, [-exclude => ...]] - my $attr_haystack = [ map {$_->name} $object->meta->parameter_attributes ]; + my $attr_haystack = [ map { $_->name } $object->meta->parameter_attributes ]; if(!defined $attr_rules){ $attr_rules = [qr/./]; } elsif( (!ref $attr_rules && $attr_rules) || (ref $attr_rules eq 'Regexp') ){ @@ -743,9 +792,8 @@ class DBIC, which { my $attributes = $self->parse_reflect_rules($attr_rules, $attr_haystack); #create the class - my $meta = eval { Class::MOP::load_class($class) } ? - $class->meta : $base->meta->create($class, superclasses => [$base]); - my $make_immutable = $meta->is_immutable; + my $meta = $self->_load_or_create($class, $base); + my $make_immutable = $meta->is_immutable || $self->make_classes_immutable; $meta->make_mutable if $meta->is_immutable; for my $attr_name (keys %$attributes){ @@ -753,7 +801,10 @@ class DBIC, which { my $o_attr = $o_meta->find_attribute_by_name($attr_name); my $s_attr_name = $o_attr->orig_attr_name || $attr_name; my $s_attr = $s_meta->find_attribute_by_name($s_attr_name); - next unless $s_attr->get_write_method; #only rw attributes! + confess("Unable to find attribute for '${s_attr_name}' via '${source}'") + unless defined $s_attr; + next unless $s_attr->get_write_method + && $s_attr->get_write_method !~ /^_/; #only rw attributes! my $attr_params = $self->parameters_for_source_object_action_attribute ( @@ -782,6 +833,8 @@ class DBIC, which { $source_class ||= $o_meta->find_attribute_by_name($dm_name)->_isa_metadata; my $from_attr = $source_class->meta->find_attribute_by_name($attr_name); + #print STDERR "$attr_name is type: " . $from_attr->meta->name . "\n"; + confess("${attr_name} is not writeable and can not be reflected") unless $from_attr->get_write_method; @@ -789,13 +842,18 @@ class DBIC, which { is => 'rw', isa => $from_attr->_isa_metadata, required => $from_attr->is_required, + ($from_attr->is_required + ? () : (clearer => "clear_${attr_name}")), predicate => "has_${attr_name}", ); if ($attr_opts{required}) { - $attr_opts{lazy} = 1; - $attr_opts{default} = $from_attr->has_default ? $from_attr->default : - sub{confess("${attr_name} must be provided before calling reader")}; + if($from_attr->has_default) { + $attr_opts{lazy} = 1; + $attr_opts{default} = $from_attr->default; + } else { + $attr_opts{lazy_fail} = 1; + } } #test for relationships @@ -831,10 +889,210 @@ class DBIC, which { }; } #use Data::Dumper; + #print STDERR "\n" .$attr_name ." - ". $object . "\n"; #print STDERR Dumper(\%attr_opts); return \%attr_opts; }; + implements _load_or_create => as { + my ($self, $class, $base) = @_; + my $meta = $self->_maybe_load_class($class) ? + $class->meta : $base->meta->create($class, superclasses => [ $base ]); + return $meta; + }; + + implements _maybe_load_class => as { + my ($self, $class) = @_; + my $file = $class . '.pm'; + $file =~ s{::}{/}g; + my $ret = eval { Class::MOP::load_class($class) }; + if ($INC{$file} && $@) { + confess "Error loading ${class}: $@"; + } + return $ret; + }; + }; 1; + +#--------#---------#---------#---------#---------#---------#---------#---------# +__END__; + +=head1 NAME + +Reaction::InterfaceModel::Reflector::DBIC - +Automatically Generate InterfaceModels from DBIx::Class models + +=head1 DESCRIPTION + +The InterfaceModel reflectors are classes that are meant to aid you in easily +generating Reaction::InterfaceModel classes that represent their underlying +DBIx::Class domain models by introspecting your Ls +and creating a collection of L and +L classes for you to use. + +The default base class of all Object classes will be + L and the default Collection type will be +L. + +Additionally, the reflector can create InterfaceModel actions that interact +with the supplied L, allowing you +to easily set up a highly customizable CRUD interface in minimal time. + +At this time, supported collection actions consist of: + +=over 4 + +=item B<> L + +Creates a new item in the collection and underlying ResultSet. + +=item B<> L + +Deletes all the items in a collection and it's underlying resultset using +C + +=back + +And supported object actions are : + +=over 4 + +=item B - via L + +Updates an existing object. + +=item B - via L + +Deletes an existing object. + +=back + +=head1 SYNOPSIS + + package MyApp::IM::TestModel; + use base 'Reaction::InterfaceModel::Object'; + use Reaction::Class; + use Reaction::InterfaceModel::Reflector::DBIC; + my $reflector = Reaction::InterfaceModel::Reflector::DBIC->new; + + #Reflect everything + $reflector->reflect_schema + ( + model_class => __PACKAGE__, + schema_class => 'MyApp::Schema', + ); + +=head2 Selectively including and excluding sources + + #reflect everything except for the FooBar and FooBaz classes + $reflector->reflect_schema + ( + model_class => __PACKAGE__, + schema_class => 'MyApp::Schema', + sources => [-exclude => [qw/FooBar FooBaz/] ], + # you could also do: + sources => [-exclude => qr/(?:FooBar|FooBaz)/, + # or even + sources => [-exclude => [qr/FooBar/, qr/FooBaz/], + ); + + #reflect only the Foo family of sources + $reflector->reflect_schema + ( + model_class => __PACKAGE__, + schema_class => 'MyApp::Schema', + sources => qr/^Foo/, + ); + +=head2 Selectively including and excluding fields in sources + + #Reflect Foo and Baz in their entirety and exclude the field 'avatar' in the Bar ResultSource + $reflector->reflect_schema + ( + model_class => __PACKAGE__, + schema_class => 'MyApp::Schema', + sources => [qw/Foo Baz/, + [ Bar => {attributes => [[-exclude => 'avatar']] } ], + # or exclude by regex + [ Bar => {attributes => [-exclude => qr/avatar/] } ], + # or simply do not include it... + [ Bar => {attributes => [qw/id name description/] } ], + ], + ); + +=head1 ATTRIBUTES + +=head2 make_classes_immutable + +=head2 object_actions + +=head2 collection_actions + +=head2 default_object_actions + +=head2 default_collection_actions + +=head2 builtin_object_actions + +=head2 builtin_collection_actions + +=head1 METHODS + +=head2 new + +=head2 _all_object_actions + +=head2 _all_collection_actions + +=head2 dm_name_from_class_name + +=head2 dm_name_from_source_name + +=head2 class_name_from_source_name + +=head2 class_name_for_collection_of + +=head2 merge_hashes + +=head2 parse_reflect_rules + +=head2 merge_reflect_rules + +=head2 reflect_schema + +=head2 _compute_source_options + +=head2 add_source + +=head2 reflect_source + +=head2 reflect_source_collection + +=head2 reflect_source_object + +=head2 reflect_source_object_attribute + +=head2 parameters_for_source_object_attribute + +=head2 reflect_source_action + +=head2 parameters_for_source_object_action_attribute + +=head1 TODO + +Allow the reflector to dump the generated code out as files, eliminating the need to +reflect on startup every time. This will likely take quite a bit of work though. The +main work is already in place, but the grunt work is still left. At the moment there +is no closures that can't be dumped out as code with a little bit of work. + +=head1 AUTHORS + +See L for authors. + +=head1 LICENSE + +See L for the license. + +=cut