Feeds:
Posts
Comments

Posts Tagged ‘yaml’

YAML Is Not Dead

Nor is it pining for the fjords.

First sign a technology is dying: supporters start writing articles stating [technology] is not dead.

Ron Savage asserts on my blog:

Whether you like YAML or not, it’s effectively dead and gone. Accept this reality and forget it.

  • No supporting argument
  • No Link
  • No alternative evidence

It’s the first time I’ve heard anyone say this, and normally I’d dismiss a naked assertion like this out of hand. But I’m not too invested in YAML at this point (although getting more invested all the time) and it is probably worth a few minutes digging to avoid several months of time wasted. Whether it is worth the extra minutes to write this blog post is debatable however :)

So first of all I search at duckduckgo.com for "YAML is dead". None of the first 30 links I glance at are talking about YAML’s moribundity.

Next I look at Stack Overflow. 170 questions tagged YAML vs 3,554 tagged JSON (10,978 tagged xml). But I’m not particularly worried about popularity, otherwise I’d be using Java over Perl right? A lot of the YAML questions are fairly recent and have responses.

That’s enough for me. It has parsers, a perl module and it looks far nicer than JSON. I’m going to stick with it.

Read Full Post »

Last time, I left you with a bit of a cliffhanger. I’d implemented fallbacks but I still wasn’t substituting variables which I promised for this time. It turns out that the solution is really simple.

my %matching_bracket = qw#{ } ( ) [ ]#;

sub param
{
    my ($self, $param, $full_alias, $base_ref) = @_;
    $full_alias = $self->{'full_alias'} if (! defined($full_alias));
    $base_ref = $self->{'base'} if (! defined($base_ref));
    my $alias_data = $self->get_alias_data($full_alias);

    my $retval = get_if_hash($alias_data, $param);
    if (! defined($retval)) {
        foreach my $fallback (@$base_ref) {
            if ($fallback eq '~VAR') {
                if (exists($self->{'vars'}{$param})) {
                    $retval = $self->{'vars'}{$param};
                    last;
                }
            } elsif ($fallback eq '~ENV') {
                if (exists($ENV{$param})) {
                    $retval = $ENV{$param};
                    last;
                }
            } else {
                $retval = $self->param($param,
                                       $fallback,
                                       $self->get_base($fallback));
                last if (defined($retval));
            }
        }
    }

    if (defined($retval)) {
         # Substitute any variables which are present
         $retval =~ s/\$([{([])
                        ([^}\])]+)
                        # $matching_bracket{\1}
                        [})\]]
                    /$self->param(lc($2))/egx;
    }

    return $retval;
}

I was disappointed that I couldn’t quickly get %matching_bracket to work, but for my purposes it doesn’t matter that much.

param() is fairly complex now. It recursively calls itself a few times to get the appropriate fallback and it is important to pass the correct $full_alias and $base_ref to ensure we are making progress towards the base case.

The result is pleasing.

$ perl MyConfig.pl db-test.yaml
Alias: database/saturn
server: DB_SATURN_SERVER
database: saturn
user: saturn_user
password: saturn_user_1
x:

Alias: database/mars
server: DB_MARS_SERVER
database: mars
user: mars_user
password: qwerty
x: y

Alias: databasky/neptune
AliasError: unable to find alias databasky

A couple of things I’m not completely happy with:

  • it should detect infinite recursion
  • %matching_bracket doesn’t work
  • the class name – would YAML::Extended be reasonable?

The Full Source

package MyConfig;

use strict;
use warnings;

use YAML;

sub get_if_hash
{
    my ($hash_ref, $key) = @_;
    if (defined($hash_ref) and
        ref($hash_ref) eq 'HASH' and
        exists $hash_ref->{$key})
    {
        return $hash_ref->{$key};
    }
    # Clarify that we are returning undef deliberately
    return undef;
}

sub new
{
    my ($class, $file) = @_;
    my $self = { yaml => YAML::LoadFile($file) };
    bless $self, $class;
    return $self;
}

sub alias_parts_iterator
{
    my ($self, $full_alias, $fn) = @_;

    my $partial = '';
    my $alias_data = $self->{'yaml'}{'aliases'};
    my @parts = split '/', $full_alias;
    foreach (@parts) {
        $partial .= '/' if length($partial);
        $partial .= (length($partial) ? '/' : '') . $_;
        if (! exists $alias_data->{$_}) {
            die "AliasError: unable to find alias $partial\n";
        }

        $alias_data = $alias_data->{$_};
        $fn->($self, $alias_data, $partial);
    }
}

