TODO tests for loading required attributes from config
[gitmo/MooseX-Getopt.git] / lib / MooseX / Getopt.pm
index e6e07a1..a8ce4a3 100644 (file)
 package MooseX::Getopt;
 use Moose::Role;
 
-use Getopt::Long;
-
 use MooseX::Getopt::OptionTypeMap;
 use MooseX::Getopt::Meta::Attribute;
+use MooseX::Getopt::Meta::Attribute::NoGetopt;
+
+use Getopt::Long (); # GLD uses it anyway, doesn't hurt
+use constant HAVE_GLD => not not eval { require Getopt::Long::Descriptive };
 
-our $VERSION   = '0.01';
+our $VERSION   = '0.12';
 our $AUTHORITY = 'cpan:STEVAN';
 
+has ARGV       => (is => 'rw', isa => 'ArrayRef', metaclass => "NoGetopt");
+has extra_argv => (is => 'rw', isa => 'ArrayRef', metaclass => "NoGetopt");
+
 sub new_with_options {
-    my ($class, %params) = @_;
-
-    my (@options, %name_to_init_arg);
-    foreach my $attr ($class->meta->compute_all_applicable_attributes) {
-        my $name = $attr->name;
-        
-        if ($attr->isa('MooseX::Getopt::Meta::Attribute') && $attr->has_cmd_flag) { 
-            $name = $attr->cmd_flag;
-        }          
-        
-        $name_to_init_arg{$name} = $attr->init_arg;        
-        
-        if ($attr->has_type_constraint) {
-            my $type_name = $attr->type_constraint->name;
-            if (MooseX::Getopt::OptionTypeMap->has_option_type($type_name)) {                   
-                $name .= MooseX::Getopt::OptionTypeMap->get_option_type($type_name);
-            }
+    my ($class, @params) = @_;
+
+    my %processed = $class->_parse_argv( 
+        options => [ 
+            $class->_attrs_to_options( @params ) 
+        ] 
+    );
+
+    my $params = $processed{params};
+
+    if($class->meta->does_role('MooseX::ConfigFromFile')) {
+        my $configfile;
+
+        if(defined $params->{configfile}) {
+            $configfile = $params->{configfile}
+        }
+        else {
+            my $cfmeta = $class->meta->get_attribute('configfile');
+            $configfile = $cfmeta->default if $cfmeta->has_default;
+        }
+
+        if(defined $configfile) {
+            %$params = (
+                %{$class->get_config_from_file($configfile)},
+                %$params,
+            );
         }
-        
-        push @options => $name;
     }
 
-    my %options;
-    
-    GetOptions(\%options, @options);
-    
-    #use Data::Dumper;
-    #warn Dumper \@options;
-    #warn Dumper \%name_to_init_arg;
-    #warn Dumper \%options;
-    
     $class->new(
-        %params, 
+        ARGV       => $processed{argv_copy},
+        extra_argv => $processed{argv},
+        @params, # explicit params to ->new
+        %$params, # params from CLI
+    );
+}
+
+sub _parse_argv {
+    my ( $class, %params ) = @_;
+
+    local @ARGV = @{ $params{argv} || \@ARGV };
+
+    my ( $opt_spec, $name_to_init_arg ) = ( HAVE_GLD ? $class->_gld_spec(%params) : $class->_traditional_spec(%params) );
+
+    # Get a clean copy of the original @ARGV
+    my $argv_copy = [ @ARGV ];
+
+    my @err;
+
+    my ( $parsed_options, $usage ) = eval {
+        local $SIG{__WARN__} = sub { push @err, @_ };
+
+        if ( HAVE_GLD ) {
+            return Getopt::Long::Descriptive::describe_options($class->_usage_format(%params), @$opt_spec);
+        } else {
+            my %options;
+            Getopt::Long::GetOptions(\%options, @$opt_spec);
+            return ( \%options, undef );
+        }
+    };
+
+    die join "", grep { defined } @err, $@ if @err or $@;
+
+    # Get a copy of the Getopt::Long-mangled @ARGV
+    my $argv_mangled = [ @ARGV ];
+
+    my %constructor_args = (
         map { 
-            $name_to_init_arg{$_} => $options{$_} 
-        } keys %options
+            $name_to_init_arg->{$_} => $parsed_options->{$_} 
+        } keys %$parsed_options,   
+    );
+
+    return (
+        params    => \%constructor_args,
+        argv_copy => $argv_copy,
+        argv      => $argv_mangled,
+        ( defined($usage) ? ( usage => $usage ) : () ),
     );
 }
 
+sub _usage_format {
+    return "usage: %c %o";
+}
+
+sub _traditional_spec {
+    my ( $class, %params ) = @_;
+    
+    my ( @options, %name_to_init_arg, %options );
+
+    foreach my $opt ( @{ $params{options} } ) {
+        push @options, $opt->{opt_string};
+
+        my $identifier = $opt->{name};
+        $identifier =~ s/\W/_/g; # Getopt::Long does this to all option names
+
+        $name_to_init_arg{$identifier} = $opt->{init_arg};
+    }
+
+    return ( \@options, \%name_to_init_arg );
+}
+
+sub _gld_spec {
+    my ( $class, %params ) = @_;
+
+    my ( @options, %name_to_init_arg );
+
+    foreach my $opt ( @{ $params{options} } ) {
+        push @options, [
+            $opt->{opt_string},
+            $opt->{doc} || ' ', # FIXME new GLD shouldn't need this hack
+            {
+                ( $opt->{required} ? (required => $opt->{required}) : () ),
+                ( exists $opt->{default}  ? (default  => $opt->{default})  : () ),
+            },
+        ];
+
+        my $identifier = $opt->{name};
+        $identifier =~ s/\W/_/g; # Getopt::Long does this to all option names
+
+        $name_to_init_arg{$identifier} = $opt->{init_arg};
+    }
+
+    return ( \@options, \%name_to_init_arg );
+}
+
+sub _compute_getopt_attrs {
+    my $class = shift;
+    grep {
+        $_->does("MooseX::Getopt::Meta::Attribute::Trait")
+            or
+        $_->name !~ /^_/
+    } grep {
+        !$_->does('MooseX::Getopt::Meta::Attribute::Trait::NoGetopt')
+    } $class->meta->compute_all_applicable_attributes
+}
+
+sub _get_cmd_flags_for_attr {
+    my ( $class, $attr ) = @_;
+
+    my $flag = $attr->name;
+
+    my @aliases;
+
+    if ($attr->does('MooseX::Getopt::Meta::Attribute::Trait')) {
+        $flag = $attr->cmd_flag if $attr->has_cmd_flag;
+        @aliases = @{ $attr->cmd_aliases } if $attr->has_cmd_aliases;
+    }
+
+    return ( $flag, @aliases );
+}
+
+sub _attrs_to_options {
+    my $class = shift;
+
+    my @options;
+
+    foreach my $attr ($class->_compute_getopt_attrs) {
+        my ( $flag, @aliases ) = $class->_get_cmd_flags_for_attr($attr);
+
+        my $opt_string = join(q{|}, $flag, @aliases);
+
+        if ($attr->has_type_constraint) {
+            my $type_name = $attr->type_constraint->name;
+            if (MooseX::Getopt::OptionTypeMap->has_option_type($type_name)) {
+                $opt_string .= MooseX::Getopt::OptionTypeMap->get_option_type($type_name)
+            }
+        }
+
+        push @options, {
+            name       => $flag,
+            init_arg   => $attr->init_arg,
+            opt_string => $opt_string,
+            required   => $attr->is_required && !$attr->has_default && !$attr->has_builder,
+            ( ( $attr->has_default && ( $attr->is_default_a_coderef xor $attr->is_lazy ) ) ? ( default => $attr->default({}) ) : () ),
+            ( $attr->has_documentation ? ( doc => $attr->documentation ) : () ),
+        }
+    }
+
+    return @options;
+}
+
 no Moose::Role; 1;
 
 __END__
@@ -93,7 +240,37 @@ This module attempts to DWIM as much as possible with the command line
 params by introspecting your class's attributes. It will use the name 
 of your attribute as the command line option, and if there is a type 
 constraint defined, it will configure Getopt::Long to handle the option
-accordingly. 
+accordingly.
+
+You can use the attribute metaclass L<MooseX::Getopt::Meta::Attribute>
+to get non-default commandline option names and aliases.
+
+You can use the attribute metaclass L<MooseX::Getopt::Meta::Attribute::NoGetOpt>
+to have C<MooseX::Getopt> ignore your attribute in the commandline options.
+
+By default, attributes which start with an underscore are not given
+commandline argument support, unless the attribute's metaclass is set
+to L<MooseX::Getopt::Meta::Attribute>. If you don't want you accessors
+to have the leading underscore in thier name, you can do this:
+
+  # for read/write attributes
+  has '_foo' => (accessor => 'foo', ...);
+  
+  # or for read-only attributes
+  has '_bar' => (reader => 'bar', ...);  
+
+This will mean that Getopt will not handle a --foo param, but your 
+code can still call the C<foo> method. 
+
+If your class also uses a configfile-loading role based on
+L<MooseX::ConfigFromFile>, such as L<MooseX::SimpleConfig>,
+L<MooseX::Getopt>'s C<new_with_options> will load the configfile
+specified by the C<--configfile> option (or the default you've
+given for the configfile attribute) for you.
+
+Options specified in multiple places follow the following
+precendence order: commandline overrides configfile, which
+overrides explicit new_with_options parameters.
 
 =head2 Supported Type Constraints
 
@@ -187,6 +364,20 @@ the type constraint validations with the Getopt::Long validations.
 
 Better examples are certainly welcome :)
 
