Snippets/CatalystX_Model_Resource

CatalystX::Model::Resource

package CatalystX::Model::Resource;

use strict;
use warnings;

use Moose;
use namespace::autoclean;

extends 'Catalyst::Model';

use YAML::Any 'LoadFile';
use Data::Visitor 'visit';
use Hash::Merge 'merge';

# one or more paths to yaml files
has filename => ( is => 'ro' );
# if enabled check for file updates on each access
# if disabled only load the first time the service is called
has autoreload => ( is => 'ro', default => 0 );
# if enabled expand references to other yaml elements in values
# uses -> to access child elements
has interpolate => ( is => 'ro', default => 0 );
# start and stop delimiter for variable expansion
has delimiter => ( is => 'ro', default => sub { [ '${', '}' ] } );
# failsafe to avoid infinite loop during interpolation
has max_recursion => ( is => 'ro', default => 10 );

# internal storage for file reload checks
has last_modified_ts => ( is => 'rw', default => sub { {} } );
# internal storage for parsed yaml tree
has data => ( is => 'rw' );

sub get
{
    my $self = shift;

    if ( !$self->data or $self->autoreload ) {
        # normalize filename parameter to an array even if it is only one element
        my @files = ref( $self->filename ) eq "ARRAY" ? @{ $self->filename } : $self->filename;

        # check if any of the files have changed since the last reload
        my $is_dirty = 0;
        for my $fn (@files) {
            my $file_modified_ts = ( stat($fn) )[9] or die "error stat '$fn': $!";
            if (  !$self->last_modified_ts->{$fn}
                or $file_modified_ts > $self->last_modified_ts->{$fn} )
            {
                # save timestamp before the actual load to prevent a race condition
                $self->last_modified_ts->{$fn} = $file_modified_ts;

                $is_dirty = 1;
            }
        }

        # if there are changes reparse all of the files and run post-processing
        if ($is_dirty) {
            # clear the data then load elements from each file
            $self->data( {} );
            for my $fn (@files) {
                # merge contents of file into data hashref
                # this allows containers to span multiple files
                $self->data( merge( LoadFile($fn), $self->data ) );
            }

            # if a variable delimiter was specified attempt substitutions
            if ( $self->delimiter ) {
                $self->interpolate_variables();
            }
        }
    }

    # if specific element was requested attempt to look it up
    return $self->lookup(@_);
}

sub interpolate_variables
{
    my $self = shift;

    # if one delimiter is specified use it for both the start and stop
    my $start = ref( $self->delimiter ) ? $self->delimiter->[0] : $self->delimiter;
    my $stop  = ref( $self->delimiter ) ? $self->delimiter->[1] : $self->delimiter;
    my $var_re = qr/\Q$start\E([\w_>-]+)\Q$stop\E/;

    # walk the entire tree looking for variable replacements
    # when we find them attempt to do a lookup and replace the token with the value
    # keep going until no more substitions are performed or the max recursion is reached
    my $passes = 0;
    my $replaced;
    do {
        $replaced = 0;
        my $v = Data::Visitor::Callback->new(
            value => sub {
                return unless defined($_);
                s{$var_re}{
                    $replaced++;
                    $self->lookup( split '->', $1 );
                }egx;
            }
        );
        $v->visit( $self->data );
        $passes++;
    } while ( $replaced > 0 and $passes < $self->max_recursion );

}

sub lookup
{
    my $self = shift;

    # descend through the tree looking for the requested element
    my $ref = $self->data;
    for my $key (@_) {
        $ref = $ref->{$key};
    }

    return $ref;
}

1;

MyApp::Model::Resource

package MyApp::Model::Resource;

use Moose;
use namespace::autoclean;

use CatalystX::Model::Resource;
extends 'CatalystX::Model::Resource';

__PACKAGE__->config( filename => MyApp->path_to('root', 'resources', 'resources.yaml') );

resource.yaml

test:
    key: this is the value

Usage

sub home : Local {
    my ( $self, $c ) = @_;

    $self->stash->{thevalue} = $c->Model('Resource')->get('test', 'key');
}