added support for:
Joel Bernstein [Wed, 21 Feb 2007 22:24:17 +0000 (22:24 +0000)]
'force_plugins => [ qw(Config::Any::Foo Config::Any::Blah) ]' parameter to load_(files|stems)
$Config::Any::INI::MAP_SECTION_SPACE_TO_NESTED_KEY - boolean, defaulting to on, controlling whether to map spaces
in INI section headings to nested hashrefs

lib/Config/Any.pm
lib/Config/Any/INI.pm
t/20-parse.t
t/51-ini.t
t/61-features.t [new file with mode: 0644]
t/conf/conf.foo [new file with mode: 0644]

index d097776..7eaae8a 100644 (file)
@@ -6,7 +6,7 @@ use Carp;
 use Module::Pluggable::Object ();
 use English qw(-no_match_vars);
 
-our $VERSION = '0.04';
+our $VERSION = '0.05';
 
 =head1 NAME
 
@@ -14,7 +14,7 @@ Config::Any - Load configuration from different file formats, transparently
 
 =head1 VERSION
 
-This document describes Config::Any version 0.0.4
+This document describes Config::Any version 0.0.5
 
 =head1 SYNOPSIS
 
@@ -69,6 +69,11 @@ be aware that you will lose flexibility -- for example, a file called C<myapp.cf
 containing YAML data will not be offered to the YAML plugin, whereas C<myapp.yml>
 or C<myapp.yaml> would be.
 
+C<load_files()> also supports a 'force_plugins' parameter, whose value should be an
+arrayref of plugin names like C<Config::Any::INI>. Its intended use is to allow the use 
+of a non-standard file extension while forcing it to be offered to a particular parser.
+It is not compatible with 'use_ext'. 
+
 =cut
 
 sub load_files {
@@ -79,10 +84,10 @@ sub load_files {
         return;
     }
 
-    my $files = [ grep { -f $_ } @{$args->{files}} ];
-    my $filter_cb = delete $args->{filter};
-    my $use_ext   = delete $args->{use_ext};
-    return $class->_load($files, $filter_cb, $use_ext);
+    my %load_args = map { $_ => defined $args->{$_} ? $args->{$_} : undef } 
+        qw(filter use_ext force_plugins);
+    $load_args{files} = [ grep { -f $_ } @{$args->{files}} ];
+    return $class->_load(\%load_args);
 }
 
 =head2 load_stems( )
@@ -108,9 +113,18 @@ sub load_stems {
         return;
     }
         
-    my $filter_cb = delete $args->{filter};
-    my $use_ext   = delete $args->{use_ext};
-    my $stems = $args->{stems};
+    my %load_args = map { $_ => defined $args->{$_} ? $args->{$_} : undef } 
+        qw(filter use_ext force_plugins);
+
+    my $filenames = $class->_stems_to_files($args->{stems});
+    $load_args{files} = [ grep { -f $_ } @{$filenames} ];
+    return $class->_load(\%load_args);
+}
+
+sub _stems_to_files {
+    my ($class, $stems) = @_;
+    return unless defined $stems;
+
     my @files;
     STEM:
     for my $s (@$stems) {
@@ -122,37 +136,46 @@ sub load_stems {
             last EXT;
         }
     }
-    return $class->_load(\@files, $filter_cb, $use_ext);
+    \@files;
 }
 
