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 — a bless-like constructor helper that also creates accessors

  • d2o / 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 it

  • Debugging 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 → baptise

  • You already have a hashref and want accessors → h2o

  • You have nested data (JSON / API / DB results) with arrays and hashes → d2o

  • You want missing keys to return undef without exists checks → add -autoundef (d2o or Getopt2h2o)

  • You need plain Perl structures again (serialization / frameworks) → o2h or o2d

  • INI config files → ini2h2o / h2o2ini

  • YAML files or YAML strings → yaml2h2o

  • HTTP::Tiny JSON responses → HTTPTiny2h2o

Minimal Cheatsheet

  • baptise — like bless, but also creates accessors

  • h2o — 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” helpers

  • o2h — turn an objectified top-level hash back into a plain hashref (useful before JSON encoding)

  • o2d — de-objectify a structure created by d2o (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 (uses YAML::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:

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>