1package Badger::Config::Filesystem; 2 3use Badger::Class 4 version => 0.01, 5 debug => 0, 6 import => 'class', 7 base => 'Badger::Config Badger::Workplace', 8 utils => 'split_to_list extend VFS join_uri resolve_uri', 9 accessors => 'root filespec encoding codecs extensions quiet', 10 words => 'ENCODING CODECS', 11 constants => 'DOT NONE TRUE FALSE YAML JSON UTF8 ARRAY HASH SCALAR', 12 constant => { 13 ABSOLUTE => 'absolute', 14 RELATIVE => 'relative', 15 # extra debugging flags 16 DEBUG_FETCH => 0, 17 }, 18 messages => { 19 load_fail => 'Failed to load data from %s: %s', 20 no_config_file => 'Missing configuration file: %s', 21 merge_mismatch => 'Cannot merge items for %s: %s and %s', 22 }; 23 24our $EXTENSIONS = [YAML, JSON]; 25our $ENCODING = UTF8; 26our $CODECS = { }; 27our $STAT_TTL = 0; 28 29 30#----------------------------------------------------------------------------- 31# Initialisation methods called at object creation time 32#----------------------------------------------------------------------------- 33 34sub init { 35 my ($self, $config) = @_; 36 37 # First call Badger::Config base class method to handle any 'items' 38 # definitions and other general initialisation 39 $self->init_config($config); 40 41 # Then our own custom init method 42 $self->init_filesystem($config); 43} 44 45sub init_filesystem { 46 my ($self, $config) = @_; 47 my $class = $self->class; 48 49 $self->debug_data( filesystem_config => $config ) if DEBUG; 50 51 # The filespec can be specified as a hash of options for file objects 52 # created by the top-level directory object. If unspecified, we construct 53 # it using any encoding option, or falling back on a $ENCODING package 54 # variable. This is then passed to init_workplace(). 55 my $encoding = $config->{ encoding } 56 || $class->any_var(ENCODING); 57 58 my $filespec = $config->{ filespec } ||= { 59 encoding => $encoding 60 }; 61 62 # now initialise the workplace root directory 63 $self->init_workplace($config); 64 65 # Configuration files can be in any data format which Badger::Codecs can 66 # handle (e.g. JSON, YAML, etc). The 'extensions' configuration option 67 # and any $EXTENSIONS defined in package variables (for the current class 68 # and all base classes) will be tried in order 69 my $exts = $class->list_vars( 70 EXTENSIONS => $config->{ extensions } 71 ); 72 $exts = [ 73 map { @{ split_to_list($_) } } 74 @$exts 75 ]; 76 77 # Construct a regex to match any of the above 78 my $qm_ext = join('|', map { quotemeta $_ } @$exts); 79 my $ext_re = qr/.($qm_ext)$/i; 80 81 $self->debug( 82 "extensions: ", $self->dump_data($exts), "\n", 83 "extension regex: $ext_re" 84 ) if DEBUG; 85 86 # The 'codecs' option can provide additional mapping from filename extension 87 # to codec for any that Badger::Codecs can't grok automagically 88 my $codecs = $class->hash_vars( 89 CODECS => $config->{ codecs } 90 ); 91 92 my $data = $config->{ data } || { }; 93 94 $self->{ data } = $data; 95 $self->{ extensions } = $exts; 96 $self->{ match_ext } = $ext_re; 97 $self->{ codecs } = $codecs; 98 $self->{ encoding } = $encoding; 99 $self->{ filespec } = $filespec; 100 $self->{ quiet } = $config->{ quiet } || FALSE; 101 $self->{ dir_tree } = $config->{ dir_tree } // TRUE; 102 $self->{ stat_ttl } = $config->{ stat_ttl } // $data->{ stat_ttl } // $STAT_TTL; 103 $self->{ not_found } = { }; 104 105 # Add any item schemas 106 $self->items( $config->{ schemas } ) 107 if $config->{ schemas }; 108 109 # Configuration file allows further data items (and schemas) to be defined 110 $self->init_file( $config->{ file } ) 111 if $config->{ file }; 112 113 return $self; 114} 115 116sub init_file { 117 my ($self, $file) = @_; 118 my $data = $self->get($file); 119 120 if ($data) { 121 # must copy data so as not to damage cached version 122 $data = { %$data }; 123 124 $self->debug( 125 "config file data from $file: ", 126 $self->dump_data($data) 127 ) if DEBUG; 128 129 # file can contain 'items' or 'schemas' (I don't love this, but it'll do for now) 130 $self->items( 131 delete $data->{ items }, 132 delete $data->{ schemas } 133 ); 134 135 # anything else is config data 136 extend($self->{ data }, $data); 137 138 $self->debug("merged data: ", $self->dump_data($self->{ data })) if DEBUG; 139 } 140 elsif (! $self->{ quiet }) { 141 return $self->no_config_file($file); 142 } 143 144 return $self; 145} 146 147sub no_config_file { 148 shift->warn_msg( no_config_file => @_ ); 149} 150 151 152#----------------------------------------------------------------------------- 153# Redefine head() method in Badger::Config to hook into fetch() to load data 154#----------------------------------------------------------------------------- 155 156sub head { 157 my ($self, $name) = @_; 158 return $self->{ data }->{ $name } 159 // $self->fetch($name); 160} 161 162sub tail { 163 my ($self, $name, $data) = @_; 164 return $data; 165} 166 167 168#----------------------------------------------------------------------------- 169# Filesystem-specific fetch methods 170#----------------------------------------------------------------------------- 171 172sub fetch { 173 my ($self, $uri) = @_; 174 175 return if $self->previously_not_found($uri); 176 177 $self->debug("fetch($uri)") if DEBUG or DEBUG_FETCH; 178 179 my $file = $self->config_file($uri); 180 my $dir = $self->dir($uri); 181 my $fok = $file && $file->exists; 182 my $dok = $dir && $dir->exists; 183 184 if ($dok) { 185 $self->debug("Found directory for $uri, loading tree") if DEBUG or DEBUG_FETCH; 186 return $self->config_tree($uri, $file, $dir); 187 } 188 189 if ($fok) { 190 $self->debug("Found file for $uri, loading file data => ", $file->absolute) if DEBUG or DEBUG_FETCH; 191 my $data = $file->try->data; 192 return $self->error_msg( load_fail => $file => $@ ) if $@; 193 return $self->tail( 194 $uri, $data, 195 $self->item_schema_from_data( 196 $uri, $data 197 ) 198 ); 199 } 200 201 $self->debug("No file or directory found for $uri") if DEBUG or DEBUG_FETCH; 202 $self->{ not_found }->{ $uri } = time(); 203 return undef; 204} 205 206sub previously_not_found { 207 my ($self, $uri) = @_; 208 my $sttl = $self->{ stat_ttl } || return 0; 209 my $when = $self->{ not_found }->{ $uri } || return 0; 210 # we maintain the "not_found" status until stat_ttl seconds have elapsed 211 if (time < $when + $sttl) { 212 $self->debug("$uri NOT FOUND at $when") if DEBUG; # or DEBUG_FETCH; 213 return 1 214 } 215 else { 216 return 0; 217 } 218} 219 220#----------------------------------------------------------------------------- 221# Tree walking 222#----------------------------------------------------------------------------- 223 224sub config_tree { 225 my $self = shift; 226 my $name = shift; 227 my $file = shift || $self->config_file($name); 228 my $dir = shift || $self->dir($name); 229 my $do_tree = $self->{ dir_tree }; 230 my $data = undef; #{ }; 231 my ($file_data, $binder, $more); 232 233 unless ($file && $file->exists || $dir->exists) { 234 return $self->decline_msg( not_found => 'file or directory' => $name ); 235 } 236 237 # start by looking for a data file 238 if ($file && $file->exists) { 239 $file_data = $file->try->data; 240 return $self->error_msg( load_fail => $file => $@ ) if $@; 241 $self->debug("Read metadata from file '$file':", $self->dump_data($file_data)) if DEBUG; 242 } 243 244 # fetch a schema for this data item constructed from the default schema 245 # specification, any named schema for this item, any arguments, then any 246 # local schema defined in the data file 247 my $schema = $self->item_schema_from_data($name, $file_data); 248 249 $self->debug( 250 "combined schema for $name: ", 251 $self->dump_data($schema) 252 ) if DEBUG; 253 254 if ($more = $schema->{ tree_type }) { 255 $self->debug("schema.tree_type: $more") if DEBUG; 256 if ($more eq NONE) { 257 $self->debug("schema rules indicate we shouldn't descend into the tree") if DEBUG; 258 $do_tree = FALSE; 259 } 260 elsif ($binder = $self->tree_binder($more)) { 261 $self->debug("schema rules indicate a $more tree tree") if DEBUG; 262 $do_tree = TRUE; 263 } 264 else { 265 return $self->error_msg( invalid => tree_type => $more ); 266 } 267 } 268 269 if ($do_tree) { 270 # merge file data using binder 271 $data ||= { }; 272 $binder ||= $self->tree_binder('nest'); 273 $binder->($self, $data, [ ], $file_data, $schema); 274 275 if ($dir->exists) { 276 # create a virtual file system rooted on the metadata directory 277 # so that all file paths are resolved relative to it 278 my $vfs = VFS->new( root => $dir ); 279 $self->debug("Reading metadata from dir: ", $dir->name) if DEBUG; 280 $self->scan_config_dir($vfs->root, $data, [ ], $schema, $binder); 281 } 282 } 283 else { 284 $data = $file_data; 285 } 286 287 $self->debug("$name config: ", $self->dump_data($data)) if DEBUG; 288 289 return $self->tail( 290 $name, $data, $schema 291 ); 292} 293 294sub scan_config_dir { 295 my ($self, $dir, $data, $path, $schema, $binder) = @_; 296 my $files = $dir->files; 297 my $dirs = $dir->dirs; 298 $path ||= [ ]; 299 $binder ||= $self->tree_binder; 300 301 $self->debug( 302 "scan_config_dir($dir, $data, ", 303 $self->dump_data_inline($path), ", ", 304 $self->dump_data_inline($schema), ", ", 305 $binder, ")" 306 ) if DEBUG; 307 308 $data ||= { }; 309 310 foreach my $file (@$files) { 311 next unless $file->name =~ $self->{ match_ext }; 312 $self->debug("found file: ", $file->name, ' at ', $file->path) if DEBUG; 313 $self->scan_config_file($file, $data, $path, $schema, $binder); 314 } 315 foreach my $subdir (@$dirs) { 316 $self->debug("found dir: ", $subdir->name, ' at ', $subdir->path) if DEBUG; 317 # if we don't have a data binder then we need to create a sub-hash 318 my $name = $subdir->name; 319 #my $more = $binder ? $data : ($data->{ $name } = { }); 320 push(@$path, $name); 321 #$self->scan_config_dir($subdir, $more, $path, $schema, $binder); 322 $self->scan_config_dir($subdir, $data, $path, $schema, $binder); 323 pop(@$path); 324 } 325} 326 327sub scan_config_file { 328 my ($self, $file, $data, $path, $schema, $binder) = @_; 329 my $base = $file->basename; 330 my $ext = $file->extension; 331 332 $self->debug( 333 "scan_config_file($file, $data, ", 334 $self->dump_data_inline($path), ", ", 335 $self->dump_data_inline($schema), ", ", 336 $binder, ")" 337 ) if DEBUG; 338 339 # set the codec to match the extension (or any additional mapping) 340 # and set the data encoding 341 $file->codec( $self->codec($ext) ); 342 $file->encoding( $self->{ encoding } ); 343 344 my $meta = $file->try->data; 345 return $self->error_msg( load_fail => $file => $@ ) if $@; 346 347 $self->debug("Metadata: ", $self->dump_data($meta)) if DEBUG; 348 349 if ($binder) { 350 $path ||= [ ]; 351 push(@$path, $base); 352 $binder->($self, $data, $path, $meta, $schema); 353 pop(@$path); 354 } 355 else { 356 $base =~ s[^/][]; 357 $data->{ $base } = $meta; 358 } 359} 360 361 362#----------------------------------------------------------------------------- 363# Binder methods for combining multiple data sources (e.g. files in sub- 364# directories) into a single tree. 365#----------------------------------------------------------------------------- 366 367sub tree_binder { 368 my $self = shift; 369 my $name = shift 370 || $self->{ tree_type } 371 || return $self->error_msg( missing => 'tree_type' ); 372 373 return $self->can("${name}_tree_binder") 374 || return $self->decline_msg( invalid => binder => $name ); 375} 376 377sub nest_tree_binder { 378 my ($self, $parent, $path, $child, $schema) = @_; 379 my $data = $parent; 380 my $uri = join('/', @$path); 381 my @bits = @$path; 382 my $last = pop @bits; 383 384 $self->debug("Adding [$uri] as ", $self->dump_data($child))if DEBUG; 385 386 foreach my $key (@bits) { 387 $data = $data->{ $key } ||= { }; 388 } 389 390 if ($last) { 391 my $tail = $data->{ $last }; 392 393 if ($tail) { 394 my $tref = ref $tail || SCALAR; 395 my $cref = ref $child || SCALAR; 396 397 if ($tref eq HASH && $cref eq HASH) { 398 $self->debug("Merging into $last") if DEBUG; 399 @$tail{ keys %$child } = values %$tail; 400 } 401 else { 402 return $self->error_msg( merge_mismatch => $uri, $tref, $cref ); 403 } 404 } 405 else { 406 $self->debug("setting $last in data to $child") if DEBUG; 407 $data->{ $last } = $child; 408 } 409 } 410 else { 411 $self->debug("No path, simple merge of child into parent") if DEBUG; 412 @$data{ keys %$child } = values %$child; 413 } 414 415 $self->debug("New parent: ", $self->dump_data($parent)) if DEBUG; 416} 417 418sub flat_tree_binder { 419 my ($self, $parent, $path, $child, $schema) = @_; 420 421 while (my ($key, $value) = each %$child) { 422 $parent->{ $key } = $value; 423 } 424} 425 426sub join_tree_binder { 427 my ($self, $parent, $path, $child, $schema) = @_; 428 my $joint = $schema->{ tree_joint } || $self->{ tree_joint }; 429 my $base = join($joint, @$path); 430 431 $self->debug( 432 "join_binder path is set: ", 433 $self->dump_data($path), 434 "\nnew base is $base" 435 ) if DEBUG; 436 437 # Similar to the above but this joins items with underscores 438 # e.g. an entry "foo" in site/bar.yaml will become "bar_foo" 439 while (my ($key, $value) = each %$child) { 440 if ($key =~ s/^\///) { 441 # if the child item has a leading '/' then we want to put it in 442 # the root so we leave $key unchanged 443 } 444 elsif (length $base) { 445 # otherwise the $key is appended onto $base 446 $key = join($joint, $base, $key); 447 } 448 $parent->{ $key } = $value; 449 } 450} 451 452sub uri_tree_binder { 453 my ($self, $parent, $path, $child, $schema) = @_; 454 my $opt = $schema->{ uri_paths } || $self->{ uri_paths }; 455 my $base = join_uri(@$path); 456 457 $self->debug("uri_paths option: $opt") if DEBUG; 458 459 $self->debug( 460 "uri_binder path is set: ", 461 $self->dump_data($path), 462 "\nnew base is $base" 463 ) if DEBUG; 464 465 # This resolves base items as URIs relative to the parent 466 # e.g. an entry "foo" in the site/bar.yaml file will be stored in the parent 467 # site as "bar/foo", but an entry "/bam" will be stored as "/bam" because 468 # it's an absolute URI rather than a relative one (relative to the $base) 469 while (my ($key, $value) = each %$child) { 470 my $uri = $base ? resolve_uri($base, $key) : $key; 471 if ($opt) { 472 $uri = $self->fix_uri_path($uri, $opt); 473 } 474 $parent->{ $uri } = $value; 475 $self->debug( 476 "loaded metadata for [$base] + [$key] = [$uri]" 477 ) if DEBUG; 478 } 479} 480 481sub fix_uri_path { 482 my ($self, $uri, $option) = @_; 483 484 $option ||= $self->{ uri_paths } || return $uri; 485 486 if ($option eq 'absolute') { 487 $self->debug("setting absolute URI path") if DEBUG; 488 $uri = "/$uri" unless $uri =~ /^\//; 489 } 490 elsif ($option eq 'relative') { 491 $self->debug("setting relative URI path") if DEBUG; 492 $uri =~ s/^\///; 493 } 494 else { 495 return $self->error_msg( invalid => 'uri_paths option' => $option ); 496 } 497 498 return $uri; 499} 500 501#----------------------------------------------------------------------------- 502# Internal methods 503#----------------------------------------------------------------------------- 504 505sub config_file { 506 my ($self, $name) = @_; 507 508 return $self->{ config_file }->{ $name } 509 ||= $self->find_config_file($name); 510} 511 512sub config_file_data { 513 my $self = shift; 514 my $file = $self->config_file(@_) || return; 515 my $data = $file->try->data; 516 return $self->error_msg( load_fail => $file => $@ ) if $@; 517 return $data; 518} 519 520sub config_filespec { 521 my $self = shift; 522 my $defaults = $self->{ filespec }; 523 524 return @_ 525 ? extend({ }, $defaults, @_) 526 : { %$defaults }; 527} 528 529sub find_config_file { 530 my ($self, $name) = @_; 531 my $root = $self->root; 532 my $exts = $self->extensions; 533 534 foreach my $ext (@$exts) { 535 my $path = $name.DOT.$ext; 536 my $file = $self->file($path); 537 if ($file->exists) { 538 $file->codec($self->codec($ext)); 539 return $file; 540 } 541 } 542 return $self->decline_msg( 543 not_found => file => $name 544 ); 545} 546 547sub write_config_file { 548 my ($self, $name, $data) = @_; 549 my $root = $self->root; 550 my $exts = $self->extensions; 551 my $ext = $exts->[0]; 552 my $path = $name.DOT.$ext; 553 my $file = $self->file($path); 554 555 $file->codec($self->codec($ext)); 556 $file->data($data); 557 return $file; 558} 559 560 561sub codec { 562 my ($self, $name) = @_; 563 return $self->codecs->{ $name } 564 || $name; 565} 566 567 568#----------------------------------------------------------------------------- 569# item schema management 570#----------------------------------------------------------------------------- 571 572sub items { 573 return extend( 574 shift->{ item }, 575 @_ 576 ); 577} 578 579sub item { 580 my ($self, $name) = @_; 581 582 $self->debug_data("looking for $name in items: ", $self->{ item }) if DEBUG; 583 584 return $self->{ item }->{ $name } 585 ||= $self->lookup_item($name); 586} 587 588sub lookup_item { 589 # hook for subclasses 590 return undef; 591} 592 593sub item_schema { 594 my ($self, $name, $schema) = @_; 595 my $data = $self->item($name); 596 597 if (DEBUG) { 598 $self->debug_data("$name item schema data: ", $data); 599 $self->debug_data("$name file schema: ", $schema); 600 } 601 602 if ($schema) { 603 $data = extend({ }, $data, $schema); 604 } 605 606 # the schema we got may have been for a parent via lookup_item. 607 $self->{ item }->{ $name } = $data; 608 $self->debug_data("set new item $name data", $data) if DEBUG; 609 610 return $data; 611} 612 613sub item_schema_from_data { 614 my ($self, $name, $data) = @_; 615 my $more; 616 617 if ($data && ref $data eq HASH) { 618 # In the event that someone needs to store a 'schema' item in the *real* 619 # configuration data, we look for '_schema_' first and delete that, 620 # leaving 'schema' untouched 621 $more = delete $data->{_schema_} 622 || delete $data->{ schema }; 623 } 624 return$self->item_schema($name, $more); 625} 626 627 628 629sub has_item { 630 my $self = shift->prototype; 631 my $name = shift; 632 my $item = $self->{ item }->{ $name }; 633 634 # This is all the same as in the base class up to the final test which 635 # looks for $self->config_file($name) as a last-ditch attempt 636 637 if (defined $item) { 638 # A 1/0 entry in the item tells us if an item categorically does or 639 # doesn't exist in the config data set (or allowable set - it might 640 # be a valid configuration option that simply hasn't been set yet) 641 return $item; 642 } 643 else { 644 # Otherwise the existence (or not) of an item in the data set is 645 # enough to satisfy us one way or another 646 return 1 647 if exists $self->{ data }->{ $name }; 648 649 # Special case for B::C::Filesystem which looks to see if there's a 650 # matching config file. We cache the existence in $self->{ item } 651 # so we know if it's there (or not) for next time 652 return $self->{ item }->{ $name } 653 = $self->config_file($name); 654 } 655} 656 657 6581; 659 660__END__ 661 662=head1 NAME 663 664Badger::Config::Filesystem - reads configuration files in a directory 665 666=head1 SYNOPSIS 667 668 use Badger::Config::Filesystem; 669 670 my $config = Badger::Config::Filesystem->new( 671 root => 'path/to/some/dir' 672 ); 673 674 # Fetch the data in user.[yaml|json] in above dir 675 my $user = $config->get('user') 676 || die "user: not found"; 677 678 # Fetch sub-data items using dotted syntax 679 print $config->get('user.name'); 680 print $config->get('user.emails.0'); 681 682=head1 DESCRIPTION 683 684This module is a subclass of L<Badger::Config> for reading data from 685configuration files in a directory. 686 687Consider a directory that contains the following files and sub-directories: 688 689 config/ 690 site.yaml 691 style.yaml 692 pages.yaml 693 pages/ 694 admin.yaml 695 developer.yaml 696 697We can create a L<Badger::Config::Filesystem> object to read the configuration 698data from the files in this directory like so: 699 700 my $config = Badger::Config::Filesystem->new( 701 root => 'config' 702 ); 703 704Reading the data from C<site.yaml> is as simple as this: 705 706 my $site = $config->get('site'); 707 708Note that the file extension is B<not> required. You can have either a 709C<site.yaml> or a C<site.json> file in the directory and the module will 710load whichever one it finds first. It's possible to add other data codecs 711if you want to use something other than YAML or JSON. 712 713You can also access data from within a configuration file. If the C<site.yaml> 714file contains the following: 715 716 name: My Site 717 version: 314 718 author: 719 name: Andy Wardley 720 email: abw@wardley.org 721 722Then we can read the version and author name like so: 723 724 print $config->get('site.version'); 725 print $config->get('author.name'); 726 727If the configuration directory contains a sub-directory with the same name 728as the data file being loaded (minus the extension) then any files under 729that directory will also be loaded. Going back to our earlier example, 730the C<pages> item is such a case: 731 732 config/ 733 site.yaml 734 style.yaml 735 pages.yaml 736 pages/ 737 admin.yaml 738 developer.yaml 739 740There are three files relevant to C<pages> here. Let's assume the content 741of each is as follow: 742 743F<pages.yaml>: 744 745 one: Page One 746 two: Page Two 747 748F<pages/admin.yaml>: 749 750 three: Page Three 751 four: Page Four 752 753F<pages/developer.yaml>: 754 755 five: Page Five 756 757When we load the C<pages> data like so: 758 759 my $pages = $config->get('pages'); 760 761We end up with a data structure like this: 762 763 { 764 one => 'Page One', 765 two => 'Page Two', 766 admin => { 767 three => 'Page Three', 768 four => 'Page Four', 769 }, 770 developer => { 771 five => 'Page Five', 772 }, 773 } 774 775Note how the C<admin> and C<developer> items have been nested into the data. 776The filename base (e.g. C<admin>, C<developer>) is used to define an entry 777in the "parent" hash array containing the data in the "child" data file. 778 779The C<tree_type> option can be used to change the way that this data is merged. 780To use this option, put it in a C<schema> section in the top level 781configuration file, e.g. the C<pages.yaml>: 782 783F<pages.yaml>: 784 785 one: Page One 786 two: Page Two 787 schema: 788 tree_type: flat 789 790If you don't want the data nested at all then specify a C<flat> value for 791C<tree_type>. This would return the following data: 792 793 { 794 one => 'Page One', 795 two => 'Page Two', 796 three => 'Page Three', 797 four => 'Page Four', 798 five => 'Page Five', 799 } 800 801The C<join> type collapses the nested data files by joining the file path 802(without extension) onto the data items contain therein. e.g. 803 804 { 805 one => 'Page One', 806 two => 'Page Two', 807 admin_three => 'Page Three', 808 admin_four => 'Page Four', 809 developer_five => 'Page Five', 810 } 811 812You can specify a different character sequence to join paths via the 813C<tree_joint> option, e.g. 814 815 schema: 816 tree_type: join 817 tree_joint: '-' 818 819That would producing this data structure: 820 821 { 822 one => 'Page One', 823 two => 'Page Two', 824 admin-three => 'Page Three', 825 admin-four => 'Page Four', 826 developer-five => 'Page Five', 827 } 828 829The C<uri> type is a slightly smarter version of the C<join> type. 830It joins path elements with the C</> character to create URI paths. 831 832 { 833 one => 'Page One', 834 two => 'Page Two', 835 admin/three => 'Page Three', 836 admin/four => 'Page Four', 837 developer/five => 'Page Five', 838 } 839 840What makes it special is that it follows the standard rules for URI resolution 841and recognises a path with a leading slash to be absolute rather than relative 842to the current location. 843 844For example, the F<pages/admin.yaml> file could contain something like this: 845 846F<pages/admin.yaml>: 847 848 three: Page Three 849 /four: Page Four 850 851The C<three> entry is considered to be relative to the C<admin> file so results 852in a final path of C<admin/three> as before. However, C</four> is an absolute 853path so the C<admin> path is ignored. The end result is a data structure like 854this: 855 856 { 857 one => 'Page One', 858 two => 'Page Two', 859 admin/three => 'Page Three', 860 /four => 'Page Four', 861 developer/five => 'Page Five', 862 } 863 864In this example we've ended up with an annoying inconsistency in that our 865C</four> path has a leading slash when the other items don't. The 866C<uri_paths> option can be set to C<relative> or C<absolute> to remove or add 867leading slashes respectively, effectively standardising all paths as one or 868the other. 869 870 schema: 871 tree_type: uri 872 uri_paths: absolute 873 874The data would then be returned like so: 875 876 { 877 /one => 'Page One', 878 /two => 'Page Two', 879 /admin/three => 'Page Three', 880 /four => 'Page Four', 881 /developer/five => 'Page Five', 882 } 883 884=head1 CONFIGURATION OPTIONS 885 886=head2 root / directory / dir 887 888The C<root> (or C<directory> or C<dir> if you prefer) option must be provided 889to specify the directory that the module should load configuration files 890from. Directories can be specified as absolute paths or relative to the 891current working directory. 892 893 my $config = Badger::Config::Filesystem->new( 894 dir => 'path/to/config/dir' 895 ); 896 897=head2 data 898 899Any additional configuration data can be provided via the C<data> named 900parameter: 901 902 my $config = Badger::Config::Filesystem->new( 903 dir => 'path/to/config/dir' 904 data => { 905 name => 'Arthur Dent', 906 email => 'arthur@dent.org', 907 }, 908 ); 909 910=head2 encoding 911 912The character encoding of the configuration files. Defaults to C<utf8>. 913 914=head2 extensions 915 916A list of file extensions to try in addition to C<yaml> and C<json>. 917Note that you may also need to define a C<codecs> entry to map the 918file extension to a data encoder/decoder module. 919 920 my $config = Badger::Config::Filesystem->new( 921 dir => 'path/to/config/dir' 922 extensions => ['str'], 923 codecs => { 924 str => 'storable', 925 } 926 ); 927 928=head2 codecs 929 930File extensions like C<.yaml> and C<.json> are recognised by L<Badger::Codecs> 931which can then provide the appropriate L<Badger::Codec> module to handle the 932encoding and decoding of data in the file. The L<codecs> options can be used 933to provide mapping from other file extensions to L<Badger::Codec> modules. 934 935 my $config = Badger::Config::Filesystem->new( 936 dir => 'path/to/config/dir' 937 extensions => ['str'], 938 codecs => { 939 str => 'storable', # *.str files loaded via storable codec 940 } 941 ); 942 943You may need to write a simple codec module yourself if there isn't one for 944the data format you want, but it's usually just a few lines of code that are 945required to provide the L<Badger::Codec> wrapper module around whatever other 946Perl module or custom code you've using to load and save the data format. 947 948=head2 schemas 949 950TODO: document specification of item schemas. The items below (tree_type 951through uri_paths) must now be defined in a schema. Support for a default 952schema has temporarily been disabled/broken. 953 954=head2 tree_type 955 956This option can be used to sets the default tree type for any configuration 957items that don't explicitly declare it by other means. The default tree 958type is C<nest>. 959 960NOTE: this has been changed. Don't trust these docs. 961 962The following tree types are supported: 963 964=head3 nest 965 966This is the default tree type, creating nested hash arrays of data. 967 968=head3 flat 969 970Creates a flat hash array by merging all nested hash array of data into one. 971 972=head3 join 973 974Joins data paths together using the C<tree_joint> string which is C<_> by 975default. 976 977=head3 uri 978 979Joins data paths together using slash characters to create URI paths. 980An item in a sub-directory can have a leading slash (i.e. an absolute path) 981and it will be promoted to the top-level data hash. 982 983e.g. 984 985 foo/bar + baz = foo/bar/baz 986 foo/bar + /bam = /bam 987 988=head3 none 989 990No tree is created. No sub-directories are scanned. You never saw me. 991I wasn't here. 992 993=head2 tree_joint 994 995This option can be used to set the default character sequence for joining 996paths 997 998=head2 uri_paths 999 1000This option can be used to set the default C<uri_paths> option for joining 1001paths as URIs. It should be set to C<relative> or C<absolute>. It can 1002be over-ridden in a C<schema> section of a top-level configuration file. 1003 1004=head1 METHODS 1005 1006The module inherits all methods defined in the L<Badger::Config> and 1007L<Badger::Workplace> base classes. 1008 1009=head1 INTERNAL METHODS 1010 1011The following methods are defined for internal use. 1012 1013=head2 init($config) 1014 1015This overrides the default initialisation method inherited from 1016L<Badger::Config>. It calls the L<init_config()|Badger::Config/init_config()> 1017method to perform the base class L<Badger::Config> initialisation and then 1018the L<init_filesystem()> method to perform initialisation specific to the 1019L<Badger::Config::Filesystem> module. 1020 1021=head2 init_filesystem($config) 1022 1023This performs the initialisation of the object specific to the filesystem 1024object. 1025 1026=head2 head($item) 1027 1028This redefines the L<head()|Badger::Config/head()> method in the 1029L<Badger::Config> base class. The method is called by 1030L<get()|Badger::Config/get()> to fetch a top-level data item 1031(e.g. C<user> in C<$config-E<gt>get('user.name')>). This implementation 1032looks for existing data items as usual, but additionally falls back on a 1033call to L<fetch($item)> to load additional data (or attempt to load it). 1034 1035=head2 tail($item, $data) 1036 1037This is a do-nothing stub for subclasses to redefine. It is called after 1038a successful call to L<fetch()>. 1039 1040=head2 fetch($item) 1041 1042This is the main method called to load a configuration file (or tree of 1043files) from the filesystem. It looks to see if a configuration file 1044(with one of the known L<extensions> appended, e.g. C<"$item.yaml">, 1045C<"$item.json">, etc) exists and/or a directory named C<$item>. 1046 1047If the file exists but the directory doesn't then the configuration data 1048is read from the file. If the directory exists 1049 1050=head2 config_tree($item, $file, $dir) 1051 1052This scans a configuration tree comprising of a configuration file and/or 1053a directory. The C<$file> and C<$dir> arguments are optional and are only 1054supported as an internal optimisation. The method can safely be called with 1055a single C<$item> argument and the relevant file and directory will be 1056determined automatically. 1057 1058The configuration file is loaded (via L<scan_config_file()>). If the 1059directory exists then it is also scanned (via L<scan_config_dir()>) and the 1060files contained therein are loaded. 1061 1062=head2 scan_config_file($file, $data, $path, $schema, $binder) 1063 1064Loads the data in a configuration C<$file> and merges it into the common 1065C<$data> hash under the C<$path> prefix (a reference to an array). The 1066C<$schema> contains any schema rules for this data item. The C<$binder> 1067is a reference to a L<tree_binder()> method to handle the data merge. 1068 1069=head2 scan_config_dir($dir, $data, $path, $schema, $binder) 1070 1071Scans the diles in a configuration directory, C<$dir> and recursively calls 1072L<scan_config_dir()> for each sub-directory found, and L<scan_config_file()> 1073for each file. 1074 1075=head2 tree_binder($name) 1076 1077This method returns a reference to one of the binder methods below based 1078on the C<$name> parameter provided. 1079 1080 # returns a reference to the nest_binder() method 1081 my $binder = $config->tree_binder('nest'); 1082 1083If no C<$name> is specified then it uses the default C<tree_type> of C<nest>. 1084This can be changed via the L<tree_type> configuration option. 1085 1086=head2 nest_tree_binder($parent, $path, $child, $schema) 1087 1088This handles the merging of data for the L<nest> L<tree_type>. 1089 1090=head2 flat_tree_binder($parent, $path, $child, $schema) 1091 1092This handles the merging of data for the L<flat> L<tree_type>. 1093 1094=head2 uri_tree_binder($parent, $path, $child, $schema) 1095 1096This handles the merging of data for the L<uri> L<tree_type>. 1097 1098=head2 join_tree_binder($parent, $path, $child, $schema) 1099 1100This handles the merging of data for the L<join> L<tree_type>. 1101 1102=head2 config_file($name) 1103 1104This method returns a L<Badger::Filesystem::File> object representing a 1105configuration file in the configuration directory. It will automatically 1106have the correct filename extension added (via a call to L<config_filename>) 1107and the correct C<codec> and C<encoding> parameters set (via a call to 1108L<config_filespec>) so that the data in the configuration file can be 1109automatically loaded (see L<config_data($name)>). 1110 1111=head2 config_file_data($name) 1112 1113This method fetches a configuration file via a call to L<config_file()> 1114and then returns the data contained therein. 1115 1116=head2 config_filespec($params) 1117 1118Returns a reference to a hash array containing appropriate initialisation 1119parameters for L<Badger::Filesystem::File> objects created to read general 1120and resource-specific configuration files. The parameters are constructed 1121from the C<codecs> (default: C<yaml>) and C<encoding> (default: C<utf8>) 1122configuration options. These can be overridden or augmented by extra 1123parameters passed as arguments. 1124 1125 1126=head1 AUTHOR 1127 1128Andy Wardley L<http://wardley.org/> 1129 1130=head1 COPYRIGHT 1131 1132Copyright (C) 2008-2014 Andy Wardley. All Rights Reserved. 1133 1134This module is free software; you can redistribute it and/or modify it 1135under the same terms as Perl itself. 1136 1137=cut 1138