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

  • h2o - the Util::H2O entry point, plus the optional -lvalue behavior documented here.

  • d2o / o2d - objectify and de-objectify nested HASH/ARRAY mixtures.

  • INI helpers using Config::Tiny and YAML helpers using YAML.

  • opt2h2o and Getopt2h2o for Getopt::Long option specifications.

  • HTTPTiny2h2o for HTTP::Tiny response hashes and JSON content.

  • ddd and dddie for quick Data::Dumper debugging.

  • tr4h2o for normalizing hash keys into accessor-safe names.

WHICH FUNCTION SHOULD I USE?

  • Writing a constructor -> baptise

  • You already have a hashref and want accessors -> h2o

  • You want $o->foo = VALUE syntax -> add -lvalue

  • You have nested arrays and hashes -> d2o

  • Missing keys should read as undef -> d2o -autoundef or Getopt2h2o -autoundef

  • You need plain structures again -> o2h or o2d

  • INI configuration -> ini2h2o / h2o2ini

  • YAML -> yaml2h2o

  • HTTP::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, REF

  • ini2h2o -lvalue, FILENAME

  • ini2o -lvalue, FILENAME

  • yaml2h2o -lvalue, FILENAME_OR_YAML_STRING

  • yaml2o -lvalue, FILENAME_OR_YAML_STRING

  • HTTPTiny2h2o -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:

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>