+sub _maphash (@) { map { $_ => 1 } @_ } # sugar
+
 # this is where we do the real work
 # it's a private class-method because users should use the interface described
 # in the POD.
 sub _load {
-    my ($class, $files_ref, $filter_cb, $use_ext) = @_;
+    my ($class, $args) = @_;
+    my ($files_ref, $filter_cb, $use_ext, $force_plugins_ref) = 
+        @{$args}{qw(files filter use_ext force_plugins)};
     croak "_load requires a arrayref of file paths" unless defined $files_ref;
 
+       my %files           = _maphash @$files_ref;
+    my %force_plugins   = _maphash @$force_plugins_ref;
+    my $enforcing       = keys %force_plugins ? 1 : 0;
+
     my $final_configs       = [];
     my $originally_loaded   = {};
-       my %files = map { $_ => 1 } @$files_ref;
 
+    # perform a separate file loop for each loader
     for my $loader ( $class->plugins ) {
+        next if $enforcing && not defined $force_plugins{$loader};
                last unless keys %files;
-#      warn "loader: $loader\n";
-        my %ext = map { $_ => 1 } $loader->extensions;
+        my %ext = _maphash $loader->extensions;
+
         FILE:
         for my $filename (keys %files) {
-            if (defined $use_ext) {
-#                      warn "using file extension to decide which loader to use for file $filename\n";
+            # use file extension to decide whether this loader should try this file
+            # use_ext => 1 hits this block
+            if (defined $use_ext && !$enforcing) {
                                my $matched_ext = 0;
                 EXT:
                 for my $e (keys %ext) {
-#                              warn "trying ext $e\n";
                     next EXT  unless $filename =~ m{ \. $e \z }xms; 
-#                                      warn "filename $filename matched extension $e\n";
                     next FILE unless exists $ext{$e};
                                        $matched_ext = 1;
                 }
+
                                next FILE unless $matched_ext;
             }
 
@@ -160,11 +183,14 @@ sub _load {
                        eval {
                                $config = $loader->load( $filename );
                        };
-                       next if $EVAL_ERROR;
+
+                       next if $EVAL_ERROR; # if it croaked or warned, we can't use it
             next if !$config;
                        delete $files{$filename};
-#                      warn "loader $loader loaded file $filename\n";
+
+            # post-process config with a filter callback, if we got one
             $filter_cb->( $config ) if defined $filter_cb;
+
             push @$final_configs, { $filename => $config };
         }
     }
@@ -273,9 +299,12 @@ module by Brian Cassidy C<< <bricas@cpan.org> >>.
 
 With ideas and support from Matt S Trout C<< <mst@shadowcatsystems.co.uk> >>.
 
+Further enhancements suggested by Evan Kaufman C<< <evank@cpan.org> >>.
+
 =head1 LICENCE AND COPYRIGHT
 
 Copyright (c) 2006, Portugal Telecom C<< http://www.sapo.pt/ >>. All rights reserved.
+Portions copyright 2007, Joel Bernstein C<< <rataxis@cpan.org> >>.
 
 This module is free software; you can redistribute it and/or
 modify it under the same terms as Perl itself. See L<perlartistic>.
@@ -310,5 +339,4 @@ L<Catalyst::Plugin::ConfigLoader|Catalyst::Plugin::ConfigLoader>
 
 =cut
 
-1; # Magic true value required at end of module
-
+"Drink more beer";
index a091ba9..3b92c5b 100644 (file)
@@ -3,6 +3,8 @@ package Config::Any::INI;
 use strict;\r
 use warnings;\r
 \r
+our $MAP_SECTION_SPACE_TO_NESTED_KEY = 1;\r
+\r
 =head1 NAME\r
 \r
 Config::Any::INI - Load INI config files\r
@@ -49,7 +51,7 @@ sub load {
        $out->{$_} = $main->{$_} for keys %$main;\r
 \r
        for my $k (keys %$config) {\r
-               my @keys = split /\s+/, $k;\r
+               my @keys = split /\s+/, $k if $MAP_SECTION_SPACE_TO_NESTED_KEY;\r
                my $ref = $config->{$k};\r
 \r
                if (@keys > 1) {\r
@@ -62,6 +64,25 @@ sub load {
     return $out;\r
 }\r
 \r
+=head1 PACKAGE VARIABLES\r
+\r
+=over 4\r
+\r
+=item $MAP_SECTION_SPACE_TO_NESTED_KEY (boolean)\r
+\r
+This variable controls whether spaces in INI section headings will be expanded into nested hash keys.\r
+e.g. it controls whether [Full Power] maps to $config->{'Full Power'} or $config->{'Full'}->{'Power'}\r
+\r
+By default it is set to 1 (i.e. true). \r
+\r
+Set it to 0 to preserve literal spaces in section headings:\r
+\r
+    use Config::Any;\r
+    use Config::Any::INI;\r
+    $Config::Any::INI::MAP_SECTION_SPACE_TO_NESTED_KEY = 0;\r
+\r
+=back\r
+\r
 =head1 AUTHOR\r
 \r
 =over 4 \r
@@ -74,7 +95,7 @@ sub load {
 \r
 =head1 COPYRIGHT AND LICENSE\r
 \r
-Copyright 2006 by Brian Cassidy\r
+Copyright 2006 by Brian Cassidy, portions copyright 2006, 2007 by Joel Bernstein\r
 \r
 This library is free software; you can redistribute it and/or modify\r
 it under the same terms as Perl itself. \r
index db7ccde..420b581 100644 (file)
@@ -1,5 +1,8 @@
 package MockApp;\r
+use strict;\r
+use warnings;\r
 \r
+$|++;\r
 use Test::More tests => 54;\r
 use Scalar::Util qw(blessed reftype);\r
 use Config::Any;\r
@@ -11,7 +14,7 @@ use Config::Any::XML;
 use Config::Any::YAML;\r
 \r
 \r
-my %ext_map = (\r
+our %ext_map = (\r
        conf => 'Config::Any::General',\r
        ini  => 'Config::Any::INI',\r
        json => 'Config::Any::JSON',\r
@@ -20,20 +23,24 @@ my %ext_map = (
        yml  => 'Config::Any::YAML'\r
 );\r
 \r
-my @files = map { "t/conf/$_" } \r
-       qw(conf.conf conf.ini conf.json conf.pl conf.xml conf.yml);\r
+sub load_parser_for {\r
+    my $f = shift;\r
+    return unless $f;\r
 \r
-for my $f (@files) {\r
        my ($ext) = $f =~ m{ \. ( [^\.]+ ) \z }xms;\r
        my $mod = $ext_map{$ext};\r
        my $mod_load_result;\r
        eval { $mod_load_result = $mod->load( $f ); delete $INC{$f} if $ext eq 'pl' };\r
+    return $@ ? (1,$mod) : (0,$mod);\r
+}\r
+\r
+for my $f (map { "t/conf/conf.$_" } keys %ext_map) {\r
+    my ($skip,$mod) = load_parser_for($f);\r
        SKIP: {\r
-               my $skip = !!$@;\r
                skip "File loading backend for $mod not found", 9 if $skip;\r
        \r
                ok(my $c_arr = Config::Any->load_files({files=>[$f], use_ext=>1}), \r
-                       "load_files with use_ext works");\r
+                       "load_files with use_ext works [$f]");\r
                ok(my $c = $c_arr->[0], "load_files returns an arrayref");\r
                \r
                ok(ref $c, "load_files arrayref contains a ref");\r
index 17b5bcd..dbeadc5 100644 (file)
@@ -1,17 +1,26 @@
-use Test::More tests => 6;\r
-\r
-use Config::Any::INI;\r
-\r
-my $config =       eval { Config::Any::INI->load( 't/conf/conf.ini' ) };\r
-my $simpleconfig = eval { Config::Any::INI->load( 't/conf/conf2.ini' ) };\r
-\r
-SKIP: {\r
-    skip "Couldn't Load INI plugin", 6 if $@;\r
-    ok( $config );\r
-    is( $config->{ name }, 'TestApp' );\r
-       is( $config->{Component}->{Controller::Foo}->{foo}, 'bar');\r
-       \r
-       ok( $simpleconfig );\r
-    is( $simpleconfig->{ name }, 'TestApp' );\r
-    is( $simpleconfig->{Controller::Foo}->{foo}, 'bar' );\r
-}\r
+use Test::More tests => 9;
+
+use Config::Any::INI;
+
+my $config =       eval { Config::Any::INI->load( 't/conf/conf.ini' ) };
+my $simpleconfig = eval { Config::Any::INI->load( 't/conf/conf2.ini' ) };
+
+SKIP: {
+    skip "Couldn't Load INI plugin", 6 if $@;
+    ok( $config, "loaded INI config #1" );
+    is( $config->{ name }, 'TestApp', "toplevel key lookup succeeded" );
+    is( $config->{Component}->{Controller::Foo}->{foo}, 'bar', "nested hashref hack lookup succeeded");
+    
+    ok( $simpleconfig, "loaded INI config #1" );
+    is( $simpleconfig->{ name }, 'TestApp', "toplevel key lookup succeeded" );
+    is( $simpleconfig->{Controller::Foo}->{foo}, 'bar', "nested hashref hack lookup succeeded" );
+}
+
+$Config::Any::INI::MAP_SECTION_SPACE_TO_NESTED_KEY = 0;
+my $unspaced_config = eval { Config::Any::INI->load( 't/conf/conf.ini' ); };
+SKIP: {
+    skip "Couldn't load INI plugin", 3 if $@;
+    ok( $unspaced_config, "loaded INI config #1 in no-map-space mode" );
+    is( $unspaced_config->{name}, 'TestApp', "toplevel key lookup succeeded" );
+    is( $unspaced_config->{'Component Controller::Foo'}->{foo}, 'bar', "unnested key lookup succeeded");
+}
diff --git a/t/61-features.t b/t/61-features.t
new file mode 100644 (file)
index 0000000..b2d8467
--- /dev/null
@@ -0,0 +1,51 @@
+package MockApp;\r
+use strict;\r
+use warnings;\r
+\r
+$|++;\r
+\r
+use Test::More tests => 10;\r
+use Scalar::Util qw(blessed reftype);\r
+\r
+use Config::Any;\r
+use Config::Any::INI;\r
+\r
+our $cfg_file = 't/conf/conf.foo';\r
+\r
+eval { Config::Any::INI->load($cfg_file); };\r
+SKIP: {\r
+    skip "File loading backend for INI not found", 9 if $@;\r
+\r
+    ok( my $c_arr = Config::Any->load_files({ \r
+            files           => [ $cfg_file ], \r
+            force_plugins   => [qw(Config::Any::INI)] \r
+        }), "load file with parser forced" );\r
+\r
+    ok(my $c = $c_arr->[0], "load_files returns an arrayref");\r
+    \r
+    ok(ref $c, "load_files arrayref contains a ref");\r
+    my $ref = blessed $c ? reftype $c : ref $c;\r
+    is(substr($ref,0,4), "HASH", "hashref");\r
+\r
+    my ($name, $cfg) = each %$c;\r
+    is($name, $cfg_file, "filename matches");\r
+    \r
+    my $cfgref = blessed $cfg ? reftype $cfg : ref $cfg;\r
+    is(substr($cfgref,0,4), "HASH", "hashref cfg");\r
+\r
+    is( $cfg->{name}, 'TestApp', "appname parses" );\r
+    is( $cfg->{Component}{ "Controller::Foo" }->{ foo }, 'bar',                  \r
+        "component->cntrlr->foo = bar" );\r
+    is( $cfg->{Model}{ "Model::Baz" }->{ qux },                 'xyzzy',                 \r
+        "model->model::baz->qux = xyzzy" );\r
+\r
+\r
+    ok( my $c_arr_2 = Config::Any->load_files({ \r
+            files           => [ $cfg_file ], \r
+            force_plugins   => [qw(Config::Any::INI)],\r
+            use_ext         => 1\r
+        }), "load file with parser forced" );\r
+}\r
+\r
+\r
+\r
diff --git a/t/conf/conf.foo b/t/conf/conf.foo
new file mode 100644 (file)
index 0000000..a47b2d0
--- /dev/null
@@ -0,0 +1,7 @@
+name=TestApp\r
+    \r
+[Component Controller::Foo]\r
+foo=bar\r
+\r
+[Model Model::Baz]\r
+qux=xyzzy\r