Beefy Boxes and Bandwidth Generously Provided by pair Networks
Pathologically Eclectic Rubbish Lister
 
PerlMonks  

Re: How best to validate the keys of hashref arguments?

by haukex (Archbishop)
on Mar 16, 2017 at 09:39 UTC ( [id://1184851]=note: print w/replies, xml ) Need Help??


in reply to How best to validate the keys of hashref arguments?

If I just want to check that no unknown args are passed, I'll sometimes write something like this:

use Carp; my %FOO_KNOWN_ARGS = map {$_=>1} qw/ quz baz /; sub foo { my %args = @_; $FOO_KNOWN_ARGS{$_} or croak "Invalid argument '$_'" for keys %ar +gs; # ... } foo(quz=>1,baz=>1); foo(quz=>1,baz=>1,bar=>1); # dies

Sometimes, to prevent typos, I'll use restricted hashes via Hash::Util:

use Hash::Util qw/lock_ref_keys/; my $foo = {}; lock_ref_keys($foo, qw/ quz baz /); $foo->{quz} = 1; $foo->{bar} = 1; # dies

However, both of these solutions only cover validating the keys, not the values of the hashes.

For more advanced validation, I'd suggest Params::Validate.

Or, as you suggested, you can make a simple object, for this I'd recommend Moo over Moose, as the former is (with a few minor exceptions) a subset of the latter, and is more lightweight. You can implement constraints on the values manually using coderefs, or with something like Type::Tiny.

{ package Foo; use Moo; use Types::Standard qw/ Str Int /; use Type::Utils qw/ declare as where /; use namespace::clean; has foo => ( is=>'ro', required=>1, isa => sub { $_[0]=~/^bar\d+$/ or die "Invalid foo" } ); has bar => ( is=>'ro', required=>1, isa => Int ); my $Hostname = declare as Str, where { /^\w+(\.\w+)*$/ }; has quz => ( is=>'ro', required=>1, isa => $Hostname ); sub baz { my $self = shift; print "baz, my foo is ",$self->foo,"\n"; } } my $foo = Foo->new(foo=>'bar123', bar=>4, quz=>'aa.bb.cc'); $foo->baz; baz2($foo); baz2({x=>1}); # dies # possible alternative to Foo::baz use Scalar::Util qw/blessed/; sub baz2 { my $foo = shift; die "Not a Foo" unless blessed($foo) && $foo->isa('Foo'); print "baz2, the foo is ",$foo->foo,"\n"; }

Replies are listed 'Best First'.
Re^2: How best to validate the keys of hashref arguments?
by cbeckley (Curate) on Mar 16, 2017 at 16:54 UTC

    Wow, haukex, thank you. There's a lot of info in there. Restricted hashes from Hash::Util works beautifully and is exactly what I was looking for.

    For the curious, I added a sub to my ssh module:

    sub ops_new_cmd { my ($init_hash) = @_; my $new_cmd = {}; lock_ref_keys($new_cmd, qw(name user host key command status ssh_re +tcode ssh_retmsg output std_err cmd_ret_code cmd_ret_msg)); for my $k (keys %$init_hash) { $new_cmd->{$k} = $init_hash->{$k}; } return $new_cmd }

    And then invoking it, from my initial example:

    my @commands = ( ops_new_cmd({ name => 'command_name1', user => 'user1', host => 'server1.foo.com', command => 'rsync_command yadda yadda' }), ops_new_cmd({ name => 'command_name2', user => 'user2', host => 'server2.foo.com', command => 'rsync_command yadda yadda' }) );

    You are correct, however, I am going to need to add some validation to the values at some point. I'm leaning toward using OO. Thank you for the examples there as well.

    Thanks,
    cbeckley

      Ah, forgot to ask, in the previous node, the line of code:

      for my $k (keys %$init_hash) { $new_cmd->{$k} = $init_hash->{$k}; }

      Is there a more idiomatic way to say that?

      Thanks,
      cbeckley

        for my $k (keys %$init_hash) { $new_cmd->{$k} = $init_hash->{$k}; }
        Is there a more idiomatic way to say that?

        Several, but it seems to me you're just copying over every value from %$init_hash into %$new_cmd, so it seems easiest to write:

        my %new_cmd = %$init_hash;

        This creates the same shallow copy of the hash that your current code is creating. You can lock_keys afterwards and incorrect keys will still cause corresponding errors. In the interest of TIMTOWTDI, here's a couple other solutions, most of which would be better applicable if you only wanted to copy over a subset of the keys. Note that the latter three clobber the entire contents of the hash, which should be fine in your case:

        sub ops_new_cmd { my ($init_hash) = @_; my %new_cmd; # -OR- #my %new_cmd = %$init_hash; lock_keys(%new_cmd, qw/name user host command/); my @keyset = keys %$init_hash; #for my $k (@keyset) { $new_cmd{$k} = $init_hash->{$k}; } # -OR- #$new_cmd{$_} = $init_hash->{$_} for @keyset; # -OR- %new_cmd = map { $_=>$init_hash->{$_} } @keyset; # -OR- #use 5.020; # For Key/Value Slices #%new_cmd = %$init_hash{ @keyset }; # -OR- #use experimental 'postderef'; # For Postfix Deref + Key/Value Sli +ces #%new_cmd = $init_hash->%{ @keyset }; return \%new_cmd }

        As for locked hashes, there was some discussion on P5P recently about them, but even if something were to happen to this feature, tied hashes would be an easy replacement. For example, a quick search on CPAN shows Tie::Hash::FixedKeys. On the other hand, if you start taking locked hashes that seriously, it's probably better to start moving to OO.

        Update in response to your reply, since I should have included it here in the first place: Personally what I currently use locked hashes for is mostly typo prevention, which is helpful during development, but could also be removed later without really affecting the code.

        Like this perhaps ?

        #!perl use strict; use Hash::Util 'lock_keys'; use Data::Dumper ; # valid my $cmd = ops_new_cmd({ name=>1, user=>2, host=>3}); print Dumper $cmd; # not valid $cmd = ops_new_cmd({ name=>1, user=>2, hostt=>3}); sub ops_new_cmd { my ($init_hash) = @_; my @valid = qw( name user host key command status ssh_retcode ssh_retmsg output std_err cmd_ret_code cmd_ret_msg); + lock_keys(my %new_cmd,@valid); %new_cmd = %$init_hash; return \%new_cmd; }
        poj

Log In?
Username:
Password:

What's my password?
Create A New User
Domain Nodelet?
Node Status?
node history
Node Type: note [id://1184851]
help
Chatterbox?
and the web crawler heard nothing...

How do I use this?Last hourOther CB clients
Other Users?
Others studying the Monastery: (6)
As of 2024-04-23 08:00 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    No recent polls found