90% there with Reflector code generation!
[catagits/Reaction.git] / lib / Reaction / InterfaceModel / Reflector / DBIC.pm
index 8df719b..3643bd1 100644 (file)
@@ -161,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
@@ -279,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) = @_;
@@ -301,7 +305,7 @@ class DBIC, which {
     unless( $reader ){
       $reader = $source;
       $reader =~ s/([a-z0-9])([A-Z])/${1}_${2}/g ;
-      $reader = lc($reader) . "_collection"; #XXX change to not use  _collection ?
+      $reader = $self->_class_to_attribute_name($reader) . "_collection";
     }
     unless( $dm_name ){
       my @haystack = $meta->domain_models;
@@ -324,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);
@@ -375,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 || $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 {
@@ -463,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);
@@ -494,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');
 
@@ -660,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;
@@ -701,6 +729,14 @@ class DBIC, which {
         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;
@@ -735,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') ){
@@ -756,8 +792,7 @@ 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 $meta = $self->_load_or_create($class, $base);
     my $make_immutable = $meta->is_immutable || $self->make_classes_immutable;
     $meta->make_mutable if $meta->is_immutable;
 
@@ -766,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
         (
@@ -795,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;
 
@@ -802,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
@@ -849,6 +894,205 @@ class DBIC, which {
     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 L<DBIx::Class::ResultSource>s
+and creating a collection of L<Reaction::InterfaceModel::Object> and
+L<Reaction::InterfaceModel::Collection> classes for you to use.
+
+The default base class of all Object classes will be
+ L<Reaction::InterfaceModel::Object> and the default Collection type will be
+L<Reaction::InterfaceModel::Collection::Virtual::ResultSet>.
+
+Additionally, the reflector can create InterfaceModel actions that interact
+with the supplied L<Reaction::UI::Controller::Collection::CRUD>, 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<Reaction::INterfaceModel::Action::DBIC::ResultSet::Create>
+
+Creates a new item in the collection and underlying ResultSet.
+
+=item B<> L<Reaction::INterfaceModel::Action::DBIC::ResultSet::DeleteAll>
+
+Deletes all the items in a collection and it's underlying resultset using
+C<delete_all>
+
+=back
+
+And supported object actions are :
+
+=over 4
+
+=item B<Update> - via L<Reaction::INterfaceModel::Action::DBIC::Result::Update>
+
+Updates an existing object.
+
+=item B<Delete> - via L<Reaction::INterfaceModel::Action::DBIC::Result::Delete>
+
+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<Reaction::Class> for authors.
+
+=head1 LICENSE
+
+See L<Reaction::Class> for the license.
+
+=cut