sub get_base
{
    my ($self, $full_alias) = @_;

    my $base;
    $self->alias_parts_iterator(
        $full_alias,
        sub {
            my ($obj, $alias_data) = @_;
            my $_base = get_if_hash($alias_data, '_base');
            $base = $_base if (defined($_base));
        }
    );

    if (! defined($base)) {
        if (exists($self->{'yaml'}{'default'}{'_base'})) {
            $base = $self->{'yaml'}{'default'}{'_base'};
        } else {
            $base = [];
        }
    }

    return $base;
}

sub get_alias_data
{
    my ($self, $full_alias) = @_;

    my $alias_data;
    $self->alias_parts_iterator(
        $full_alias,
        sub {
            my ($obj, $_alias_data) = @_;
            $alias_data = $_alias_data;
        }
    );
    return $alias_data;
}

sub set_var
{
    my ($self, $var, $value) = @_;
    $self->{'vars'}{$var} = $value;
}

sub set_alias
{
    my ($self, $full_alias, $flags) = @_;

    $self->{'params'} = {};
    my $clear_flag = get_if_hash($flags, 'clear');
    $self->{'vars'} = {} unless defined($clear_flag) and $clear_flag eq 0;
    $self->{'full_alias'} = $full_alias;

    if ($full_alias =~ m{([^/]+$)}) {
        $self->set_var('alias', lc($1));
        $self->set_var('uc_alias', uc($1));
    }
    $self->{'base'} = $self->get_base($full_alias);
}

my %matching_bracket = qw#{ } ( ) [ ]#;

sub param
{
    my ($self, $param, $full_alias, $base_ref) = @_;
    $full_alias = $self->{'full_alias'} if (! defined($full_alias));
    $base_ref = $self->{'base'} if (! defined($base_ref));
    my $alias_data = $self->get_alias_data($full_alias);

    my $retval = get_if_hash($alias_data, $param);
    if (! defined($retval)) {
        foreach my $fallback (@$base_ref) {
            if ($fallback eq '~VAR') {
                if (exists($self->{'vars'}{$param})) {
                    $retval = $self->{'vars'}{$param};
                    last;
                }
            } elsif ($fallback eq '~ENV') {
                if (exists($ENV{$param})) {
                    $retval = $ENV{$param};
                    last;
                }
            } else {
                $retval = $self->param($param,
                                       $fallback,
                                       $self->get_base($fallback));
                last if (defined($retval));
            }
        }
    }

    if (defined($retval)) {
         # Substitute any variables which are present
         $retval =~ s/\$([{([])
                        ([^}\])]+)
                        # $matching_bracket{\1}
                        [})\]]
                    /$self->param(lc($2))/egx;
    }

    return $retval;
}

Read Full Post »

I really like YAML. But I often find myself adding perl-like variables and fallbacks to defaults. Does anyone else do that? Do people still use YAML or has everybody moved on to JSON?

Anyway, there may already be a module to do this, but the more the merrier right ;)

I have to work with a lot of databases, and there is often, but not always a pattern between the database, the username and the password1

Please note I’m not advocating putting plaintext passwords in a config file – that is generally a bad idea. This is just for the purposes of the example.

---
default:
    _base: [~VAR, ~ENV]

aliases:

    db:
        _base: []

        default:
            x: y

        other:
            _base: [db/default]

    database:
        _base: [~VAR, database/default, ~ENV]

        default:
            _base: []
            server: DB_$[UC_ALIAS]_SERVER
            database: $[ALIAS]
            user: $[ALIAS]_user
            password: $[USER]_1

        # --

        saturn: 1
        neptune: 1

        mars:
            _base: [~VAR, database/default, db/other]
            password: qwerty

So basically, this config file says that there are three databases – saturn, neptune and mars. Username is (for example) saturn_user and the password would be saturn_user_1 except for mars which has a special password of qwerty.

I’ll stick to a fairly standard Object Based approach.

package MyConfig;

use strict;
use warnings;

use YAML;

sub new
{
    my ($class, $file) = @_;
    # error checking elided
    my $self = { yaml => YAML::LoadFile($file) };
    bless $self, $class;
    return $self;
}

~VAR mentioned in the YAML above can be set by a member method.

sub set_var
{
    my ($self, $var, $value) = @_;
    $self->{'vars'}{$var} = $value;
}

Now, you can probably see my intent from the yaml above, that I’m using forward-slash as my namespace seperator and variables from more global namespaces are overridden by more local namespaces. Anyway, I need some way of iterating over these namespaces – I like closures for this sort of thing.

sub alias_parts_iterator
{
    my ($self, $full_alias, $fn) = @_;

    my $partial = '';
    my $alias_data = $self->{'yaml'}{'aliases'};
    my @parts = split '/', $full_alias;
    foreach (@parts) {
        $partial .= '/' if length($partial);
        $partial .= (length($partial) ? '/' : '') . $_;
        if (! exists $alias_data->{$_}) {
            die "AliasError: unable to find alias $partial\n";
        }

        $alias_data = $alias_data->{$_};
        $fn->($self, $alias_data, $partial);
    }
}

