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 Etokuhirom@gmail.comE =head1 SEE ALSO L, L L =cut