X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?a=blobdiff_plain;f=lib%2FReaction%2FInterfaceModel%2FReflector%2FDBIC.pm;h=230683ad65db4d802e39672bb8015c27bd0e0632;hb=7fdf5e932b12471adba977ef9c627c204807a64f;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..230683a 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'; @@ -12,829 +13,1071 @@ use Class::MOP; use Catalyst::Utils; -class DBIC, which { +use namespace::clean -except => [ qw(meta) ]; + +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); + +#which actions to create by default +has default_object_actions => (isa => "ArrayRef", is => "rw", lazy_build => 1); +has default_collection_actions => (isa => "ArrayRef", is => "rw", lazy_build => 1); + +#builtin actions and prototypes +has builtin_object_actions => (isa => "HashRef", is => "rw", lazy_build => 1); +has builtin_collection_actions => (isa => "HashRef", is => "rw", lazy_build => 1); +sub _build_object_actions { {} }; +sub _build_collection_actions { {} }; +sub _build_default_object_actions { [ qw/Update Delete/ ] }; +sub _build_default_collection_actions { [ qw/Create DeleteAll/ ] }; +sub _build_builtin_object_actions { + { + Update => { name => 'Update', base => Update }, + Delete => { name => 'Delete', base => Delete, attributes => [] }, + }; +}; +sub _build_builtin_collection_actions { + { + Create => {name => 'Create', base => Create }, + DeleteAll => {name => 'DeleteAll', base => DeleteAll, attributes => [] } + }; +}; +sub _all_object_actions { + my $self = shift; + return $self->merge_hashes + ($self->builtin_object_actions, $self->object_actions); +}; +sub _all_collection_actions { + my $self = shift; + return $self->merge_hashes + ($self->builtin_collection_actions, $self->collection_actions); +}; +sub dm_name_from_class_name { + my($self, $class) = @_; + confess("wrong arguments") unless $class; + $class =~ s/::/_/g; + $class = "_" . $self->_class_to_attribute_name($class) . "_store"; + return $class; +}; +sub dm_name_from_source_name { + my($self, $source) = @_; + confess("wrong arguments") unless $source; + $source =~ s/([a-z0-9])([A-Z])/${1}_${2}/g ; + $source = "_" . $self->_class_to_attribute_name($source) . "_store"; + return $source; +}; +sub class_name_from_source_name { + my ($self, $model_class, $source_name) = @_; + confess("wrong arguments") unless $model_class && $source_name; + return join "::", $model_class, $source_name; +}; +sub class_name_for_collection_of { + my ($self, $object_class) = @_; + confess("wrong arguments") unless $object_class; + return "${object_class}::Collection"; +}; +sub merge_hashes { + my($self, $left, $right) = @_; + return Catalyst::Utils::merge_hashes($left, $right); +}; +sub parse_reflect_rules { + my ($self, $rules, $haystack) = @_; + confess('$rules must be an array reference') unless ref $rules eq 'ARRAY'; + confess('$haystack must be an array reference') unless ref $haystack eq 'ARRAY'; + + my $needles = {}; + my (@exclude, @include, $global_opts); + if(@$rules == 2 && $rules->[0] eq '-exclude'){ + push(@exclude, (ref $rules->[1] eq 'ARRAY' ? @{$rules->[1]} : $rules->[1])); + } else { + for my $rule ( @$rules ){ + if (ref $rule eq 'ARRAY' && $rule->[0] eq '-exclude'){ + push(@exclude, (ref $rule->[1] eq 'ARRAY' ? @{$rule->[1]} : $rule->[1])); + } elsif( ref $rule eq 'HASH' ){ + $global_opts = ref $global_opts eq 'HASH' ? + $self->merge_hashes($global_opts, $rule) : $rule; + } else { + push(@include, $rule); + } + } + } + my $check_exclude = sub{ + for my $rule (@exclude){ + return 1 if(ref $rule eq 'Regexp' ? $_[0] =~ /$rule/ : $_[0] eq $rule); + } + return; + }; - #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); + @$haystack = grep { !$check_exclude->($_) } @$haystack; + $self->merge_reflect_rules(\@include, $needles, $haystack, $global_opts); + return $needles; +}; +sub merge_reflect_rules { + my ($self, $rules, $needles, $haystack, $local_opts) = @_; + for my $rule ( @$rules ){ + if(!ref $rule && ( grep {$rule eq $_} @$haystack ) ){ + $needles->{$rule} = defined $needles->{$rule} ? + $self->merge_hashes($needles->{$rule}, $local_opts) : $local_opts; + } elsif( ref $rule eq 'Regexp' ){ + for my $match ( grep { /$rule/ } @$haystack ){ + $needles->{$match} = defined $needles->{$match} ? + $self->merge_hashes($needles->{$match}, $local_opts) : $local_opts; + } + } elsif( ref $rule eq 'ARRAY' ){ + my $opts; + $opts = pop(@$rule) if @$rule > 1 and ref $rule->[$#$rule] eq 'HASH'; + $opts = $self->merge_hashes($local_opts, $opts) if defined $local_opts; + $self->merge_reflect_rules($rule, $needles, $haystack, $opts); + } + } +}; +sub reflect_schema { + my ($self, %opts) = @_; + my $base = delete $opts{base} || Object; + my $roles = delete $opts{roles} || []; + my $model = delete $opts{model_class}; + my $schema = delete $opts{schema_class}; + my $dm_name = delete $opts{domain_model_name}; + my $dm_args = delete $opts{domain_model_args} || {}; + $dm_name ||= $self->dm_name_from_class_name($schema); + + #load all necessary classes + confess("model_class and schema_class are required parameters") + unless($model && $schema); + Class::MOP::load_class( $base ); + Class::MOP::load_class( $schema ); + my $meta = $self->_load_or_create( + $model, + superclasses => [$base], + ( @$roles ? (roles => $roles) : ()), + ); + + # sources => undef, #default to qr/./ + # sources => [], #default to nothing + # sources => qr//, #DWIM, treated as [qr//] + # sources => [{...}] #DWIM, treat as [qr/./, {...} ] + # sources => [[-exclude => ...]] #DWIM, treat as [qr/./, [-exclude => ...]] + my $haystack = [ $schema->sources ]; + + my $rules = delete $opts{sources}; + if(!defined $rules){ + $rules = [qr/./]; + } elsif( ref $rules eq 'Regexp'){ + $rules = [ $rules ]; + } elsif( ref $rules eq 'ARRAY' && @$rules){ + #don't add a qr/./ rule if we have at least one match rule + push(@$rules, qr/./) unless grep {(ref $_ eq 'ARRAY' && $_->[0] ne '-exclude') + || !ref $_ || ref $_ eq 'Regexp'} @$rules; + } + + my $sources = $self->parse_reflect_rules($rules, $haystack); + + my $make_immutable = $meta->is_immutable || $self->make_classes_immutable; + $meta->make_mutable if $meta->is_immutable; + + $meta->add_domain_model + ($dm_name, is => 'rw', isa => $schema, required => 1, %$dm_args); + + for my $source_name (keys %$sources){ + my $source_opts = $sources->{$source_name} || {}; + $self->reflect_source( + source_name => $source_name, + parent_class => $model, + schema_class => $schema, + source_class => $schema->class($source_name), + parent_domain_model_name => $dm_name, + %$source_opts + ); + } + + $meta->make_immutable if $make_immutable; + return $meta; +}; +sub _compute_source_options { + my ($self, %opts) = @_; + my $schema = delete $opts{schema_class}; + my $source_name = delete $opts{source_name}; + my $source_class = delete $opts{source_class}; + my $parent = delete $opts{parent_class}; + my $parent_dm = delete $opts{parent_domain_model_name}; + + #this is the part where I hate my life for promissing all sorts of DWIMery + confess("parent_class and source_name or source_class are required parameters") + unless($parent && ($source_name || $source_class)); + +OUTER: until( $schema && $source_name && $source_class && $parent_dm ){ + if( $schema && !$source_name){ + next OUTER if $source_name = $schema->source($source_class)->source_name; + } elsif( $schema && !$source_class){ + next OUTER if $source_class = eval { $schema->class($source_name) }; + } + + my @haystack = $parent_dm ? $parent->meta->find_attribute_by_name($parent_dm) : (); + + #there's a lot of guessing going on, but it should work fine on most cases + INNER: for my $needle (@haystack){ + my $isa = $needle->_isa_metadata; + next INNER unless Class::MOP::load_class( $isa->_isa_metadata ); + next INNER unless $isa->isa('DBIx::Class::Schema'); + + if( $source_name ){ + my $src_class = eval{ $isa->class($source_name) }; + next INNER unless $src_class; + next INNER if($source_class && $source_class ne $src_class); + $schema = $isa; + $parent_dm = $needle->name; + $source_class = $src_class; + next OUTER; + } + } - #which actions to create by default - has default_object_actions => (isa => "ArrayRef", is => "rw", lazy_build => 1); - has default_collection_actions => (isa => "ArrayRef", is => "rw", lazy_build => 1); - #builtin actions and prototypes - has builtin_object_actions => (isa => "HashRef", is => "rw", lazy_build => 1); - has builtin_collection_actions => (isa => "HashRef", is => "rw", lazy_build => 1); + confess("Could not determine options automatically from: schema " . + "'${schema}', source_name '${source_name}', source_class " . + "'${source_class}', parent_domain_model_name '${parent_dm}'"); + } + + return { + source_name => $source_name, + schema_class => $schema, + source_class => $source_class, + parent_class => $parent, + parent_domain_model_name => $parent_dm, + }; +}; +sub _class_to_attribute_name { + 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)) +}; +sub add_source { + my ($self, %opts) = @_; + + my $model = delete $opts{model_class}; + my $reader = delete $opts{reader}; + my $source = delete $opts{source_name}; + my $dm_name = delete $opts{domain_model_name}; + my $collection = delete $opts{collection_class}; + my $name = delete $opts{attribute_name} || $source; + + confess("model_class and source_name are required parameters") + unless $model && $source; + my $meta = $model->meta; + + unless( $collection ){ + my $object = $self->class_name_from_source_name($model, $source); + $collection = $self->class_name_for_collection_of($object); + } + unless( $reader ){ + $reader = $source; + $reader =~ s/([a-z0-9])([A-Z])/${1}_${2}/g ; + $reader = $self->_class_to_attribute_name($reader) . "_collection"; + } + unless( $dm_name ){ + my @haystack = $meta->domain_models; + if( @haystack > 1 ){ + @haystack = grep { $_->_isa_metadata->isa('DBIx::Class::Schema') } @haystack; + } + if(@haystack == 1){ + $dm_name = $haystack[0]->name; + } elsif(@haystack > 1){ + confess("Failed to automatically determine domain_model_name. More than one " . + "possible match (".(join ", ", map{"'".$_->name."'"} @haystack).")"); + } else { + confess("Failed to automatically determine domain_model_name. No matches."); + } + } + + my %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 => sub { + $collection->new + ( + _source_resultset => $_[0]->$dm_name->resultset($source), + _parent => $_[0], + ); + }, + ); + + my $make_immutable = $meta->is_immutable; + $meta->make_mutable if $make_immutable; + my $attr = $meta->add_attribute($name, %attr_opts); + $meta->make_immutable if $make_immutable; + + return $attr; +}; +sub reflect_source { + my ($self, %opts) = @_; + my $collection = delete $opts{collection} || {}; + %opts = %{ $self->merge_hashes(\%opts, $self->_compute_source_options(%opts)) }; + + my $obj_meta = $self->reflect_source_object(%opts); + my $col_meta = $self->reflect_source_collection + ( + object_class => $obj_meta->name, + source_class => $opts{source_class}, + schema => $opts{schema_class}, + %$collection + ); + + $self->add_source( + %opts, + model_class => delete $opts{parent_class}, + domain_model_name => delete $opts{parent_domain_model_name}, + collection_class => $col_meta->name, + ); +}; +sub reflect_source_collection { + my ($self, %opts) = @_; + my $base = delete $opts{base} || ResultSet; + my $roles = delete $opts{roles} || []; + my $class = delete $opts{class}; + my $object = delete $opts{object_class}; + my $source = delete $opts{source_class}; + my $action_rules = delete $opts{actions}; + my $schema = delete $opts{schema}; + + confess('object_class and source_class are required parameters') + unless $object && $source; + $class ||= $self->class_name_for_collection_of($object); + + Class::MOP::load_class( $base ); + Class::MOP::load_class( $object ); + + my $meta = $self->_load_or_create( + $class, + superclasses => [$base], + ( @$roles ? (roles => $roles) : ()), + ); + + my $make_immutable = $meta->is_immutable || $self->make_classes_immutable;; + $meta->make_mutable if $meta->is_immutable; + $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 { + my $super = shift; + return { (target_model => $_[0]->_source_resultset), %{ $super->(@_) } }; + }; + $meta->add_around_method_modifier('_default_action_args_for', $def_act_args); - 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' ] }; + { + my $all_actions = $self->_all_collection_actions; + my $action_haystack = [keys %$all_actions]; + if(!defined $action_rules){ + $action_rules = $self->default_collection_actions; + } elsif( (!ref $action_rules && $action_rules) || (ref $action_rules eq 'Regexp') ){ + $action_rules = [ $action_rules ]; + } elsif( ref $action_rules eq 'ARRAY' && @$action_rules){ + #don't add a qr/./ rule if we have at least one match rule + push(@$action_rules, qr/./) + unless grep {(ref $_ eq 'ARRAY' && $_->[0] ne '-exclude') + || !ref $_ || ref $_ eq 'Regexp'} @$action_rules; + } - implements build_builtin_object_actions => as { - { - Update => { base => Update }, - Delete => { base => Delete, attributes => [] }, + # XXX this is kind of a dirty hack to support custom actions that are not + # previously defined and still be able to use the parse_reflect_rules mechanism + my @custom_actions = grep {!exists $all_actions->{$_}} + map{ $_->[0] } grep {ref $_ eq 'ARRAY' && $_->[0] ne '-exclude'} @$action_rules; + push(@$action_haystack, @custom_actions); + my $actions = $self->parse_reflect_rules($action_rules, $action_haystack); + 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( + schema => $schema, + name => $action, + object_class => $object, + source_class => $source, + %$action_opts, + ); + + # XXX i will move this to use the coercion method soon. this will be + # GoodEnough until then. I still need to think a little about the type coercion + # thing so i don't make a mess of it + my $act_args = sub { #override target model for this action + my $super = shift; + return { %{ $super->(@_) }, + ($_[1] eq $action ? (target_model => $_[0]->_source_resultset) : () ) }; + }; + $meta->add_around_method_modifier('_default_action_args_for', $act_args); + } + } + $meta->make_immutable if $make_immutable; + return $meta; +}; +sub reflect_source_object { + my($self, %opts) = @_; + %opts = %{ $self->merge_hashes(\%opts, $self->_compute_source_options(%opts)) }; + + my $base = delete $opts{base} || Object; + my $roles = delete $opts{roles} || []; + my $class = delete $opts{class}; + my $dm_name = delete $opts{domain_model_name}; + my $dm_opts = delete $opts{domain_model_args} || {}; + + my $source_name = delete $opts{source_name}; + my $schema = delete $opts{schema_class}; + my $source_class = delete $opts{source_class}; + my $parent = delete $opts{parent_class}; + my $parent_dm = delete $opts{parent_domain_model_name}; + + my $action_rules = delete $opts{actions}; + my $attr_rules = delete $opts{attributes}; + + $class ||= $self->class_name_from_source_name($parent, $source_name); + + Class::MOP::load_class($parent); + Class::MOP::load_class($schema) if $schema; + Class::MOP::load_class($source_class); + + my $meta = $self->_load_or_create( + $class, + superclasses => [$base], + ( @$roles ? (roles => $roles) : ()), + ); + + #create the domain model + $dm_name ||= $self->dm_name_from_source_name($source_name); + + $dm_opts->{isa} = $source_class; + $dm_opts->{is} ||= 'rw'; + $dm_opts->{required} ||= 1; + + 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); + my $dm_reader = $dm_attr->get_read_method; + + unless( $class->can('inflate_result') ){ + my $inflate_method = sub { + my $class = shift; my ($src) = @_; + $src = $src->resolve if $src->isa('DBIx::Class::ResultSourceHandle'); + $class->new($dm_name, $src->result_class->inflate_result(@_)); }; + $meta->add_method('inflate_result', $inflate_method); + } + + #XXX this is here to allow action prototypes to work with ListView + # maybe Collections hsould have this kind of thing too to allow you to reconstruct them? + #i like the possibility to be honest... as aset of key/value pairs they could be URId + #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 Action, ChooseOne and ChooseMany need this shit + $meta->add_method('__ident_condition', sub {shift->$dm_reader->ident_condition} ) + unless $class->can('__ident_condition'); + + #XXX this is just a disaster + $meta->add_method('display_name', sub {shift->$dm_reader->display_name} ) + if( $source_class->can('display_name') && !$class->can('display_name')); + + #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 { + my $super = shift; + confess "no dm reader: $dm_reader on $_[0]" unless $_[0]->can($dm_reader); + return { (target_model => $_[0]->$dm_reader), %{ $super->(@_) } }; }; + $meta->add_around_method_modifier('_default_action_args_for', $def_act_args); - implements build_builtin_collection_actions => as { - { Create => {base => Create } }; - }; + { + # attributes => undef, #default to qr/./ + # attributes => [], #default to nothing + # attributes => qr//, #DWIM, treated as [qr//] + # attributes => [{...}] #DWIM, treat as [qr/./, {...} ] + # attributes => [[-exclude => ...]] #DWIM, treat as [qr/./, [-exclude => ...]] + my $attr_haystack = + [ map { $_->name } $source_class->meta->compute_all_applicable_attributes ]; - implements _all_object_actions => as { - my $self = shift; - return $self->merge_hashes - ($self->builtin_object_actions, $self->object_actions); - }; + if(!defined $attr_rules){ + $attr_rules = [qr/./]; + } elsif( (!ref $attr_rules && $attr_rules) || (ref $attr_rules eq 'Regexp') ){ + $attr_rules = [ $attr_rules ]; + } elsif( ref $attr_rules eq 'ARRAY' && @$attr_rules){ + #don't add a qr/./ rule if we have at least one match rule + push(@$attr_rules, qr/./) unless + grep {(ref $_ eq 'ARRAY' && $_->[0] ne '-exclude') + || !ref $_ || ref $_ eq 'Regexp'} @$attr_rules; + } - implements _all_collection_actions => as { - my $self = shift; - return $self->merge_hashes - ($self->builtin_collection_actions, $self->collection_actions); - }; + my $attributes = $self->parse_reflect_rules($attr_rules, $attr_haystack); + for my $attr_name (keys %$attributes){ + $self->reflect_source_object_attribute( + schema => $schema, + class => $class, + source_class => $source_class, + parent_class => $parent, + attribute_name => $attr_name, + domain_model_name => $dm_name, + %{ $attributes->{$attr_name} || {}}, + ); + } + } + + { + my $all_actions = $self->_all_object_actions; + my $action_haystack = [keys %$all_actions]; + if(!defined $action_rules){ + $action_rules = $self->default_object_actions; + } elsif( (!ref $action_rules && $action_rules) || (ref $action_rules eq 'Regexp') ){ + $action_rules = [ $action_rules ]; + } elsif( ref $action_rules eq 'ARRAY' && @$action_rules){ + #don't add a qr/./ rule if we have at least one match rule + push(@$action_rules, qr/./) + unless grep {(ref $_ eq 'ARRAY' && $_->[0] ne '-exclude') + || !ref $_ || ref $_ eq 'Regexp'} @$action_rules; + } - implements dm_name_from_class_name => as { - my($self, $class) = @_; - confess("wrong arguments") unless $class; - $class =~ s/::/_/g; - $class = "_" . lc($class) . "_store"; - return $class; - }; + # XXX this is kind of a dirty hack to support custom actions that are not + # previously defined and still be able to use the parse_reflect_rules mechanism + my @custom_actions = grep {!exists $all_actions->{$_}} map{ $_->[0] } + grep {ref $_ eq 'ARRAY' && $_->[0] ne '-exclude'} @$action_rules; + push(@$action_haystack, @custom_actions); + my $actions = $self->parse_reflect_rules($action_rules, $action_haystack); + 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( + schema => $schema, + name => $action, + object_class => $class, + source_class => $source_class, + %$action_opts, + ); + + # XXX i will move this to use the coercion method soon. this will be + # GoodEnough until then. I still need to think a little about the type coercion + # thing so i don't make a mess of it + my $act_args = sub { #override target model for this action + my $super = shift; + confess "no dm reader: $dm_reader on $_[0]" unless $_[0]->can($dm_reader); + return { %{ $super->(@_) }, + ($_[1] eq $action ? (target_model => $_[0]->$dm_reader) : () ) }; + }; + $meta->add_around_method_modifier('_default_action_args_for', $act_args); + } + } - implements dm_name_from_source_name => as { - my($self, $source) = @_; - confess("wrong arguments") unless $source; - $source =~ s/([a-z0-9])([A-Z])/${1}_${2}/g ; - $source = "_" . lc($source) . "_store"; - return $source; - }; + $meta->make_immutable if $make_immutable; + return $meta; +}; - implements class_name_from_source_name => as { - my ($self, $model_class, $source_name) = @_; - confess("wrong arguments") unless $model_class && $source_name; - return join "::", $model_class, $source_name; - }; +# needs class, attribute_name domain_model_name +sub reflect_source_object_attribute { + my ($self, %opts) = @_; + unless( $opts{attribute_name} && $opts{class} && $opts{parent_class} + && ( $opts{source_class} || $opts{domain_model_name} ) ){ + confess( "Error: class, parent_class, attribute_name, and either " . + "domain_model_name or source_class are required parameters" ); + } - implements class_name_for_collection_of => as { - my ($self, $object_class) = @_; - confess("wrong arguments") unless $object_class; - return "${object_class}::Collection"; - }; + my $meta = $opts{class}->meta; + my $attr_opts = $self->parameters_for_source_object_attribute(%opts); - implements merge_hashes => as { - my($self, $left, $right) = @_; - return Catalyst::Utils::merge_hashes($left, $right); - }; + my $make_immutable = $meta->is_immutable; + $meta->make_mutable if $meta->is_immutable; - implements parse_reflect_rules => as { - my ($self, $rules, $haystack) = @_; - confess('$rules must be an array reference') unless ref $rules eq 'ARRAY'; - confess('$haystack must be an array reference') unless ref $haystack eq 'ARRAY'; + my $attr = $meta->add_attribute($opts{attribute_name}, %$attr_opts); - my $needles = {}; - my (@exclude, @include, $global_opts); - if(@$rules == 2 && $rules->[0] eq '-exclude'){ - push(@exclude, (ref $rules->[1] eq 'ARRAY' ? @{$rules->[1]} : $rules->[1])); - } else { - for my $rule ( @$rules ){ - if (ref $rule eq 'ARRAY' && $rule->[0] eq '-exclude'){ - push(@exclude, (ref $rule->[1] eq 'ARRAY' ? @{$rule->[1]} : $rule->[1])); - } elsif( ref $rule eq 'HASH' ){ - $global_opts = ref $global_opts eq 'HASH' ? - $self->merge_hashes($global_opts, $rule) : $rule; - } else { - push(@include, $rule); + $meta->make_immutable if $make_immutable; + return $attr; +}; + +# needs class, attribute_name domain_model_name +sub parameters_for_source_object_attribute { + my ($self, %opts) = @_; + + my $class = delete $opts{class}; + my $attr_name = delete $opts{attribute_name}; + my $dm_name = delete $opts{domain_model_name}; + my $source_class = delete $opts{source_class}; + my $parent_class = delete $opts{parent_class}; + my $schema = $opts{schema}; + confess("parent_class is a required argument") unless $parent_class; + confess("You must supply at least one of domain_model_name and source_class") + unless $dm_name || $source_class; + + my $source = $schema->source($source_class); + my $from_attr = $source_class->meta->find_attribute_by_name($attr_name); + my $reader = $from_attr->get_read_method; + die("Could not find reader for attribute '$attr_name' on $source_class") + unless $reader; + + #default options. lazy build but no outsider method + my %attr_opts = ( is => 'ro', lazy => 1, required => 1, + clearer => "_clear_${attr_name}", + predicate => { + "has_${attr_name}" => + sub { defined(shift->$dm_name->$reader) } + }, + domain_model => $dm_name, + orig_attr_name => $attr_name, + ); + $attr_opts{coerce} = 1 if $from_attr->should_coerce; + + #m2m / has_many + my $m2m_meta; + if(my $coderef = $source->result_class->can('_m2m_metadata')){ + $m2m_meta = $source->result_class->$coderef; + } + + my $constraint_is_ArrayRef = + $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 = $schema->source($rel_info->{class})->source_name; + + if($rel_accessor eq 'multi' && $constraint_is_ArrayRef) { + #has_many + my $sm = $self->class_name_from_source_name($parent_class, $rel_moniker); + #type constraint is a collection, and default builds it + my $isa = $attr_opts{isa} = $self->class_name_for_collection_of($sm); + $attr_opts{default} = eval "sub { + my \$rs = shift->${dm_name}->related_resultset('${attr_name}'); + return ${isa}->new(_source_resultset => \$rs); + }"; + } elsif( $rel_accessor eq 'single' || $rel_accessor eq 'filter' ) { + #belongs_to + #type constraint is the foreign IM object, default inflates it + my $isa = $attr_opts{isa} = $self->class_name_from_source_name($parent_class, $rel_moniker); + $attr_opts{default} = eval "sub { + if (defined(my \$o = shift->${dm_name}->${reader})) { + return ${isa}->inflate_result(\$o->result_source, { \$o->get_columns }); } - } + return undef; + }"; } - my $check_exclude = sub{ - for my $rule (@exclude){ - return 1 if(ref $rule eq 'Regexp' ? $_[0] =~ /$rule/ : $_[0] eq $rule); + } elsif( $constraint_is_ArrayRef && $attr_name =~ m/^(.*)_list$/ ) { + #m2m magic + my $mm_name = $1; + my $link_table = "links_to_${mm_name}_list"; + my ($hm_source, $far_side); + eval { $hm_source = $source->related_source($link_table); } + || confess "Can't find ${link_table} has_many for ${mm_name}_list"; + eval { $far_side = $hm_source->related_source($mm_name); } + || confess "Can't find ${mm_name} belongs_to on ".$hm_source->result_class + ." traversing many-many for ${mm_name}_list"; + + my $sm = $self->class_name_from_source_name($parent_class,$far_side->source_name); + my $isa = $attr_opts{isa} = $self->class_name_for_collection_of($sm); + + #proper collections will remove the result_class uglyness. + $attr_opts{default} = eval "sub { + my \$rs = shift->${dm_name}->related_resultset('${link_table}')->related_resultset('${mm_name}'); + return ${isa}->new(_source_resultset => \$rs); + }"; + } elsif( $constraint_is_ArrayRef && defined $m2m_meta && exists $m2m_meta->{$attr_name} ){ + #m2m if using introspectable m2m component + my $rel = $m2m_meta->{$attr_name}->{relation}; + my $far_rel = $m2m_meta->{$attr_name}->{foreign_relation}; + my $far_source = $source->related_source($rel)->related_source($far_rel); + my $sm = $self->class_name_from_source_name($parent_class, $far_source->source_name); + my $isa = $attr_opts{isa} = $self->class_name_for_collection_of($sm); + + my $rs_meth = $m2m_meta->{$attr_name}->{rs_method}; + $attr_opts{default} = eval "sub { + return ${isa}->new(_source_resultset => shift->${dm_name}->${rs_meth}); + }"; + } else { + #no rel + $attr_opts{isa} = $from_attr->_isa_metadata; + my $default_code = "sub{ shift->${dm_name}->${reader} }"; + $attr_opts{default} = eval $default_code; + die "Could not generate default for attribute, code '$default_code' did not compile with: $@" if $@; + } + return \%attr_opts; +}; +sub reflect_source_action { + my($self, %opts) = @_; + my $name = delete $opts{name}; + my $base = delete $opts{base} || Action; + my $roles = delete $opts{roles} || []; + my $class = delete $opts{class}; + my $object = delete $opts{object_class}; + my $source = delete $opts{source_class}; + my $schema = delete $opts{schema}; + + confess("name, object_class and source_class are required arguments") + unless $source && $name && $object; + + my $attr_rules = delete $opts{attributes}; + $class ||= $object->_default_action_class_for($name); + + Class::MOP::load_class( $base ); + Class::MOP::load_class( $object ); + Class::MOP::load_class( $source ); + + #print STDERR "\n\t", ref $attr_rules eq 'ARRAY' ? @$attr_rules : $attr_rules,"\n"; + # attributes => undef, #default to qr/./ + # attributes => [], #default to nothing + # attributes => qr//, #DWIM, treated as [qr//] + # attributes => [{...}] #DWIM, treat as [qr/./, {...} ] + # attributes => [[-exclude => ...]] #DWIM, treat as [qr/./, [-exclude => ...]] + my $attr_haystack = [ map { $_->name } $object->parameter_attributes ]; + if(!defined $attr_rules){ + $attr_rules = [qr/./]; + } elsif( (!ref $attr_rules && $attr_rules) || (ref $attr_rules eq 'Regexp') ){ + $attr_rules = [ $attr_rules ]; + } elsif( ref $attr_rules eq 'ARRAY' && @$attr_rules){ + #don't add a qr/./ rule if we have at least one match rule + push(@$attr_rules, qr/./) unless + grep {(ref $_ eq 'ARRAY' && $_->[0] ne '-exclude') + || !ref $_ || ref $_ eq 'Regexp'} @$attr_rules; + } + + #print STDERR "${name}\t${class}\t${base}\n"; + #print STDERR "\t${object}\t${source}\n"; + #print STDERR "\t",@$attr_rules,"\n"; + + my $o_meta = $object->meta; + my $s_meta = $source->meta; + my $attributes = $self->parse_reflect_rules($attr_rules, $attr_haystack); + + #create the class + my $meta = $self->_load_or_create( + $class, + superclasses => [$base], + ( @$roles ? (roles => $roles) : ()), + ); + my $make_immutable = $meta->is_immutable || $self->make_classes_immutable; + $meta->make_mutable if $meta->is_immutable; + + for my $attr_name (keys %$attributes){ + my $attr_opts = $attributes->{$attr_name} || {}; + 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); + 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 + ( + schema => $schema, + object_class => $object, + source_class => $source, + attribute_name => $attr_name + ); + $meta->add_attribute( $attr_name => %$attr_params); + } + + $meta->make_immutable if $make_immutable; + return $meta; +}; +sub parameters_for_source_object_action_attribute { + my ($self, %opts) = @_; + + my $object = delete $opts{object_class}; + my $attr_name = delete $opts{attribute_name}; + my $source_class = delete $opts{source_class}; + my $schema = delete $opts{schema}; + my $source = $schema->source($source_class); + confess("object_class and attribute_name are required parameters") + unless $attr_name && $object; + + my $o_meta = $object->meta; + my $dm_name = $o_meta->find_attribute_by_name($attr_name)->domain_model; + $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; + + my %attr_opts = ( + 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}) { + if($from_attr->has_default) { + $attr_opts{lazy} = 1; + $attr_opts{default} = $from_attr->default; + } else { + $attr_opts{lazy_fail} = 1; } - return; - }; + } - @$haystack = grep { !$check_exclude->($_) } @$haystack; - $self->merge_reflect_rules(\@include, $needles, $haystack, $global_opts); - return $needles; - }; - implements merge_reflect_rules => as { - my ($self, $rules, $needles, $haystack, $local_opts) = @_; - for my $rule ( @$rules ){ - if(!ref $rule && ( grep {$rule eq $_} @$haystack ) ){ - $needles->{$rule} = defined $needles->{$rule} ? - $self->merge_hashes($needles->{$rule}, $local_opts) : $local_opts; - } elsif( ref $rule eq 'Regexp' ){ - for my $match ( grep { /$rule/ } @$haystack ){ - $needles->{$match} = defined $needles->{$match} ? - $self->merge_hashes($needles->{$match}, $local_opts) : $local_opts; - } - } elsif( ref $rule eq 'ARRAY' ){ - my $opts; - $opts = pop(@$rule) if @$rule > 1 and ref $rule->[$#$rule] eq 'HASH'; - $opts = $self->merge_hashes($local_opts, $opts) if defined $local_opts; - $self->merge_reflect_rules($rule, $needles, $haystack, $opts); - } - } - }; + my $m2m_meta; + if(my $coderef = $source_class->result_class->can('_m2m_metadata')){ + $m2m_meta = $source_class->result_class->$coderef; + } + #test for relationships + my $constraint_is_ArrayRef = + $from_attr->type_constraint->name eq 'ArrayRef' || + $from_attr->type_constraint->is_subtype_of('ArrayRef'); - implements reflect_schema => as { - my ($self, %opts) = @_; - my $base = delete $opts{base} || Object; - my $model = delete $opts{model_class}; - my $schema = delete $opts{schema_class}; - my $dm_name = delete $opts{domain_model_name}; - my $dm_args = delete $opts{domain_model_args} || {}; - $dm_name ||= $self->dm_name_from_class_name($schema); - - #load all necessary classes - confess("model_class and schema_class are required parameters") - 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 ]); - - # sources => undef, #default to qr/./ - # sources => [], #default to nothing - # sources => qr//, #DWIM, treated as [qr//] - # sources => [{...}] #DWIM, treat as [qr/./, {...} ] - # sources => [[-exclude => ...]] #DWIM, treat as [qr/./, [-exclude => ...]] - my $haystack = [ $schema->sources ]; - - my $rules = delete $opts{sources}; - if(!defined $rules){ - $rules = [qr/./]; - } elsif( ref $rules eq 'Regexp'){ - $rules = [ $rules ]; - } elsif( ref $rules eq 'ARRAY' && @$rules){ - #don't add a qr/./ rule if we have at least one match rule - push(@$rules, qr/./) unless grep {(ref $_ eq 'ARRAY' && $_->[0] ne '-exclude') - || !ref $_ || ref $_ eq 'Regexp'} @$rules; + if (my $rel_info = $source->relationship_info($attr_name)) { + my $rel_accessor = $rel_info->{attrs}->{accessor}; + + if($rel_accessor eq 'multi' && $constraint_is_ArrayRef) { + confess "${attr_name} is a rw has_many, this won't work."; + } elsif( $rel_accessor eq 'single' || $rel_accessor eq 'filter') { + $attr_opts{valid_values} = sub { + shift->target_model->result_source->related_source($attr_name)->resultset; + }; } + } elsif ( $constraint_is_ArrayRef && $attr_name =~ m/^(.*)_list$/) { + my $mm_name = $1; + my $link_table = "links_to_${mm_name}_list"; + $attr_opts{default} = sub { [] }; + $attr_opts{valid_values} = sub { + shift->target_model->result_source->related_source($link_table) + ->related_source($mm_name)->resultset; + }; + } elsif( $constraint_is_ArrayRef && defined $m2m_meta && exists $m2m_meta->{$attr_name} ){ + #m2m if using introspectable m2m component + my $rel = $m2m_meta->{$attr_name}->{relation}; + my $far_rel = $m2m_meta->{$attr_name}->{foreign_relation}; + $attr_opts{default} = sub { [] }; + $attr_opts{valid_values} = sub { + shift->target_model->result_source->related_source($rel) + ->related_source($far_rel)->resultset; + }; + } + #use Data::Dumper; + #print STDERR "\n" .$attr_name ." - ". $object . "\n"; + #print STDERR Dumper(\%attr_opts); + return \%attr_opts; +}; - my $sources = $self->parse_reflect_rules($rules, $haystack); +sub _load_or_create { + my ($self, $class, %options) = @_; + + if( $self->_maybe_load_class($class) ){ + return $class->meta; + } + my $base; + if( exists $options{superclasses} ){ + ($base) = @{ $options{superclasses} }; + } else { + $base = 'Reaction::InterfaceModel::Action'; + } + return $base->meta->create($class, %options); +} + +sub _maybe_load_class { + 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; +} + +__PACKAGE__->meta->make_immutable; - my $make_immutable = $meta->is_immutable; - $meta->make_mutable if $meta->is_immutable; - $meta->add_domain_model - ($dm_name, is => 'rw', isa => $schema, required => 1, %$dm_args); +1; - for my $source_name (keys %$sources){ - my $source_opts = $sources->{$source_name} || {}; - $self->reflect_source( - source_name => $source_name, - parent_class => $model, - schema_class => $schema, - source_class => $schema->class($source_name), - parent_domain_model_name => $dm_name, - %$source_opts - ); - } +#--------#---------#---------#---------#---------#---------#---------#---------# +__END__; - $meta->make_immutable if $make_immutable; - return $meta; - }; +=head1 NAME - implements _compute_source_options => as { - my ($self, %opts) = @_; - my $schema = delete $opts{schema_class}; - my $source_name = delete $opts{source_name}; - my $source_class = delete $opts{source_class}; - my $parent = delete $opts{parent_class}; - my $parent_dm = delete $opts{parent_domain_model_name}; - - #this is the part where I hate my life for promissing all sorts of DWIMery - confess("parent_class and source_name or source_class are required parameters") - unless($parent && ($source_name || $source_class)); - - OUTER: until( $schema && $source_name && $source_class && $parent_dm ){ - if( $schema && !$source_name){ - next OUTER if $source_name = $source_class->result_source_instance->source_name; - } elsif( $schema && !$source_class){ - next OUTER if $source_class = eval { $schema->class($source_name) }; - } +Reaction::InterfaceModel::Reflector::DBIC - +Automatically Generate InterfaceModels from DBIx::Class models - if($source_class && (!$schema || !$source_name)){ - if(!$schema){ - $schema = $source_class->result_source_instance->schema; - next OUTER if $schema && Class::MOP::load_class($schema); - } - if(!$source_name){ - $source_name = $source_class->result_source_instance->source_name; - next OUTER if $source_name; - } - } - my @haystack = $parent_dm ? - $parent->meta->find_attribute_by_name($parent_dm) : $parent->domain_models; - - #there's a lot of guessing going on, but it should work fine on most cases - INNER: for my $needle (@haystack){ - my $isa = $needle->_isa_metadata; - next INNER unless Class::MOP::load_class( $isa->_isa_metadata ); - next INNER unless $isa->isa('DBIx::Class::Schema'); - if(!$parent_dm && $schema && $isa eq $schema){ - $parent_dm = $needle->name; - next OUTER; - } +=head1 DESCRIPTION - if( $source_name ){ - my $src_class = eval{ $isa->class($source_name) }; - next INNER unless $src_class; - next INNER if($source_class && $source_class ne $src_class); - $schema = $isa; - $parent_dm = $needle->name; - $source_class = $src_class; - next OUTER; - } - } +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. - #do we even need to go this far? - if( !$parent_dm && $schema ){ - my $tentative = $self->dm_name_from_class_name($schema); - $parent_dm = $tentative if grep{$_->name eq $tentative} @haystack; - } +The default base class of all Object classes will be + L and the default Collection type will be +L. - confess("Could not determine options automatically from: schema " . - "'${schema}', source_name '${source_name}', source_class " . - "'${source_class}', parent_domain_model_name '${parent_dm}'"); - } +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. - return { - source_name => $source_name, - schema_class => $schema, - source_class => $source_class, - parent_class => $parent, - parent_domain_model_name => $parent_dm, - }; - }; +At this time, supported collection actions consist of: +=over 4 - implements add_source => as { - my ($self, %opts) = @_; +=item B<> L - my $model = delete $opts{model_class}; - my $reader = delete $opts{reader}; - my $source = delete $opts{source_name}; - my $dm_name = delete $opts{domain_model_name}; - my $collection = delete $opts{collection_class}; - my $name = delete $opts{attribute_name} || $source; +Creates a new item in the collection and underlying ResultSet. - confess("model_class and source_name are required parameters") - unless $model && $source; - my $meta = $model->meta; +=item B<> L - unless( $collection ){ - my $object = $self->class_name_from_source_name($model, $source); - $collection = $self->class_name_for_collection_of($object); - } - unless( $reader ){ - $reader = $source; - $reader =~ s/([a-z0-9])([A-Z])/${1}_${2}/g ; - $reader = lc($reader) . "_collection"; - } - unless( $dm_name ){ - my @haystack = $meta->domain_models; - if( @haystack > 1 ){ - @haystack = grep { $_->_isa_metadata->isa('DBIx::Class::Schema') } @haystack; - } - if(@haystack == 1){ - $dm_name = $haystack[0]->name; - } elsif(@haystack > 1){ - confess("Failed to automatically determine domain_model_name. More than one " . - "possible match (".(join ", ", map{"'".$_->name."'"} @haystack).")"); - } else { - confess("Failed to automatically determine domain_model_name. No matches."); - } - } +Deletes all the items in a collection and it's underlying resultset using +C - my %attr_opts = - ( - lazy => 1, - required => 1, - isa => $collection, - reader => $reader, - predicate => "has_${name}", - domain_model => $dm_name, - orig_attr_name => $source, - default => sub { - $collection->new(_source_resultset => shift->$dm_name->resultset($source)); - }, - ); +=back - my $make_immutable = $meta->is_immutable; - $meta->make_mutable if $make_immutable; - my $attr = $meta->add_attribute($name, %attr_opts); - $meta->make_immutable if $make_immutable; +And supported object actions are : - return $attr; - }; +=over 4 + +=item B - via L + +Updates an existing object. + +=item B - via L - implements reflect_source => as { - my ($self, %opts) = @_; - my $collection = delete $opts{collection} || {}; - %opts = %{ $self->merge_hashes(\%opts, $self->_compute_source_options(%opts)) }; +Deletes an existing object. - my $obj_meta = $self->reflect_source_object(%opts); - my $col_meta = $self->reflect_source_collection +=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 ( - object_class => $obj_meta->name, - source_class => $opts{source_class}, - %$collection + model_class => __PACKAGE__, + schema_class => 'MyApp::Schema', ); - $self->add_source( - model_class => $opts{parent_class}, - source_name => $opts{source_name}, - domain_model_name => $opts{parent_domain_model_name}, - collection_class => $col_meta->name, - ); - }; - - implements reflect_source_collection => as { - my ($self, %opts) = @_; - my $base = delete $opts{base} || ResultSet; - my $class = delete $opts{class}; - my $object = delete $opts{object_class}; - my $source = delete $opts{source_class}; - my $action_rules = delete $opts{actions}; - - confess('object_class and source_class are required parameters') - unless $object && $source; - $class ||= $self->class_name_for_collection_of($object); - - 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 $make_immutable = $meta->is_immutable; - $meta->make_mutable if $meta->is_immutable; - $meta->add_method(_build_im_class => 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 { - my $super = shift; - return { (target_model => $_[0]->_source_resultset), %{ $super->(@_) } }; - }; - $meta->add_around_method_modifier('_default_action_args_for', $def_act_args); - - - { - my $all_actions = $self->_all_collection_actions; - my $action_haystack = [keys %$all_actions]; - if(!defined $action_rules){ - $action_rules = $self->default_collection_actions; - } elsif( (!ref $action_rules && $action_rules) || (ref $action_rules eq 'Regexp') ){ - $action_rules = [ $action_rules ]; - } elsif( ref $action_rules eq 'ARRAY' && @$action_rules){ - #don't add a qr/./ rule if we have at least one match rule - push(@$action_rules, qr/./) - unless grep {(ref $_ eq 'ARRAY' && $_->[0] ne '-exclude') - || !ref $_ || ref $_ eq 'Regexp'} @$action_rules; - } +=head2 Selectively including and excluding sources - # XXX this is kind of a dirty hack to support custom actions that are not - # previously defined and still be able to use the parse_reflect_rules mechanism - my @custom_actions = grep {!exists $all_actions->{$_}} - map{ $_->[0] } grep {ref $_ eq 'ARRAY' && $_->[0] ne '-exclude'} @$action_rules; - push(@$action_haystack, @custom_actions); - my $actions = $self->parse_reflect_rules($action_rules, $action_haystack); - for my $action (keys %$actions){ - my $action_opts = $self->merge_hashes - ($all_actions->{$action} || {}, $actions->{$action} || {}); - $self->reflect_source_action( - name => $action, - object_class => $object, - source_class => $source, - %$action_opts, - ); - - # XXX i will move this to use the coercion method soon. this will be - # GoodEnough until then. I still need to think a little about the type coercion - # thing so i don't make a mess of it - my $act_args = sub { #override target model for this action - my $super = shift; - return { %{ $super->(@_) }, - ($_[1] eq $action ? (target_model => $_[0]->_source_resultset) : () ) }; - }; - $meta->add_around_method_modifier('_default_action_args_for', $act_args); - } - } - $meta->make_immutable if $make_immutable; - return $meta; - }; + #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/], + ); - implements reflect_source_object => as { - my($self, %opts) = @_; - %opts = %{ $self->merge_hashes(\%opts, $self->_compute_source_options(%opts)) }; + #reflect only the Foo family of sources + $reflector->reflect_schema + ( + model_class => __PACKAGE__, + schema_class => 'MyApp::Schema', + sources => qr/^Foo/, + ); - my $base = delete $opts{base} || Object; - my $class = delete $opts{class}; - my $dm_name = delete $opts{domain_model_name}; - my $dm_opts = delete $opts{domain_model_args} || {}; +=head2 Selectively including and excluding fields in sources - my $source_name = delete $opts{source_name}; - my $schema = delete $opts{schema_class}; - my $source_class = delete $opts{source_class}; - my $parent = delete $opts{parent_class}; - my $parent_dm = delete $opts{parent_domain_model_name}; + #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/] } ], + ], + ); - my $action_rules = delete $opts{actions}; - my $attr_rules = delete $opts{attributes}; +=head1 ATTRIBUTES - $class ||= $self->class_name_from_source_name($parent, $source_name); +=head2 make_classes_immutable - Class::MOP::load_class($parent); - Class::MOP::load_class($schema) if $schema; - Class::MOP::load_class($source_class); +=head2 object_actions - my $meta = eval { Class::MOP::load_class($class) } ? - $class->meta : $base->meta->create($class, superclasses => [ $base ]); +=head2 collection_actions - #create the domain model - $dm_name ||= $self->dm_name_from_source_name($source_name); +=head2 default_object_actions - $dm_opts->{isa} = $source_class; - $dm_opts->{is} ||= 'rw'; - $dm_opts->{required} ||= 1; +=head2 default_collection_actions - my $make_immutable = $meta->is_immutable; - $meta->make_mutable if $meta->is_immutable; +=head2 builtin_object_actions - my $dm_attr = $meta->add_domain_model($dm_name, %$dm_opts); - my $dm_reader = $dm_attr->get_read_method; +=head2 builtin_collection_actions - unless( $class->can('inflate_result') ){ - my $inflate_method = sub { - my $class = shift; my ($src) = @_; - $src = $src->resolve if $src->isa('DBIx::Class::ResultSourceHandle'); - $class->new($dm_name, $src->result_class->inflate_result(@_)); - }; - $meta->add_method('inflate_result', $inflate_method); - } +=head1 METHODS - #XXX this is here to allow action prototypes to work with ListView - # maybe Collections hsould have this kind of thing too to allow you to reconstruct them? - #i like the possibility to be honest... as aset of key/value pairs they could be URId - #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 - $meta->add_method('__ident_condition', sub {shift->$dm_reader->ident_condition} ) - unless $class->can('__ident_condition'); - - #XXX this is just a disaster - $meta->add_method('display_name', sub {shift->$dm_reader->display_name} ) - if( $source_class->can('display_name') && !$class->can('display_name')); - - #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 { - my $super = shift; - confess "no dm reader: $dm_reader on $_[0]" unless $_[0]->can($dm_reader); - return { (target_model => $_[0]->$dm_reader), %{ $super->(@_) } }; - }; - $meta->add_around_method_modifier('_default_action_args_for', $def_act_args); - - { - # attributes => undef, #default to qr/./ - # attributes => [], #default to nothing - # attributes => qr//, #DWIM, treated as [qr//] - # attributes => [{...}] #DWIM, treat as [qr/./, {...} ] - # attributes => [[-exclude => ...]] #DWIM, treat as [qr/./, [-exclude => ...]] - my $attr_haystack = - [ map { $_->name } $source_class->meta->compute_all_applicable_attributes ]; - - if(!defined $attr_rules){ - $attr_rules = [qr/./]; - } elsif( (!ref $attr_rules && $attr_rules) || (ref $attr_rules eq 'Regexp') ){ - $attr_rules = [ $attr_rules ]; - } elsif( ref $attr_rules eq 'ARRAY' && @$attr_rules){ - #don't add a qr/./ rule if we have at least one match rule - push(@$attr_rules, qr/./) unless - grep {(ref $_ eq 'ARRAY' && $_->[0] ne '-exclude') - || !ref $_ || ref $_ eq 'Regexp'} @$attr_rules; - } +=head2 new - my $attributes = $self->parse_reflect_rules($attr_rules, $attr_haystack); - for my $attr_name (keys %$attributes){ - $self->reflect_source_object_attribute( - class => $class, - source_class => $source_class, - parent_class => $parent, - attribute_name => $attr_name, - domain_model_name => $dm_name, - %{ $attributes->{$attr_name} || {}}, - ); - } - } +=head2 _all_object_actions - { - my $all_actions = $self->_all_object_actions; - my $action_haystack = [keys %$all_actions]; - if(!defined $action_rules){ - $action_rules = $self->default_object_actions; - } elsif( (!ref $action_rules && $action_rules) || (ref $action_rules eq 'Regexp') ){ - $action_rules = [ $action_rules ]; - } elsif( ref $action_rules eq 'ARRAY' && @$action_rules){ - #don't add a qr/./ rule if we have at least one match rule - push(@$action_rules, qr/./) - unless grep {(ref $_ eq 'ARRAY' && $_->[0] ne '-exclude') - || !ref $_ || ref $_ eq 'Regexp'} @$action_rules; - } +=head2 _all_collection_actions - # XXX this is kind of a dirty hack to support custom actions that are not - # previously defined and still be able to use the parse_reflect_rules mechanism - my @custom_actions = grep {!exists $all_actions->{$_}} map{ $_->[0] } - grep {ref $_ eq 'ARRAY' && $_->[0] ne '-exclude'} @$action_rules; - push(@$action_haystack, @custom_actions); - my $actions = $self->parse_reflect_rules($action_rules, $action_haystack); - for my $action (keys %$actions){ - my $action_opts = $self->merge_hashes - ($all_actions->{$action} || {}, $actions->{$action} || {}); - $self->reflect_source_action( - name => $action, - object_class => $class, - source_class => $source_class, - %$action_opts, - ); - - # XXX i will move this to use the coercion method soon. this will be - # GoodEnough until then. I still need to think a little about the type coercion - # thing so i don't make a mess of it - my $act_args = sub { #override target model for this action - my $super = shift; - confess "no dm reader: $dm_reader on $_[0]" unless $_[0]->can($dm_reader); - return { %{ $super->(@_) }, - ($_[1] eq $action ? (target_model => $_[0]->$dm_reader) : () ) }; - }; - $meta->add_around_method_modifier('_default_action_args_for', $act_args); - } - } +=head2 dm_name_from_class_name - $meta->make_immutable if $make_immutable; - return $meta; - }; +=head2 dm_name_from_source_name - # needs class, attribute_name domain_model_name - implements reflect_source_object_attribute => as { - my ($self, %opts) = @_; - unless( $opts{attribute_name} && $opts{class} && $opts{parent_class} - && ( $opts{source_class} || $opts{domain_model_name} ) ){ - confess( "Error: class, parent_class, attribute_name, and either " . - "domain_model_name or source_class are required parameters" ); - } +=head2 class_name_from_source_name - my $meta = $opts{class}->meta; - my $attr_opts = $self->parameters_for_source_object_attribute(%opts); +=head2 class_name_for_collection_of - my $make_immutable = $meta->is_immutable; - $meta->make_mutable if $meta->is_immutable; +=head2 merge_hashes - my $attr = $meta->add_attribute($opts{attribute_name}, %$attr_opts); +=head2 parse_reflect_rules - $meta->make_immutable if $make_immutable; - return $attr; - }; +=head2 merge_reflect_rules - # needs class, attribute_name domain_model_name - implements parameters_for_source_object_attribute => as { - my ($self, %opts) = @_; - - my $class = delete $opts{class}; - my $attr_name = delete $opts{attribute_name}; - my $dm_name = delete $opts{domain_model_name}; - my $source_class = delete $opts{source_class}; - my $parent_class = delete $opts{parent_class}; - confess("parent_class is a required argument") unless $parent_class; - confess("You must supply at least one of domain_model_name and source_class") - unless $dm_name || $source_class; - - my $source; - $source = $source_class->result_source_instance if $source_class; - #puke! dwimery - if( !$source_class ){ - my $dm = $class->meta->find_attribute_by_name($dm_name); - $source_class = $dm->_isa_metadata; - $source = $source_class->result_source_instance; - } elsif( !$dm_name ){ - ($dm_name) = map{$_->name} grep{$_->_isa_metadata eq $source_class} - $class->meta->domain_models; - if( !$dm_name ){ #last resort guess - my $tentative = $self->dm_name_from_source_name($source->source_name); - ($dm_name) = $tentative if grep{$_->name eq $tentative} $class->domain_models; - } - } +=head2 reflect_schema - my $from_attr = $source_class->meta->find_attribute_by_name($attr_name); - - #default options. lazy build but no outsider method - my %attr_opts = ( is => 'ro', lazy => 1, required => 1, - clearer => "_clear_${attr_name}", - predicate => "has_${attr_name}", - domain_model => $dm_name, - orig_attr_name => $attr_name, - ); - - #m2m / has_many - my $constraint_is_ArrayRef = - $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; - - if($rel_accessor eq 'multi' && $constraint_is_ArrayRef) { - #has_many - my $sm = $self->class_name_from_source_name($parent_class, $rel_moniker); - #type constraint is a collection, and default builds it - $attr_opts{isa} = $self->class_name_for_collection_of($sm); - $attr_opts{default} = sub { - my $rs = shift->$dm_name->related_resultset($attr_name); - return $attr_opts{isa}->new(_source_resultset => $rs); - }; - } elsif( $rel_accessor eq 'single') { - #belongs_to - #type constraint is the foreign IM object, default inflates it - $attr_opts{isa} = $self->class_name_from_source_name($parent_class, $rel_moniker); - $attr_opts{default} = sub { - shift->$dm_name - ->find_related($attr_name, {},{result_class => $attr_opts{isa}}); - }; - } - } elsif( $constraint_is_ArrayRef && $attr_name =~ m/^(.*)_list$/ ) { - #m2m magic - my $mm_name = $1; - my $link_table = "links_to_${mm_name}_list"; - my ($hm_source, $far_side); - eval { $hm_source = $source->related_source($link_table); } - || confess "Can't find ${link_table} has_many for ${mm_name}_list"; - eval { $far_side = $hm_source->related_source($mm_name); } - || confess "Can't find ${mm_name} belongs_to on ".$hm_source->result_class - ." traversing many-many for ${mm_name}_list"; - - my $sm = $self->class_name_from_source_name($parent_class,$far_side->source_name); - $attr_opts{isa} = $self->class_name_for_collection_of($sm); - - #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; - return $attr_opts{isa}->new(_source_resultset => $rs); - }; - } else { - #no rel - my $reader = $from_attr->get_read_method; - $attr_opts{isa} = $from_attr->_isa_metadata; - $attr_opts{default} = sub{ shift->$dm_name->$reader }; - } - return \%attr_opts; - }; +=head2 _compute_source_options +=head2 add_source - implements reflect_source_action => as{ - my($self, %opts) = @_; - my $name = delete $opts{name}; - my $class = delete $opts{class}; - my $base = delete $opts{base} || Action; - my $object = delete $opts{object_class}; - my $source = delete $opts{source_class}; +=head2 reflect_source - confess("name, object_class and source_class are required arguments") - unless $source && $name && $object; +=head2 reflect_source_collection - my $attr_rules = delete $opts{attributes}; - $class ||= $object->_default_action_class_for($name); +=head2 reflect_source_object - Class::MOP::load_class( $base ); - Class::MOP::load_class( $object ); - Class::MOP::load_class( $source ); +=head2 reflect_source_object_attribute - #print STDERR "\n\t", ref $attr_rules eq 'ARRAY' ? @$attr_rules : $attr_rules,"\n"; - # attributes => undef, #default to qr/./ - # attributes => [], #default to nothing - # 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 ]; - if(!defined $attr_rules){ - $attr_rules = [qr/./]; - } elsif( (!ref $attr_rules && $attr_rules) || (ref $attr_rules eq 'Regexp') ){ - $attr_rules = [ $attr_rules ]; - } elsif( ref $attr_rules eq 'ARRAY' && @$attr_rules){ - #don't add a qr/./ rule if we have at least one match rule - push(@$attr_rules, qr/./) unless - grep {(ref $_ eq 'ARRAY' && $_->[0] ne '-exclude') - || !ref $_ || ref $_ eq 'Regexp'} @$attr_rules; - } +=head2 parameters_for_source_object_attribute - #print STDERR "${name}\t${class}\t${base}\n"; - #print STDERR "\t${object}\t${source}\n"; - #print STDERR "\t",@$attr_rules,"\n"; +=head2 reflect_source_action - my $o_meta = $object->meta; - my $s_meta = $source->meta; - my $attributes = $self->parse_reflect_rules($attr_rules, $attr_haystack); +=head2 parameters_for_source_object_action_attribute - #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; - $meta->make_mutable if $meta->is_immutable; +=head1 TODO - for my $attr_name (keys %$attributes){ - my $attr_opts = $attributes->{$attr_name} || {}; - 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! - - my $attr_params = $self->parameters_for_source_object_action_attribute - ( - object_class => $object, - source_class => $source, - attribute_name => $attr_name - ); - $meta->add_attribute( $attr_name => %$attr_params); - } +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. - $meta->make_immutable if $make_immutable; - return $meta; - }; +=head1 AUTHORS - implements parameters_for_source_object_action_attribute => as { - my ($self, %opts) = @_; - - my $object = delete $opts{object_class}; - my $attr_name = delete $opts{attribute_name}; - my $source_class = delete $opts{source_class}; - confess("object_class and attribute_name are required parameters") - unless $attr_name && $object; - - my $o_meta = $object->meta; - my $dm_name = $o_meta->find_attribute_by_name($attr_name)->domain_model; - $source_class ||= $o_meta->find_attribute_by_name($dm_name)->_isa_metadata; - my $from_attr = $source_class->meta->find_attribute_by_name($attr_name); - - confess("${attr_name} is not writeable and can not be reflected") - unless $from_attr->get_write_method; - - my %attr_opts = ( - is => 'rw', - isa => $from_attr->_isa_metadata, - required => $from_attr->is_required, - 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")}; - } +See L for authors. - #test for relationships - my $constraint_is_ArrayRef = - $from_attr->type_constraint->name eq 'ArrayRef' || - $from_attr->type_constraint->is_subtype_of('ArrayRef'); - - my $source = $source_class->result_source_instance; - if (my $rel_info = $source->relationship_info($attr_name)) { - my $rel_accessor = $rel_info->{attrs}->{accessor}; - - if($rel_accessor eq 'multi' && $constraint_is_ArrayRef) { - confess "${attr_name} is a rw has_many, this won't work."; - } elsif( $rel_accessor eq 'single') { - $attr_opts{valid_values} = sub { - shift->target_model->result_source->related_source($attr_name)->resultset; - }; - } - } elsif ( $constraint_is_ArrayRef && $attr_name =~ m/^(.*)_list$/) { - my $mm_name = $1; - my $link_table = "links_to_${mm_name}_list"; - my ($hm_source, $far_side); - eval { $hm_source = $source->related_source($link_table); } - || confess "Can't find ${link_table} has_many for ${mm_name}_list"; - eval { $far_side = $hm_source->related_source($mm_name); } - || confess "Can't find ${mm_name} belongs_to on ".$hm_source->result_class - ." traversing many-many for ${mm_name}_list"; - - $attr_opts{default} = sub { [] }; - $attr_opts{valid_values} = sub { - shift->target_model->result_source->related_source($link_table) - ->related_source($mm_name)->resultset; - }; - } - #use Data::Dumper; - #print STDERR Dumper(\%attr_opts); - return \%attr_opts; - }; +=head1 LICENSE -}; +See L for the license. -1; +=cut