Getting the base is an important task as it determines the fallbacks. This uses the iterator mentioned above.

sub get_base
{
    my ($self, $full_alias) = @_;

    my $base;
    $self->alias_parts_iterator(
        $full_alias,
        sub {
            my ($obj, $alias_data) = @_;
            my $_base = get_if_hash($alias_data, '_base');
            $base = $_base if (defined($_base));
        }
    );

    if (! defined($base)) {
        if (exists($self->{'yaml'}{'default'}{'_base'})) {
            $base = $self->{'yaml'}{'default'}{'_base'};
        } else {
            $base = [];
        }
    }

    return $base;
}

I’m using a helper function here as even perl is not quite loose enough for me by default. Attempting index an undefined value as a hash reference is fatal. Anyway, that is fixable.

sub get_if_hash
{
    my ($hash_ref, $key) = @_;
    if (defined($hash_ref) and
        ref($hash_ref) eq 'HASH' and
        exists $hash_ref->{$key})
    {
        return $hash_ref->{$key};
    }
    # Clarify that we are returning undef deliberately
    return undef;
}

Getting the alias data also uses the iterator closure.

sub get_alias_data
{
    my ($self, $full_alias) = @_;

    my $alias_data;
    $self->alias_parts_iterator(
        $full_alias,
        sub {
            my ($obj, $_alias_data) = @_;
            $alias_data = $_alias_data;
        }
    );
    return $alias_data;
}

We will want to set which alias we are using before retrieving params. This will set a couple of convenience variables, ALIAS and UC_ALIAS. We’ll provide a flag parameter to avoid clearing any preset variables.

sub set_alias
{
    my ($self, $full_alias, $flags) = @_;

    $self->{'params'} = {};
    my $clear_flag = get_if_hash($flags, 'clear');
    $self->{'vars'} = {} unless defined($clear_flag) and $clear_flag eq 0;
    $self->{'full_alias'} = $full_alias;

    if ($full_alias =~ m{([^/]+$)}) {
        $self->set_var('alias', lc($1));
        $self->set_var('uc_alias', uc($1));
    }
    $self->{'base'} = $self->get_base($full_alias);
}

And after all that, we are able to retrieve the param, correctly with fallbacks.

Note: this is somewhat prone to infinite recursion if you are not careful with setting your _base parameters in the yaml. I had intended to detect this with $self->{'params'} but this overly bloats the code for the purposes of the post.

sub param
{
    my ($self, $param, $full_alias, $base_ref) = @_;
    $full_alias = $self->{'full_alias'} if (! defined($full_alias));
    $base_ref = $self->{'base'} if (! defined($base_ref));
    my $alias_data = $self->get_alias_data($full_alias);

    my $retval = get_if_hash($alias_data, $param);
    if (! defined($retval)) {
        foreach my $fallback (@$base_ref) {
            if ($fallback eq '~VAR') {
                if (exists($self->{'vars'}{$param})) {
                    $retval = $self->{'vars'}{$param};
                    last;
                }
            } elsif ($fallback eq '~ENV') {
                if (exists($ENV{$param})) {
                    $retval = $ENV{$param};
                    last;
                }
            } else {
                $retval = $self->param($param,
                                       $fallback,
                                       $self->get_base($fallback));
                last if (defined($retval));
            }
        }
    }
    return $retval;
}

A little test program to check it is working correctly at this stage.

my $file = shift;
my $cfg = MyConfig->new($file);

foreach my $alias (qw(database/saturn database/mars databasky/neptune)) {
    print "Alias: $alias\n";
    $cfg->set_alias($alias);
    foreach (qw(server database user password x)) {
        my $param = $cfg->param($_);
        $param = '' unless defined($param);
        print "$_: $param\n";
    }
    print "\n";
}

And the result. Looks okay to me.

$ perl MyConfig.pl db-test.yaml
Alias: database/saturn
server: DB_$[UC_ALIAS]_SERVER
database: $[ALIAS]
user: $[ALIAS]_user
password: $[USER]_1
x:

Alias: database/mars
server: DB_$[UC_ALIAS]_SERVER
database: $[ALIAS]
user: $[ALIAS]_user
password: qwerty
x: y

Alias: databasky/neptune
AliasError: unable to find alias databasky

Next time, we’ll talk about substituting those variables. I was going to mention it here, but this post is long enough already.


1. Well, not really, but for the purposes of the example…

Read Full Post »

Follow

Get every new post delivered to your Inbox.