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. Usage:
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 (e.g. verbose! supporting both --verbose and --no-verbose) is supported.
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);
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.
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.
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>