NAME
Util::H2O::More - Convenience utilities built on Util::H2O (baptise, d2o, INI/YAML/HTTP helpers, Getopt helpers)
SYNOPSIS
Util::H2O::More extends Util::H2O with small helpers for constructors, deep data structures, configuration files, command-line options, HTTP responses, and debugging.
A traditional constructor using baptise can remain very small:
package Foo::Bar;
use strict;
use warnings;
use Util::H2O::More qw/baptise/;
sub new {
my $pkg = shift;
my %opts = @_;
return baptise \%opts, $pkg, qw/bar haz herp derpes/;
}
Then:
my $foo = Foo::Bar->new(some => q{thing}, else => 4);
say $foo->some;
$foo->bar(1); # normal setter syntax
say $foo->bar;
As of version 0.4.6, generated accessors may optionally be made Perl lvalue methods:
use Util::H2O::More qw/h2o/;
my $o = h2o -lvalue, {
count => 1,
text => q{foo},
};
$o->count = 42;
$o->count++;
$o->text .= q{bar};
$o->count(100); # the old setter form still works
-lvalue is deliberately opt-in. Existing calls without -lvalue retain their existing behavior.
DESCRIPTION
Util::H2O provides a compact way to turn hashrefs into objects with accessors. Util::H2O::More builds practical helpers around that idea while keeping the underlying data structures recognizable as normal Perl HASH and ARRAY references.
This module provides:
baptise- abless-like constructor helper that also creates accessors.h2o- the Util::H2O entry point, plus the optional-lvaluebehavior documented here.d2o/o2d- objectify and de-objectify nested HASH/ARRAY mixtures.INI helpers using Config::Tiny and YAML helpers using YAML.
opt2h2oandGetopt2h2ofor Getopt::Long option specifications.HTTPTiny2h2ofor HTTP::Tiny response hashes and JSON content.dddanddddiefor quick Data::Dumper debugging.tr4h2ofor normalizing hash keys into accessor-safe names.
WHICH FUNCTION SHOULD I USE?
Writing a constructor ->
baptiseYou already have a hashref and want accessors ->
h2oYou want
$o->foo = VALUEsyntax -> add-lvalueYou have nested arrays and hashes ->
d2oMissing keys should read as undef ->
d2o -autoundeforGetopt2h2o -autoundefYou need plain structures again ->
o2horo2dINI configuration ->
ini2h2o/h2o2iniYAML ->
yaml2h2oHTTP::Tiny JSON responses ->
HTTPTiny2h2o
LVALUE ACCESSORS
Why -lvalue is opt-in
A normal Util::H2O accessor is a getter/setter method. Conceptually it acts like:
sub {
my $self = shift;
$self->{foo} = shift if @_;
$self->{foo};
}
Under -lvalue, Util::H2O::More lets Util::H2O create, bless, lock, and otherwise configure the object normally, then replaces only the generated data accessors with equivalent :lvalue methods. The resulting method still supports the normal setter call, but its return value may also be used as a writable alias to the underlying hash slot.
This means all of these are valid:
my $o = h2o -lvalue, { foo => 1, text => q{a} };
$o->foo = 42;
$o->foo++;
$o->text .= q{bc};
$o->foo(100);
The default was intentionally not changed. Lvalue methods expose aliasing semantics that are stronger than an ordinary getter/setter interface, and code which does not request them should not acquire those semantics accidentally.
-lvalue placement with h2o
-lvalue is a Util::H2O::More wrapper option rather than an upstream Util::H2O option. Put it first, before normal h2o options:
my $o = h2o -lvalue, -recurse, { ... };
Do not write:
my $o = h2o -recurse, -lvalue, { ... }; # not supported
In the latter form -lvalue reaches Util::H2O itself and is rejected as an unknown option.
The More-specific options on baptise, d2o, Getopt2h2o, and HTTPTiny2h2o are parsed as leading flags and may be combined before the data argument as documented for those helpers.
Existing values and undef
An existing scalar slot is directly writable:
my $o = h2o -lvalue, { count => 1 };
$o->count++;
An existing slot whose value is undef is also writable:
my $o = h2o -lvalue, { value => undef };
$o->value = 123;
Assignment to undef also works in either form:
$o->value = undef;
$o->value(undef);
Additional accessors and missing hash keys
Util::H2O can create an accessor for an additional key that does not yet exist in the hash:
my $o = h2o -lvalue, {}, qw/future/;
A simple value read does not itself need to create that key:
my $copy = $o->future; # undef
exists $o->{future}; # false
Assignment through the lvalue does create the allowed key:
$o->future = 42;
exists $o->{future}; # true
This works with the normal locked-key behavior because future was declared as an allowed additional key.
Aliases versus copies
The important Perl-specific distinction is whether the result is consumed as a value or retained as an alias.
This takes a copy of the value:
my $copy = $o->foo;
$copy = 99; # does not change $o->foo
This retains an alias to the object's storage:
my $alias = \$o->foo;
$$alias = 99; # changes $o->foo
For a declared accessor whose key is not yet present, taking a reference may materialize the hash slot because Perl must provide storage for the alias:
my $o = h2o -lvalue, {}, qw/future/;
my $alias = \$o->future;
exists $o->{future}; # now true
Passing an lvalue accessor directly as a subroutine argument can likewise cause Perl to preserve alias semantics and materialize an otherwise absent allowed slot. If key existence itself matters, copy the value first:
my $value = $o->future;
some_sub($value);
rather than depending on the aliasing behavior of:
some_sub($o->future);
This behavior comes from Perl :lvalue semantics; it is not an extra storage layer implemented by this module.
Setter syntax remains supported
-lvalue does not replace the existing setter convention:
$o->foo(42);
The generated lvalue accessor intentionally retains the same rule as the normal Util::H2O accessor: if arguments are supplied, the first argument is stored and the resulting slot value is returned.
Custom methods are not changed
When -meth (or -classify) turns a CODE-valued hash member into a real method, that method is not replaced with an lvalue accessor:
my $o = h2o -lvalue, -meth, {
value => 2,
double => sub { $_[0]->value * 2 },
};
$o->value = 4; # OK
say $o->double; # 8
$o->double = 9; # error: double is not an lvalue accessor
This remains true even if the custom method name is also present in the list of additional keys.
AUTOLOAD is also deliberately never converted to an lvalue accessor. This is particularly important for -autoundef, where AUTOLOAD provides the missing-key policy rather than actual hash storage.
-ro is incompatible with -lvalue
-ro promises an immutable object while -lvalue promises writable aliases. Combining the two is therefore rejected explicitly:
h2o -lvalue, -ro, { foo => 1 };
The call dies with an error explaining that -lvalue and -ro cannot be combined. Normal h2o -ro behavior is unchanged when -lvalue is absent.
Recursion and newly assigned references
h2o -lvalue, -recurse makes generated accessors on recursively objectified HASH refs lvalues:
my $o = h2o -lvalue, -recurse, {
child => { count => 1 },
};
$o->child->count++;
As with upstream Util::H2O, -recurse alone does not descend into ARRAY refs. Use -arrays when that behavior is wanted:
my $o = h2o -lvalue, -arrays, {
rows => [ { count => 1 } ],
};
$o->rows->[0]->count++;
d2o -lvalue traverses the HASH/ARRAY mixtures handled by d2o, so generated HASH accessors throughout that structure are lvalues.
Assignment of a new reference does not automatically objectify that new value. This is the same rule as the existing setter form:
$o->child = { count => 1 };
After that assignment $o-child> is the plain hashref supplied by the caller; -lvalue does not secretly run h2o or d2o on every later assignment. Objectify the new value explicitly when that is desired.
Arrays and virtual methods
d2o -lvalue does not turn the ARRAY container methods themselves into lvalue methods. get, i, all, scalar, count, push, pop, unshift, and shift keep their existing method contracts.
For example, this remains invalid:
$rows->i(0) = $replacement;
Use normal ARRAY syntax if the array slot itself must be assigned:
$rows->[0] = $replacement;
The existing get/i behavior that returns undef for an out-of-range positive index without growing the array is also preserved.
When an ARRAY container was created by d2o -lvalue, HASH refs later supplied to its push or unshift methods are objectified with -lvalue as well, so the lvalue mode is not unexpectedly lost for newly added records.
-autoundef propagation for values later added by push/unshift is not changed by this release; only the new -lvalue mode is retained there.
-autoundef with -lvalue
-autoundef continues to distinguish existing accessors from truly unknown method names.
my $o = d2o -autoundef, -lvalue, {
present => 1,
};
$o->present++; # existing key: lvalue accessor
$o->missing; # unknown key: AUTOLOAD returns undef
The unknown name is not converted into a generated accessor. Consequently:
$o->missing(42);
still dies with the existing -autoundef "Won't set value for non-existing key" error, and:
$o->missing = 42;
fails with Perl's normal "Can't modify non-lvalue subroutine call" error. Neither form silently creates an unknown key.
Classified, baptised, and Getopt objects
Named -classify objects retain their constructor and custom methods while the generated data accessors become lvalues:
my $prototype = h2o -lvalue, -classify => 'Point', {}, qw/x y/;
my $point = Point->new(x => 1, y => 2);
$point->x++;
baptise accepts -lvalue as a leading More-specific option:
my $self = baptise -lvalue, \%opts, $class, qw/foo bar/;
and it can be combined with recursion:
my $self = baptise -lvalue, -recurse, \%opts, $class, qw/foo bar/;
Getopt2h2o also accepts -lvalue. Accessors for declared Getopt options are then writable as lvalues, including declared options which were not present on the command line:
my $opts = Getopt2h2o -lvalue, \@ARGV, {}, qw/count=i name=s/;
$opts->count++;
$opts->name = q{Perl};
With Getopt2h2o -autoundef, -lvalue, truly unknown method names continue to use the non-lvalue AUTOLOAD policy described above.
Other helpers which accept -lvalue
The following object-producing helpers accept -lvalue before their normal argument and propagate the mode into the H2O objects they create:
o2h2o -lvalue, REFini2h2o -lvalue, FILENAMEini2o -lvalue, FILENAMEyaml2h2o -lvalue, FILENAME_OR_YAML_STRINGyaml2o -lvalue, FILENAME_OR_YAML_STRINGHTTPTiny2h2o -lvalue, REF
HTTPTiny2h2o also propagates -lvalue to successfully decoded JSON content, which continues to receive -autoundef as before.
opt2h2o does not create an object itself, so it does not need a -lvalue option. Compose it with h2o instead:
my @spec = qw/name=s count=i/;
my $opts = h2o -lvalue, {}, opt2h2o(@spec);
h2o2ini and o2ini serialize existing objects and therefore do not accept -lvalue.
Validation caveat
The generated accessors in Util::H2O are intentionally simple and currently do not perform type validation or coercion. -lvalue therefore has no validation layer to bypass for those generated accessors today.
As a general Perl rule, however, an lvalue method exposes direct mutation of the returned storage location. If a class needs validation, coercion, triggers, or other setter-side behavior, use a real method or an OO framework for that field rather than turning that method into an lvalue. Util::H2O::More deliberately does not convert custom -meth methods for this reason.
ANTI-EXAMPLE GALLERY: BRACE SOUP TO CLEAN CODE
The helpers are primarily aimed at code that already deals in hashes and arrays and would benefit from less dereferencing punctuation without changing its data model.
HTTP and JSON
Before:
my $res = HTTP::Tiny->new->get($url);
die unless $res->{success};
my $data = decode_json($res->{content});
foreach my $item (@{ $data->{results} }) {
next unless $item->{meta};
my $id = $item->{meta}->{id};
foreach my $tag (@{ $item->{tags} }) {
next unless $tag->{enabled};
print "$id => $tag->{name}\n";
}
}
After:
my $res = HTTPTiny2h2o HTTP::Tiny->new->get($url);
die unless $res->success;
foreach my $item ($res->content->results->all) {
my $id = $item->meta->id or next;
foreach my $tag ($item->tags->all) {
next unless $tag->enabled;
say "$id => " . $tag->name;
}
}
DBI rows
Before:
while (my $row = $sth->fetchrow_hashref) {
next unless $row->{address};
my $city = $row->{address}->{city};
print "$row->{name} lives in $city\n";
}
After:
my @rows;
push @rows, $_ while ($_ = $sth->fetchrow_hashref);
my $data = d2o -autoundef, \@rows;
foreach my $row ($data->all) {
next unless $row->address;
say $row->name . " lives in " . $row->address->city;
}
Configuration
Before:
my $cfg = Config::Tiny->read('app.ini');
my $host = $cfg->{database}->{host};
After:
my $cfg = ini2h2o 'app.ini';
my $host = $cfg->database->host;
METHODS
baptise [-lvalue] [-recurse] REF, PKG, LIST
Takes the same first two data parameters as bless, with an additional list of default accessors that may exist even when the corresponding top-level hash keys do not yet exist.
my $self = baptise \%opts, $class, qw/foo bar baz/;
-recurse uses h2o -recurse so nested HASH refs receive accessors. -lvalue makes the generated accessors lvalues as described in "LVALUE ACCESSORS". The two More-specific flags may be supplied before the reference.
The object is blessed into a unique package beneath PKG and inherits from PKG, so ordinary package methods remain available.
tr4h2o REF
Replaces characters not suitable for accessor names with _ using:
tr/a-zA-Z0-9/_/c
The original key names are preserved under __og_keys.
my $hash = { "foo bar" => 123, "quz-ba%z" => 456 };
my $obj = h2o tr4h2o $hash;
say $obj->foo_bar;
say $obj->__og_keys->{foo_bar};
This helper is not recursive.
Getopt2h2o [-autoundef] [-lvalue], ARGV_REF, DEFAULTS_REF, LIST
Wraps the opt2h2o idiom, loads Getopt::Long, and calls GetOptionsFromArray for you.
my $opts = Getopt2h2o \@ARGV, { n => 10 }, qw/f=s n=i/;
The first data argument is the array reference to parse, the second is the initial/default hash, and the remainder are ordinary Getopt::Long option specifications.
-autoundef
-autoundef installs an AUTOLOAD handler so an unknown option accessor may be read as undef without piercing the hash with exists.
my $opts = Getopt2h2o -autoundef, \@ARGV, { n => 10 },
qw/f=s n=i verbose!/;
Attempts to use setter-call syntax on a truly unknown key still die.
-lvalue
-lvalue makes accessors for declared option names and default keys lvalues. It may be combined with -autoundef; the AUTOLOAD handler for undeclared names remains non-lvalue.
Option arguments are not required options
In Getopt::Long syntax:
name=s
means that if --name appears it requires a string argument. It does not make --name mandatory. Application-level required-option checks are still needed:
my $opts = Getopt2h2o \@ARGV, {}, qw/name=s water=s@/;
die qq{Missing --name\n} unless defined $opts->name;
Use defined when 0 or another false-looking value is legitimate.
Parse failures and usage handling
Getopt2h2o returns the object and does not separately return the boolean result from GetOptionsFromArray. When the parser result itself is required, use opt2h2o directly:
use Getopt::Long qw/GetOptionsFromArray/;
use Util::H2O::More qw/h2o opt2h2o/;
my @spec = qw/name=s water=s@/;
my $opts = h2o {}, opt2h2o(@spec);
GetOptionsFromArray(\@ARGV, $opts, @spec)
or die qq{bad options\n};
The same form works naturally with Pod::Usage and with Getopt::Long's auto_help configuration.
opt2h2o LIST
Extracts option names from Getopt::Long specifications so the list need not be duplicated when creating H2O accessors.
my @spec = qw/option1=s option2=s@ option3 option4=i o5|option5=s option6!/;
my $o = h2o {}, opt2h2o(@spec);
Getopt::Long::GetOptionsFromArray(\@ARGV, $o, @spec);
Aliases and negative-option syntax are left to Getopt::Long; this helper only extracts the accessor names.
TESTABLE MODULINO RECIPE
A small command-line application can be both executable and testable as a package. baptise plus opt2h2o is useful here because the option accessors and the application's normal methods can live on the same object while GetOptionsFromArray remains visible to the application.
The important rule is to create a fresh object for every parse. Reusing one option object can retain values between repeated invocations in the same interpreter.
A compact pattern is:
#!/usr/bin/env perl
use strict;
use warnings;
package modulinh2o;
use Getopt::Long qw/GetOptionsFromArray/;
use Pod::Usage qw/pod2usage/;
use Util::H2O::More qw/baptise opt2h2o/;
my @opt_spec = qw/
name=s
water=s@
help
man
/;
sub new {
my $class = shift;
my %opts = @_;
return baptise \%opts, $class, opt2h2o(@opt_spec);
}
sub new_with_options {
my ( $class, $argv ) = @_;
$argv = \@ARGV unless defined $argv;
my $self = $class->new; # fresh object for every parse
GetOptionsFromArray($argv, $self, @opt_spec)
or pod2usage(-exitval => 2);
pod2usage(-exitval => 0, -verbose => 1) if $self->help;
pod2usage(-exitval => 0, -verbose => 2) if $self->man;
pod2usage(-exitval => 2, -message => q{Missing --name})
unless defined $self->name;
$self->water([qw/still sparkling tap/])
unless defined $self->water and @{ $self->water };
return $self;
}
sub run {
my $self = shift;
say q{Hello, } . $self->name;
say $_ for @{ $self->water };
return;
}
package main;
sub main {
modulinh2o->new_with_options->run;
return;
}
main() unless caller;
The distribution's t/lib/ModulinoH2O.pm and t/13-modulino.t exercise this style, including repeated parses in one interpreter so option state cannot leak between invocations.
Applications which need declarative option metadata, types, coercions, or more extensive validation may prefer a framework such as MooX::Options.
HTTPTiny2h2o [-autothrow] [-lvalue], REF
Takes a HASH reference in the usual HTTP::Tiny response shape. The response itself is objectified. When content is non-empty, the helper attempts to decode it with JSON::MaybeXS and applies d2o -autoundef to successfully decoded data.
my $res = HTTPTiny2h2o HTTP::Tiny->new->get($url);
die unless $res->success;
say $res->content->someField;
With -lvalue, both generated response accessors and generated accessors in successfully decoded JSON content are lvalues.
-autothrow
By default a JSON decode exception is suppressed and the original content string remains in place. With -autothrow, the decoder exception propagates:
my $res = HTTPTiny2h2o -autothrow, $response;
-autothrow and -lvalue may be supplied together before the response hash.
Input errors
The helper dies unless its argument is a HASH reference containing a content key.
yaml2h2o [-lvalue], FILENAME_OR_YAML_STRING
Accepts either a YAML filename or a YAML string beginning with ---. Files are loaded with YAML::LoadFile; strings are loaded with YAML::Load. Multiple YAML documents are returned as a list, and each deserialized structure is passed through d2o.
my ($config) = yaml2h2o q{/path/to/config.yaml};
With -lvalue, d2o -lvalue is used for each deserialized structure.
The helper dies when its argument looks like neither an existing filename nor a YAML string beginning with ---.
yaml2o [-lvalue], FILENAME_OR_YAML_STRING
Backward-compatible alias for yaml2h2o.
ini2h2o [-lvalue], FILENAME
Loads a file with Config::Tiny and converts it using o2h2o.
my $config = ini2h2o q{/path/to/config.ini};
say $config->section1->var1;
With -lvalue, recursively generated configuration accessors are lvalues.
ini2o [-lvalue], FILENAME
Backward-compatible alias for ini2h2o.
h2o2ini REF, FILENAME
Writes an object created from an INI-like structure back to disk using Config::Tiny.
$config->section1->var1(q{new value});
h2o2ini $config, q{/path/to/other.ini};
o2ini REF, FILENAME
Backward-compatible alias for h2o2ini.
o2h2o [-lvalue], REF
Copies the top-level hash content of a config-like object and applies h2o -recurse. With -lvalue, the generated recursive accessors are lvalues.
The top-level copy is shallow, matching the historical implementation; nested references are the same references and are objectified in place by recursive H2O processing.
o2h REF
Delegates to Util::H2O::o2h after broadening $Util::H2O::_PACKAGE_REGEX so package names generated by baptise are recognized. It returns a new plain top-level hashref.
d2o [-autoundef] [-lvalue] REF
Traverses an arbitrarily nested data structure. HASH refs are objectified and ARRAY refs are blessed as containers with the virtual methods described below.
my $data = decode_json($json);
d2o $data;
say $data->teams->i(0)->name;
-autoundef adds the missing-key AUTOLOAD behavior described above. -lvalue makes generated HASH accessors writable lvalues while leaving ARRAY container methods and AUTOLOAD alone.
o2d REF
Returns plain structures corresponding to structures objectified by d2o. It removes the internal Util::H2O::... and Util::H2O::More::__a2o... blessing where supported, matching the historical d2o/o2d behavior.
a2o REF
Internal helper used by d2o to bless ARRAY refs and install the virtual methods below. It remains callable by its fully qualified name, but is not an exported public entry point.
ARRAY CONTAINER VIRTUAL METHODS
all
Returns all items in list context:
my @items = $root->teams->all;
get INDEX, i INDEX
i is an alias for get. An out-of-range positive index returns undef without growing the array.
my $item = $root->teams->i(0);
These methods remain non-lvalue even when the container came from d2o -lvalue.
push LIST
Pushes items after applying d2o to them. For a container created by d2o -lvalue, the new -lvalue mode is retained for newly objectified HASH items.
pop
Pops an item. It intentionally does not apply o2d; a popped object remains an object.
unshift LIST
Like push at the near end. It applies d2o, and retains -lvalue for a container created by d2o -lvalue.
shift
Like pop at the near end. It intentionally does not apply o2d.
scalar, count
Returns the number of elements. count is an alias for scalar.
DEBUGGING METHODS
ddd LIST
Loads Data::Dumper, applies Dumper to each argument, and prints the result to STDERR.
dddie LIST
Like ddd, then dies with a fixed debugging exception.
EXTERNAL METHODS
h2o
Util::H2O::More exports its wrapper for Util::H2O's h2o. Without -lvalue, the call is delegated directly to Util::H2O::h2o. With -lvalue as the first option, the generated accessor methods are decorated as described in "LVALUE ACCESSORS" after upstream H2O processing succeeds.
All ordinary upstream options continue to be interpreted by Util::H2O.
DEPENDENCIES
Util::H2O
Required. This module is a convenience layer around h2o and o2h. The distribution requires Util::H2O 0.24 or newer.
The implementation also uses state, available in Perl 5.10 and newer.
Optional / conditional dependencies
Some helpers load dependencies only when called:
Getopt2h2oloads Getopt::Longini2h2o/h2o2iniload Config::Tinyyaml2h2oloads YAMLHTTPTiny2h2oloads JSON::MaybeXSddd/dddieload Data::Dumper
BACKWARD COMPATIBILITY
-lvalue is opt-in. Existing h2o, baptise, d2o, configuration, YAML, HTTP, and Getopt calls without that flag retain their normal accessor behavior. Existing setter calls such as:
$o->foo(42);
remain supported on lvalue-enabled objects as well.
The ini2o, o2ini, and yaml2o names remain backward-compatible aliases.
BUGS
Please report issues on the GitHub issue tracker for this distribution.
LICENSE AND COPYRIGHT
Perl / Perl 5.
ACKNOWLEDGEMENTS
Thank you to HAUKEX for creating Util::H2O and hearing me out on its usefulness for some unintended use cases.
SEE ALSO
Util::H2O, Getopt::Long, Config::Tiny, YAML, HTTP::Tiny, JSON::MaybeXS.
This module was featured in the 2023 Perl Advent Calendar on December 22: https://perladvent.org/2023/2023-12-22.html.
AUTHOR
Oodler 577 <oodler@cpan.org>