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