package Data::NestedParams;
use 5.008005;
use strict;
use warnings FATAL => 'all';
our $VERSION = "0.07";
use parent qw(Exporter);
our @EXPORT = qw(expand_nested_params collapse_nested_params);
# https://github.com/rack/rack/blob/master/lib/rack/utils.rb#L90
# 9a74ba3b04f2dabe4741d2d82eae723d440c3aa2
sub parse {
my $k = shift;
my @keys;
while (1) {
if ($k =~ s/\[([a-zA-Z0-9_-]+)\]\z//) {
unshift @keys, ['%', $1];
} elsif ($k =~ s/\[\]\z//) {
unshift @keys, ['@'];
} else {
unshift @keys, ['$', $k];
last;
}
}
return @keys;
}
sub set_value {
my ($ret, $k, $v) = @_;
my @keys = parse($k);
my $r = \$ret;
while (@keys) {
my $key = shift @keys;
if ($key->[0] eq '%') {
if (@keys) {
$r = \(${$r}->{$key->[1]});
} else { # last
${$r}->{$key->[1]} = $v;
}
} elsif ($key->[0] eq '@') {
if (@keys) {
if (defined($$r)) {
if (ref($$r->[@$$r-1]) eq 'HASH' && $keys[0]->[0] eq '%' && not exists $$r->[@$$r-1]->{$keys[0]->[1]}) {
$r = \(${$r}->[@$$r-1]);
} else {
$r = \(${$r}->[0+@$$r]);
}
} else {
$r = \(${$r}->[0]);
}
} else { # last
push @{$$r}, $v;
}
} elsif ($key->[0] eq '$') {
if (@keys) {
$r = \(${$r}->{$key->[1]});
} else {
${$r}->{$key->[1]} = $v;
}
} else {
die "ABORT: $key->[0]";
}
}
}
sub expand_nested_params {
my $ary = shift;
my $ret = +{};
while (my ($k, $v) = splice @$ary, 0, 2) {
set_value($ret, $k, $v);
}
return $ret;
}
our $COLLAPSE_KEY;
sub _collapse {
my ($v, $r) = @_;
if (ref $v eq 'HASH') {
while (my ($k, $v) = each %$v) {
local $COLLAPSE_KEY = length($COLLAPSE_KEY) ? sprintf('%s[%s]', $COLLAPSE_KEY, $k) : $k;
_collapse($v, $r);
}
} elsif (ref $v eq 'ARRAY') {
for (@$v) {
local $COLLAPSE_KEY = $COLLAPSE_KEY . '[]';
_collapse($_, $r);
}
} elsif (!ref $v) {
$r->{$COLLAPSE_KEY} = $v;
} else {
my $ref = ref $v;
Carp::confess("${ref} is not supported by collapse_nested_params");
}
}
sub collapse_nested_params {
my $dat = shift;
if (ref $dat ne 'HASH') {
Carp::croak("The argument should be HashRef");
}
local $COLLAPSE_KEY = '';
my $r = +{};
_collapse($dat, $r);
return $r;
}
1;
__END__
=encoding utf-8
=head1 NAME
Data::NestedParams - entry[title]=foo&tags[]=art&tags[]=modern
=head1 SYNOPSIS
use Data::NestedParams;
my $expanded = expand_nested_params(
[
'entry[title]' => 'foo',
'tags[]' => 'art',
'tags[]' => 'modern',
]
);
# $expanded = { entry => {title => 'foo'}, tags => ['art', 'modern'] };
=head1 DESCRIPTION
Ruby on Rails has a nice feature to create nested parameters that help with the organization of data in a form - parameters can be an arbitrarily deep nested structure.
The way this structure is denoted is that when you construct a form the field names have a special syntax which is parsed.
=head1 LICENSE
Copyright (C) Tokuhiro Matsuno.
This library is free software; you can redistribute it and/or modify
it under the same terms as Perl itself.
=head1 AUTHOR
Tokuhiro Matsuno E<lt>tokuhirom@gmail.comE<gt>
=head1 SEE ALSO
L<Catalyst::Plugin::Params::Nested>, L<CGI::Expand>
L<https://github.com/rack/rack/blob/master/lib/rack/utils.rb#L90>
=cut