| File: | blib/lib/Geo/Coder/Free/Config.pm |
| Coverage: | 62.6% |
| line | stmt | bran | cond | sub | pod | time | code |
|---|---|---|---|---|---|---|---|
| 1 | package Geo::Coder::Free::Config; | ||||||
| 2 | |||||||
| 3 | # VWF is licensed under GPL2.0 for personal use only | ||||||
| 4 | # njh@bandsman.co.uk | ||||||
| 5 | |||||||
| 6 | # Usage is subject to licence terms. | ||||||
| 7 | # The licence terms of this software are as follows: | ||||||
| 8 | # Personal single user, single computer use: GPL2 | ||||||
| 9 | # All other users (including Commercial, Charity, Educational, Government) | ||||||
| 10 | # must apply in writing for a licence for use from Nigel Horne at the | ||||||
| 11 | # above e-mail. | ||||||
| 12 | |||||||
| 13 | 2 2 2 | 74858 0 48 | use warnings; | ||||
| 14 | 2 2 2 | 3 2 15 | use strict; | ||||
| 15 | |||||||
| 16 | 2 2 2 | 2 2 47 | use Carp; | ||||
| 17 | 2 2 2 | 368 44580 31 | use Config::Abstraction; | ||||
| 18 | 2 2 2 | 675 96558 44 | use CGI::Info; | ||||
| 19 | 2 2 2 | 8 1 67 | use Data::Dumper; | ||||
| 20 | 2 2 2 | 264 1876 13 | use Error::Simple; | ||||
| 21 | 2 2 2 | 49 2 27 | use File::Spec; | ||||
| 22 | 2 2 2 | 4 25 957 | use Params::Get 0.13; | ||||
| 23 | |||||||
| 24 | =encoding utf-8 | ||||||
| 25 | |||||||
| 26 - 34 | =head1 NAME Geo::Coder::Free::Config - Site-independent configuration file for the Versatile Web Framework =head1 VERSION Version 0.01 =cut | ||||||
| 35 | |||||||
| 36 | our $VERSION = '0.01'; | ||||||
| 37 | |||||||
| 38 - 115 | =head1 SUBROUTINES/METHODS
=head2 new
Creates a new Geo::Coder::Free::Config instance with hierarchical configuration loading.
Takes four optional arguments:
info (CGI::Info object)
logger
config_directory - used when the configuration directory can't be worked out
config_file - name of the configuration file - otherwise determined dynamically
config (ref to hash of values to override in the config file
Values in the file are overridden by what's in the environment
B<Parameters:>
=over 4
=item * C<info> - CGI::Info object (optional, created if not provided)
=item * C<logger> - Logger object with debug() method (optional)
=item * C<config_directory> - Additional config directory path (optional)
=item * C<config_file> - Specific config filename (optional)
=item * C<config> - Hash ref of override values (optional)
=back
B<Configuration Resolution Order:>
1. Base configuration files
2. Values from config parameter
3. Environment variable overrides
B<Directory Search Order:>
1. $ENV{CONFIG_DIR} (if set)
2. Provided config_directory
3. ../conf relative to script
4. ../../conf relative to script
5. $DOCUMENT_ROOT/../lib/conf
6. $HOME/lib/conf
B<Returns:> Blessed Geo::Coder::Free::Config object
B<Throws:> Error::Simple on configuration errors
=head3 FORMAL SPECIFICATION
[STRING, HASH, LOGGER]
ConfigState ::= â¨â¨ config_dirs : â STRING;
config_data : HASH;
logger : LOGGER â©â©
ConfigArgs ::= â¨â¨ info : CGI_Info;
logger : LOGGER;
config_directory : STRING;
config_file : STRING;
config : HASH â©â©
Init : ConfigArgs â ConfigState
â params : ConfigArgs â¢
let dirs == if env.CONFIG_DIR â â
then {env.CONFIG_DIR}
else default_dirs ⪠{params.config_directory} fi â¢
let valid_dirs == {d : dirs | â f : FILE ⢠readable(d, f)} â¢
valid_dirs â â
â§
config_data â HASH â§
config_data = merge(file_config, params.config, env_overrides)
ValidConfigKey == {k : STRING | k â dom config_data}
GetConfigValue : ValidConfigKey â (STRING ⪠HASH ⪠ARRAY)
=cut | ||||||
| 116 | |||||||
| 117 | sub new | ||||||
| 118 | { | ||||||
| 119 | 3 | 1 | 105510 | my $proto = shift; | |||
| 120 | 3 | 11 | my $params = Params::Get::get_params(undef, @_); | ||||
| 121 | |||||||
| 122 | 3 | 52 | if (exists $params->{logger} && defined $params->{logger}) { | ||||
| 123 | throw Error::Simple('logger must be an object with debug method') | ||||||
| 124 | 0 | 0 | unless ref($params->{logger}) && $params->{logger}->can('debug'); | ||||
| 125 | } | ||||||
| 126 | |||||||
| 127 | 3 | 5 | if($params->{'logger'}) { | ||||
| 128 | 0 | 0 | $params->{'logger'}->debug(__PACKAGE__, '->new()'); | ||||
| 129 | } | ||||||
| 130 | |||||||
| 131 | 3 | 11 | if(exists $params->{config} && defined $params->{config}) { | ||||
| 132 | 2 | 5 | throw Error::Simple('config must be a hash reference') unless ref($params->{config}) eq 'HASH'; | ||||
| 133 | } | ||||||
| 134 | |||||||
| 135 | 3 | 8 | my $class = ref($proto) || $proto; | ||||
| 136 | 3 | 18 | my $info = $params->{info} || CGI::Info->new(); | ||||
| 137 | |||||||
| 138 | 3 | 37300 | my @config_dirs; | ||||
| 139 | 3 | 7 | if($ENV{'CONFIG_DIR'}) { | ||||
| 140 | # Validate directory exists | ||||||
| 141 | throw Error::Simple("CONFIG_DIR '$ENV{CONFIG_DIR}' does not exist or is not readable") | ||||||
| 142 | 0 | 0 | unless -d $ENV{'CONFIG_DIR'} && -r $ENV{'CONFIG_DIR'}; | ||||
| 143 | 0 | 0 | @config_dirs = ($ENV{'CONFIG_DIR'}); | ||||
| 144 | } else { | ||||||
| 145 | # Build the standard search path; caller's config_directory is prepended below | ||||||
| 146 | 3 | 10 | @config_dirs = ( | ||||
| 147 | File::Spec->catdir( | ||||||
| 148 | $info->script_dir(), | ||||||
| 149 | File::Spec->updir(), | ||||||
| 150 | File::Spec->updir(), | ||||||
| 151 | 'conf' | ||||||
| 152 | ), File::Spec->catdir( | ||||||
| 153 | $info->script_dir(), | ||||||
| 154 | File::Spec->updir(), | ||||||
| 155 | 'conf' | ||||||
| 156 | ) | ||||||
| 157 | ); | ||||||
| 158 | |||||||
| 159 | 3 | 525 | if($ENV{'DOCUMENT_ROOT'}) { | ||||
| 160 | push(@config_dirs, File::Spec->catdir( | ||||||
| 161 | 0 | 0 | $ENV{'DOCUMENT_ROOT'}, | ||||
| 162 | File::Spec->updir(), | ||||||
| 163 | 'lib', | ||||||
| 164 | 'conf' | ||||||
| 165 | )) | ||||||
| 166 | } | ||||||
| 167 | 3 | 8 | if($ENV{'HOME'}) { | ||||
| 168 | push(@config_dirs, File::Spec->catdir( | ||||||
| 169 | 3 | 11 | $ENV{'HOME'}, | ||||
| 170 | 'lib', | ||||||
| 171 | 'conf' | ||||||
| 172 | )); | ||||||
| 173 | } | ||||||
| 174 | |||||||
| 175 | # Prepend the caller-supplied directory so it takes highest precedence | ||||||
| 176 | 3 | 7 | if($params->{config_directory}) { | ||||
| 177 | 0 | 0 | throw Error::Simple("config_directory must be a string") if ref($params->{config_directory}); | ||||
| 178 | throw Error::Simple("config_directory '$params->{config_directory}' does not exist") | ||||||
| 179 | 0 | 0 | unless -d $params->{config_directory}; | ||||
| 180 | 0 | 0 | unshift @config_dirs, $params->{config_directory}; | ||||
| 181 | } | ||||||
| 182 | } | ||||||
| 183 | |||||||
| 184 | # Look for localised configurations | ||||||
| 185 | 3 | 3 | my $language; | ||||
| 186 | 3 | 4 | if(my $lingua = $params->{'lingua'}) { | ||||
| 187 | 0 | 0 | $language = $lingua->language_code_alpha2(); | ||||
| 188 | } | ||||||
| 189 | 3 | 17 | $language ||= $info->lang(); | ||||
| 190 | |||||||
| 191 | 3 | 5717 | if($language) { | ||||
| 192 | @config_dirs = map { | ||||||
| 193 | 3 9 | 5 15 | ($_, "$_/default", "$_/$language") | ||||
| 194 | } @config_dirs; | ||||||
| 195 | } else { | ||||||
| 196 | @config_dirs = map { | ||||||
| 197 | 0 0 | 0 0 | ($_, File::Spec->catdir($_, 'default')) | ||||
| 198 | } @config_dirs; | ||||||
| 199 | } | ||||||
| 200 | |||||||
| 201 | 3 | 7 | if($params->{'debug'}) { | ||||
| 202 | # Not sure this really does anything | ||||||
| 203 | # $Config::Auto::Debug = 1; | ||||||
| 204 | |||||||
| 205 | 0 | 0 | if($params->{logger}) { | ||||
| 206 | 0 | 0 | while(my ($key,$value) = each %ENV) { | ||||
| 207 | 0 | 0 | if($value) { | ||||
| 208 | 0 | 0 | $params->{logger}->debug("$key=$value"); | ||||
| 209 | } | ||||||
| 210 | } | ||||||
| 211 | } | ||||||
| 212 | } | ||||||
| 213 | |||||||
| 214 | 3 | 3 | my $config; | ||||
| 215 | 3 | 2 | eval { | ||||
| 216 | $config = Config::Abstraction->new( | ||||||
| 217 | config_dirs => \@config_dirs, | ||||||
| 218 | config_files => ['default', $info->domain_name(), $ENV{'CONFIG_FILE'}, $params->{'config_file'}], | ||||||
| 219 | 3 | 10 | logger => $params->{'logger'} | ||||
| 220 | )->all(); | ||||||
| 221 | }; | ||||||
| 222 | 3 | 4107 | if($@ || !defined($config)) { | ||||
| 223 | 0 | 0 | throw Error::Simple("Configuration error: $@"); | ||||
| 224 | } | ||||||
| 225 | |||||||
| 226 | # Validate essential configuration structure | ||||||
| 227 | 3 | 6 | throw Error::Simple('Configuration must be a hash reference') unless(ref($config) eq 'HASH'); | ||||
| 228 | |||||||
| 229 | # The values in config are defaults which can be overridden by | ||||||
| 230 | # the values in params->{config} | ||||||
| 231 | 3 | 6 | if(defined($params->{'config'})) { | ||||
| 232 | 2 2 2 | 2 3 5 | $config = { %{$config}, %{$params->{'config'}} }; | ||||
| 233 | } | ||||||
| 234 | |||||||
| 235 | # Allow variables to be overridden by the environment | ||||||
| 236 | 3 3 | 4 6 | foreach my $key(keys %{$config}) { | ||||
| 237 | 30 | 25 | if(my $value = $ENV{$key}) { | ||||
| 238 | # Validate environment variable names | ||||||
| 239 | # throw Error::Simple("Invalid environment variable name: $key") | ||||||
| 240 | # unless $key =~ /^[A-Z_][A-Z0-9_]*$/; | ||||||
| 241 | |||||||
| 242 | # Sanitize values | ||||||
| 243 | # $value =~ s/[^\w\s=\.\-]//g; # Remove potentially dangerous characters | ||||||
| 244 | |||||||
| 245 | 1 | 2 | if($params->{'logger'}) { | ||||
| 246 | 0 | 0 | $params->{'logger'}->debug(__PACKAGE__, ': ', __LINE__, " overwriting $key, ", $config->{$key}, " with $value"); | ||||
| 247 | } | ||||||
| 248 | # If the value contains an equals make it into a hash value | ||||||
| 249 | 1 | 2 | if($value =~ /(.+)=(.+)/) { | ||||
| 250 | 0 | 0 | delete $config->{$key} if(!ref($config->{$key})); | ||||
| 251 | 0 | 0 | $config->{$key}{$1} = $2; | ||||
| 252 | } else { | ||||||
| 253 | 1 | 2 | $config->{$key} = $value; | ||||
| 254 | } | ||||||
| 255 | } | ||||||
| 256 | } | ||||||
| 257 | |||||||
| 258 | # Config::Any turns fields with spaces into arrays, put them back | ||||||
| 259 | 3 | 5 | foreach my $field('Contents', 'SiteTitle') { | ||||
| 260 | 6 | 9 | next if(!exists $config->{$field}); | ||||
| 261 | 6 | 4 | my $value = $config->{$field}; | ||||
| 262 | |||||||
| 263 | 6 | 9 | if(ref($value) eq 'ARRAY') { | ||||
| 264 | 0 0 | 0 0 | $config->{$field} = join(' ', @{$value}); | ||||
| 265 | } | ||||||
| 266 | } | ||||||
| 267 | |||||||
| 268 | # unless($config->{'config_path'}) { | ||||||
| 269 | # $config->{'config_path'} = File::Spec->catdir($config_dir, $info->domain_name()); | ||||||
| 270 | # } | ||||||
| 271 | 3 | 5 | if($params->{'debug'} && $params->{'logger'}) { | ||||
| 272 | 0 | 0 | $params->{'logger'}->debug(__PACKAGE__, '(', __LINE__, '): ', Data::Dumper->new([$config])->Dump()); | ||||
| 273 | } | ||||||
| 274 | |||||||
| 275 | 3 | 20 | return bless $config, $class; | ||||
| 276 | } | ||||||
| 277 | |||||||
| 278 | sub AUTOLOAD | ||||||
| 279 | { | ||||||
| 280 | 5 | 1531 | our $AUTOLOAD; | ||||
| 281 | 5 | 5 | my $self = shift; | ||||
| 282 | |||||||
| 283 | 5 | 8 | return undef unless($self); | ||||
| 284 | 5 | 6 | return unless defined($AUTOLOAD); | ||||
| 285 | |||||||
| 286 | # Extract the key name from the AUTOLOAD variable | ||||||
| 287 | 5 | 11 | (my $key = $AUTOLOAD) =~ s/.*:://; | ||||
| 288 | |||||||
| 289 | 5 | 6 | return unless defined($key); | ||||
| 290 | |||||||
| 291 | # Don't handle special methods | ||||||
| 292 | 5 | 15 | return if $key eq 'DESTROY'; | ||||
| 293 | |||||||
| 294 | # Validate method name - only allow safe config keys | ||||||
| 295 | 2 | 5 | Carp::croak(__PACKAGE__, ": Invalid key name: $key") unless $key =~ /^[a-zA-Z_][a-zA-Z0-9_]*$/; | ||||
| 296 | |||||||
| 297 | # Return the value of the corresponding hash key | ||||||
| 298 | # Only return existing keys to avoid auto-vivification | ||||||
| 299 | 2 | 8 | return exists $self->{$key} ? $self->{$key} : undef; | ||||
| 300 | } | ||||||
| 301 | |||||||
| 302 | 1; | ||||||