NAME
Util::H2O::More - Convenience utilities built on Util::H2O (baptise, d2o, INI/YAML/HTTP helpers, Getopt helpers)
SYNOPSIS
Below is an example of a traditional Perl OOP class constructor using baptise to define a set of default accessors, in addition to any that are created by virtue of the %opts passed.
use strict;
use warnings;
package Foo::Bar;
# exports 'h2o' also
use Util::H2O::More qw/baptise/;
sub new {
my $pkg = shift;
my %opts = @_;
# replaces bless, defines default accessors and creates
# accessors based on what's passed into %opts
my $self = baptise \%opts, $pkg, qw/bar haz herp derpes/;
return $self;
}
1;
Then in a caller script:
use strict;
use warnings;
use Foo::Bar;
my $foo = Foo::Bar->new(some => q{thing}, else => 4);
print $foo->some . qq{\n};
# set bar via default accessor
$foo->bar(1);
print $foo->bar . qq{\n};
# default accessors also available from the class defined above:
# $foo->haz, $foo->herp, $foo->derpes
#
# and from the supplied tuple:
# $foo->else
In most cases, baptise can be used as a drop-in replacement for bless.
For more examples, please look at the classes created for unit tests contained in t/lib.
DESCRIPTION
Util::H2O provides a compelling approach that allows one to incrementally add OOP-ish ergonomics into Perl without committing to a full OO framework. It makes dealing with HASH references much easier while still being idiomatic Perl.
Util::H2O::More is a toolbox built on that foundation. It provides:
baptise— abless-like constructor helper that also creates accessorsd2o/o2d— objectify and de-objectify arbitrarily nested structures (HASH/ARRAY mixtures)Cookbook helpers for configuration and interoperability (INI via Config::Tiny, YAML via YAML)
Command-line options helpers (
opt2h2o,Getopt2h2o)An HTTP::Tiny response helper (
HTTPTiny2h2o) that can decode JSON content and objectify itDebugging helpers (
ddd,dddie)Key normalization helper (
tr4h2o) for “non-compliant” hash keys
This module targets a practical problem: Perl programs frequently pass around ad-hoc hashrefs (and arrays of hashrefs) from config, DBI, JSON APIs, or small in-house services. Even when correct, code can become visually dense due to repeated ->{key} and ->[idx] access. These helpers aim to reduce that syntactic noise while keeping the data model the same.
WHICH FUNCTION SHOULD I USE?
If you are new to this module, this section answers the common question: “Which helper do I actually want here?”
Quick Decision Guide
Writing a constructor →
baptiseYou already have a hashref and want accessors →
h2oYou have nested data (JSON / API / DB results) with arrays and hashes →
d2oYou want missing keys to return undef without
existschecks → add-autoundef(d2oorGetopt2h2o)You need plain Perl structures again (serialization / frameworks) →
o2horo2dINI config files →
ini2h2o/h2o2iniYAML files or YAML strings →
yaml2h2oHTTP::Tiny JSON responses →
HTTPTiny2h2o
Minimal Cheatsheet
baptise— likebless, but also creates accessorsh2o— add accessors to a hashref (non-recursive unless you use Util::H2O flags)d2o— walk a whole structure (arrays + hashes) and objectify all hashrefs, plus array “container” helperso2h— turn an objectified top-level hash back into a plain hashref (useful before JSON encoding)o2d— de-objectify a structure created byd2o(arrays + hashes back to plain refs)
ANTI-EXAMPLE GALLERY: BRACE SOUP → CLEAN CODE
This gallery shows common Perl patterns written with traditional hash/array dereferencing, then the same behavior expressed using h2o, d2o, and friends from this module.
HTTP + JSON
Before: Brace-heavy dereferencing
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: HTTPTiny2h2o + d2o -autoundef
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: d2o for iteration
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 (INI)
Before
my $cfg = Config::Tiny->read('app.ini');
my $host = $cfg->{database}->{host};
my $port = $cfg->{database}->{port};
After: ini2h2o
my $cfg = ini2h2o 'app.ini';
my $host = $cfg->database->host;
my $port = $cfg->database->port;
PATHOLOGICAL EXAMPLE: 1:1 BRACE DEREF → ACCESSORS
This example preserves all logic, control flow, and ordering. The only change is replacing deref syntax with accessor calls via h2o and d2o -autoundef. No refactoring and no “clever” simplification is introduced.
Before: Brace-heavy dereferencing (original logic)
my $res = HTTP::Tiny->new->get($url);
die unless $res->{success};
my $data = decode_json($res->{content});
foreach my $user (@{ $data->{users} }) {
next unless exists $user->{profile};
next unless exists $user->{profile}->{active};
next unless $user->{profile}->{active};
next unless exists $user->{company};
next unless exists $user->{company}->{name};
foreach my $project (@{ $user->{projects} }) {
next unless exists $project->{status};
next unless $project->{status} eq 'active';
next unless exists $project->{meta};
next unless exists $project->{meta}->{title};
print
$user->{company}->{name}
. ": "
. $project->{meta}->{title}
. "\n";
}
}
After: Same logic, same flow, accessors only
my $res = h2o HTTP::Tiny->new->get($url);
die unless $res->success;
my $data = d2o -autoundef, decode_json($res->content);
foreach my $user ($data->users->all) {
next unless $user->profile;
next unless $user->profile->active;
next unless $user->profile->active;
next unless $user->company;
next unless $user->company->name;
foreach my $project ($user->projects->all) {
next unless $project->status;
next unless $project->status eq 'active';
next unless $project->meta;
next unless $project->meta->title;
print
$user->company->name
. ": "
. $project->meta->title
. "\n";
}
}
Quantifying what was removed
The transformation above removes (in this snippet):
Hash deref operators: 48 instances of
->{...}Array deref expressions: 6 instances of
@{ ... }Structural braces used only for access: 22 braces/brackets
Paired
exists+ deref checks replaced by safe accessor reads via-autoundef
In raw punctuation characters, that’s roughly 300+ characters of access-only syntax removed in a small example. In a larger file with hundreds of dereferences, this scales to kilobytes of Perl source not typed, not diffed, and not reviewed. Even when file size is irrelevant, cognitive load is not.
METHODS
baptise [-recurse] REF, PKG, LIST
Takes the same first two parameters as bless, with an additional list that defines a set of default accessors that do not rely on top-level keys of the provided hash reference.
In other words: it looks like bless, but you can also specify a list of methods you want available as accessors even if they are not present in the hash (or not present yet).
my $self = baptise \%opts, $class, qw/foo bar baz/;
The -recurse option
Like baptise, but creates accessors recursively for a nested hash reference. This uses h2o's -recurse flag.
Note: Accessors created in nested hashes are handled by h2o -recurse. Those nested hashes are blessed with Util::H2O’s internal package naming for recursive objects. That is expected behavior.
tr4h2o REF
Replaces all characters not considered legal for subroutine/accessor names with an underscore _, using:
tr/a-zA-Z0-9/_/c
It also preserves the original keys in a hash accessible via __og_keys.
Example (adapted from the Util::H2O cookbook):
use Util::H2O::More qw/h2o tr4h2o ddd/;
my $hash = { "foo bar" => 123, "quz-ba%z" => 456 };
my $obj = h2o tr4h2o $hash;
print $obj->foo_bar, $obj->quz_ba_z, "\n"; # prints "123456"
# inspect new structure
ddd $obj; # Data::Dumper::Dumper
ddd $obj->__og_keys; # original keys
Note: This helper is not recursive; recursive key-normalization would be better handled upstream in Util::H2O (e.g., via a dedicated flag).
Getopt2h2o [-autoundef], ARGV_REF, DEFAULTS_REF, LIST
Wrapper around the idiom enabled by opt2h2o. It also requires Getopt::Long and calls GetOptionsFromArray for you.
use Util::H2O::More qw/Getopt2h2o/;
my $opts = Getopt2h2o \@ARGV, { n => 10 }, qw/f=s n=i/;
The first argument is a reference to the @ARGV array (or equivalent). The second argument is the initial state of the hash to be objectified via h2o. The remaining arguments are standard Getopt::Long option specifications.
-autoundef
With -autoundef, missing options can be queried without inspecting the hash directly. This avoids patterns like:
exists $opts->{foo}
and enables:
if (not $opts->foo) { ... }
Example:
my $opts = Getopt2h2o -autoundef, \@ARGV, { n => 10 }, qw/f=s n=i verbose!/;
Negative option syntax (for example, verbose! supporting both --verbose and --no-verbose) is supported because the option specifications are passed directly to Getopt::Long.
Option arguments are not required options
An easy point of confusion in Getopt::Long syntax is the meaning of =. For example:
name=s
means that if --name is present, it requires a string argument. It does not mean that --name itself must appear on the command line.
Required-option validation therefore remains application logic. For example:
my $opts = Getopt2h2o \@ARGV, {}, qw/name=s water=s@/;
die qq{Missing --name\n} unless defined $opts->name;
Use defined rather than a simple truth test when false-looking values such as 0 are legitimate option values.
Parse failures and usage handling
Getopt2h2o is intended as the shortest path from a Getopt specification to an accessor-based object. At present it returns that object and does not separately expose the boolean return value from GetOptionsFromArray.
If an application needs to branch on option-parser failure, call opt2h2o directly and retain the normal Getopt::Long return value:
use Getopt::Long qw/GetOptionsFromArray/;
use Util::H2O::More qw/h2o opt2h2o/;
my @opt_spec = qw/name=s water=s@/;
my $opts = h2o {}, opt2h2o(@opt_spec);
GetOptionsFromArray(\@ARGV, $opts, @opt_spec)
or die qq{bad options\n};
This is also the useful form when parse failure should invoke Pod::Usage:
use Pod::Usage qw/pod2usage/;
GetOptionsFromArray(\@ARGV, $opts, @opt_spec)
or pod2usage(-exitval => 2);
Getopt::Long 2.32 and later also provide their own help integration. Programs that want the standard automatic help behavior may configure auto_help before parsing:
use Getopt::Long qw/:config auto_help/;
For custom --help and --man behavior, add those options to the normal option specification and call pod2usage explicitly. See "TESTABLE MODULINO RECIPE" for a complete example.
opt2h2o LIST
Takes a list of Getopt::Long option specs and extracts only the flag names so they can be passed to h2o to create accessors without duplicating lists.
use Getopt::Long qw//;
my @opts = qw/option1=s options2=s@ option3 option4=i o5|option5=s option6!/;
my $o = h2o {}, opt2h2o(@opts);
Getopt::Long::GetOptionsFromArray(\@ARGV, $o, @opts);
if ($o->option3) {
do_the_thing();
}
Defaults may be provided via the initial hashref:
my $o = h2o { option1 => q{foo} }, opt2h2o(@opts);
TESTABLE MODULINO RECIPE
A small command-line program can also be written as a modulino: a file that runs as a script but can be loaded as a package by a test. Util::H2O::More works well for this style without requiring a full object framework.
For the very shortest scripts, Getopt2h2o is convenient. For a modulino, using baptise plus opt2h2o directly has two advantages:
baptisereturns a fresh object that inherits from the modulino package, so normal package methods such asrunare available on the same object as the option accessors.Calling
GetOptionsFromArraydirectly preserves its success/failure result, which makes it straightforward to invokePod::Usageor another error handler.
The important detail is to create a new object for every parse. Do not keep a single lexical option object and repeatedly parse into it; a later invocation in the same interpreter could otherwise retain values from an earlier one.
Here is a complete example in the style of a traditional testable command-line modulino:
#!/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;
# Always parse into a fresh object.
my $self = $class->new;
GetOptionsFromArray($argv, $self, @opt_spec)
or pod2usage(
-exitval => 2,
-message => q{Use --help to list options},
);
pod2usage(-exitval => 0, -verbose => 1) if $self->help;
pod2usage(-exitval => 0, -verbose => 2) if $self->man;
# "name=s" requires a value when --name is used; it does not
# make --name mandatory. Validate mandatory options here.
pod2usage(
-exitval => 2,
-message => q{Missing --name},
) unless defined $self->name;
# Application defaults can still be applied after parsing.
$self->water([qw/still sparkling tap/])
unless defined $self->water and @{ $self->water };
return $self;
}
sub time_of_day {
my %hours = (
5 => q{morning},
12 => q{afternoon},
17 => q{evening},
21 => q{night},
);
for (sort { $b <=> $a } keys %hours) {
return $hours{$_} if (localtime)[2] >= $_;
}
return q{night};
}
sub run {
my $self = shift;
printf qq{Good %s, %s!\n}, $self->time_of_day, $self->name;
print qq{What kind of water would you like?\n};
print qq{- $_\n} for @{ $self->water };
return;
}
package main;
sub main {
modulinh2o->new_with_options->run;
return;
}
main() unless caller;
__END__
=head1 NAME
modulinh2o - small testable Util::H2O::More command-line program
=head1 SYNOPSIS
modulinh2o --name Aquarius --water sparkling --water still
modulinh2o --help
modulinh2o --man
=head1 OPTIONS
=over 4
=item B<--name STRING>
Name used in the greeting. Required.
=item B<--water STRING>
Water choice. May be repeated. Defaults to still, sparkling, and tap.
=item B<--help>
Show brief usage.
=item B<--man>
Show the full manual.
=back
The unit tests in this distribution include a smaller version of this pattern in t/lib/ModulinoH2O.pm and exercise both programmatic construction and repeated command-line construction. In particular, they verify that a second parse does not inherit option values from the first.
This recipe is deliberately small. If an application needs a declarative option framework where documentation, type constraints, coercions, and validation are all described as option metadata, a framework such as MooX::Options may be a better fit. Util::H2O::More intentionally keeps Getopt::Long's compact option-specification syntax rather than trying to replace such frameworks.
HTTPTiny2h2o [-autothrow], REF
Helper for dealing with HTTP::Tiny responses, which are typically hashrefs like:
{
success => 1,
status => 200,
content => q/some string, could be JSON, etc/,
...
}
If the response contains a content field, this helper attempts to decode that content as JSON (using JSON::MaybeXS) and, if successful, applies d2o -autoundef to the decoded structure. The response hashref itself is also objectified via h2o so you can call $res->success, $res->content, etc.
Happy-path usage:
my $res = HTTPTiny2h2o HTTP::Tiny->new->get($url);
die unless $res->success;
say $res->content->someField;
HTTPTiny2h2o may die
This method expects a proper hashref returned by HTTP::Tiny that includes a content key. If the input doesn’t look like that, it throws an exception.
JSON decode failure behavior and -autothrow
By default, JSON decode errors are caught and suppressed (the original content string remains accessible). If you want malformed JSON to raise an exception, use -autothrow:
local $@;
my $ok = eval {
HTTPTiny2h2o -autothrow, $res;
1;
};
if (not $ok) {
... # handle malformed JSON
}
Note on serialization formats
Currently, this helper only attempts JSON decoding. It does not check headers to determine content type; JSON validity is determined solely by decode_json.
yaml2h2o FILENAME_OR_YAML_STRING
Takes a single parameter that may be either:
A YAML filename (uses
YAML::LoadFile)A YAML string that begins with
---\n(usesYAML::Load)
YAML may contain multiple serialized objects separated by ---\n, so yaml2h2o returns a list of objects.
For example, if myfile.yaml contains two documents:
---
database:
host: localhost
---
devices:
thingy:
active: 1
Then:
my ($dbconfig, $devices) = yaml2h2o q{/path/to/myfile.yaml};
Each returned value has been passed through d2o, so nested hashrefs are objectified and array containers gain helper methods.
yaml2h2o may die
If the argument looks like neither a filename nor a YAML string beginning with ---\n, an exception is thrown.
yaml2o FILENAME
Alias to yaml2h2o for backward compatibility.
ini2h2o FILENAME
Takes the name of a file, uses Config::Tiny to read it, then converts the result into an accessor-based object using o2h2o.
Given an INI file:
[section1]
var1=foo
var2=bar
[section2]
var3=herp
var4=derp
You can do:
my $config = ini2h2o q{/path/to/config.ini};
say $config->section1->var1;
ini2o is provided as a backward-compat alias.
ini2o FILENAME
Backward-compatible alias for ini2h2o. New code may prefer the more explicit ini2h2o name.
h2o2ini REF, FILENAME
Takes an object created via ini2h2o and writes it back to disk in INI format using Config::Tiny.
my $config = ini2h2o q{/path/to/config.ini};
$config->section1->var1("some new value");
h2o2ini $config, q{/path/to/other.ini};
o2ini is provided as a backward-compat alias.
o2ini REF, FILENAME
Backward-compatible alias for h2o2ini. New code may prefer the more explicit h2o2ini name.
o2h2o REF
General helper to objectify an already-blessed config-like object by copying its top-level hash content into a new hash and applying h2o -recurse. This is useful for objects like those returned by Config::Tiny.
o2h REF
Uses Util::H2O::o2h and behaves identically to it, but adjusts $Util::H2O::_PACKAGE_REGEX to accept package names generated by baptise. A new plain hashref is returned.
This complements h2o / baptise when you need to serialize data (e.g. JSON encoding) and the encoder dislikes blessed references.
d2o [-autoundef] REF
Wrapper around h2o that traverses an arbitrarily complex Perl data structure, applying h2o to any HASH refs along the way, and blessing ARRAY refs as containers with helper methods.
A common use case is web APIs returning arrays of hashes:
my $array_of_hashes = JSON::decode_json $json;
d2o $array_of_hashes;
my $co = $array_of_hashes->[3]->company->name;
With d2o, you can navigate without manual deref punctuation, and arrays gain helpers such as all, get, count, etc.
-autoundef
If -autoundef is used, an AUTOLOAD is attached such that calling a method for a missing key returns undef (and attempts to set a missing key die).
This avoids patterns like:
exists $hash->{k}
Example:
my $ref = somecall(...);
d2o -autoundef, $ref;
foreach my $k (qw/foo bar baz/) {
say $ref->$k if $ref->$k;
}
Relationship to Util::H2O -arrays
As of Util::H2O 0.20, h2o supports an arrays-related modifier. In many cases, that may be sufficient for nested JSON-like structures. d2o exists largely because this module originally added deep traversal before that feature was known, and because d2o also blesses array containers and provides the vmethods described below.
o2d REF
Does for structures objectified with d2o what o2h does for objects created with h2o. It removes blessing from Util::H2O::... and Util::H2O::More::__a2o... references and returns plain refs.
a2o REF
Used internally to bless arrayrefs as containers and attach “virtual methods”. Exposed in case you find a use for it directly, but it is primarily an internal implementation detail of d2o.
ARRAY CONTAINER VIRTUAL METHODS
When d2o encounters arrayrefs, it blesses them as containers and attaches helper methods. This is intentionally “heavier” than base Util::H2O.
all
Returns a LIST of all items in the array container.
my @items = $root->teams->all;
get INDEX, i INDEX
Returns element at INDEX. i is a short alias for get.
my $x = $root->teams->get(0);
my $y = $root->teams->i(0);
This makes deeply nested reads more readable:
$data->company->teams->i(0)->members->i(0)->projects->i(0)->tasks->i(1)->status('Completed');
push LIST
Pushes items onto the container and applies d2o to anything pushed.
my @added = $root->items->push({ foo => 1 }, { foo => 2 });
say $root->items->get(0)->foo;
Items pushed are returned for convenience.
pop
Pops an element from the container. pop intentionally does NOT apply o2d.
unshift LIST
Like push, but on the near end. Applies d2o to items unshifted.
shift
Like pop, but on the near end. Does NOT apply o2d.
scalar, count
Returns the number of items in the container:
my $n = $root->items->count;
DEBUGGING METHODS
ddd LIST
Applies Data::Dumper::Dumper to each argument and prints to STDERR. Data::Dumper is loaded via require.
dddie LIST
Same as ddd, but dies afterward.
EXTERNAL METHODS
h2o
Because Util::H2O::More exports h2o as the basis for its operations, h2o is also available without qualifying its full namespace.
DEPENDENCIES
Util::H2O
Required. This module is effectively a convenience layer around h2o and o2h.
It also uses the state keyword, available in Perl ≥ 5.10.
Optional / conditional dependencies
Some helpers load external modules only when you call them:
Getopt2h2oloads Getopt::Longini2h2o/h2o2iniload Config::Tinyyaml2h2oloads YAMLHTTPTiny2h2oloads JSON::MaybeXSddd/dddieload Data::Dumper
BUGS
At the time of this release, there are no bugs listed on the GitHub issue tracker.
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
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>