+=head2 Inferred Type Constraints
+
+If you define a custom subtype which is a subtype of one of the
+standard L</Supported Type Constraints> above, and do not explicitly
+provide custom support as in L</Custom Type Constraints> above,
+MooseX::Getopt will treat it like the parent type for Getopt
+purposes.
+
+For example, if you had the same custom C<ArrayOfInts> subtype
+from the examples above, but did not add a new custom option
+type for it to the C<OptionTypeMap>, it would be treated just
+like a normal C<ArrayRef> type for Getopt purposes (that is,
+C<=s@>).
+
 =head1 METHODS
 
 =over 4
@@ -197,6 +388,23 @@ This method will take a set of default C<%params> and then collect
 params from the command line (possibly overriding those in C<%params>)
 and then return a newly constructed object.
 
+If L<Getopt::Long/GetOptions> fails (due to invalid arguments),
+C<new_with_options> will throw an exception.
+
+If you have L<Getopt::Long::Descriptive> a the C<usage> param is also passed to
+C<new>.
+
+=item B<ARGV>
+
+This accessor contains a reference to a copy of the C<@ARGV> array
+as it originally existed at the time of C<new_with_options>.
+
+=item B<extra_argv>
+
+This accessor contains an arrayref of leftover C<@ARGV> elements that
+L<Getopt::Long> did not parse.  Note that the real C<@ARGV> is left
+un-mangled.
+
 =item B<meta>
 
 This returns the role meta object.
@@ -213,9 +421,11 @@ to cpan-RT.
 
 Stevan Little E<lt>stevan@iinteractive.comE<gt>
 
+Brandon L. Black, E<lt>blblack@gmail.comE<gt>
+
 =head1 COPYRIGHT AND LICENSE
 
-Copyright 2007 by Infinity Interactive, Inc.
+Copyright 2007-2008 by Infinity Interactive, Inc.
 
 L<http://www.iinteractive.com>