package CGI::State;

use strict;
use integer;
use overload;
use CGI ();

use vars qw($VERSION);

$VERSION = (qw$Revision: 0.01 $)[-1];

#Returns a state hashref
sub state {
  my $class = shift;
  my $cgi   = shift;
  my $state = shift || {};

  #Alternative calling method is with a hash, rather
  #than a CGI object.  This allows validation before
  #building a multi-dimensional hash from submitted
  #values.
  if(ref $cgi eq 'HASH') {
    $cgi = CGI->new($cgi);
  }

  foreach my $param ($cgi->param) {

    my @words = split(/[\.\[\]]+/o, $param);
    my $node  = $state;

    for( my $w = 0;          #start at the first word
         $w < scalar @words; #go until the end
         $w++ ){             #increment the w count

      #If the next word is undefined, then we must
      #be looking at the last node.  If the next word
      #is a non-number, it must be a hashref, otherwise,
      #it is an arrayref.
      my $next = (not defined $words[$w + 1])
        ? $cgi->param($param)
        : $words[$w + 1] =~ /\D/o
          ? {}
          : [];

      #Figures out if this is a reference to a hash,
      #array, an object or even an over-loaded object.
      my $ref = ref $node;

      #If it's really necessary, then use overload::StrVal
      unless($ref eq 'HASH' or $ref eq 'ARRAY') {
        $ref = overload::StrVal($node) =~ /([^=]*)\(/o;
      }

      $node = ($ref eq 'HASH')
        ? ($node->{ $words[$w] } ||= $next)
        : ($node->[ $words[$w] ] ||= $next);
    }
  }

  return $state;
}

#Takes the state, and returns a CGI object representing
#a "flattened" representation of state.
sub cgi {
  my $class  = shift;
  my $state  = shift;
  my $cgi    = shift || CGI->new('');
  my $param  = shift || '';

  #Figures out if this is a reference to a hash,
  #array, an object or even an over-loaded object.
  if(my $ref = ref $state) {

    #If it's really necessary, then use overload::StrVal
    unless($ref eq 'HASH' or $ref eq 'ARRAY') {
      $ref = overload::StrVal($state) =~ /([^=]*)\(/o;
    }

    if($ref eq 'ARRAY') {

      for( my $index = 0;           #start at the first word
           $index < scalar @$state; #go until the end
           $index++ ){
        $class->cgi( $state->[$index], $cgi, "$param\[$index\]" );
      }

    } elsif($ref eq 'HASH') {

      $param .= '.' unless $param eq '';

      foreach my $key (keys %$state) {
        $class->cgi( $state->{$key}, $cgi, $param.$key );
      }

    }

  } elsif(defined $state and $param ne '') {

    $cgi->param($param, $state);

  }

  return $cgi;
}

1;

__END__

=pod

=head1 NAME

CGI::State - Converts CGI parameters into a multi-dimensional hash

=head1 SYNOPSIS

    First you make your HTML form.  For example:

      <form action="order.cgi">
        <input type="text" name="contact.first_name" value="Dan" />
        <input type="text" name="contact.email" value="dan@mealtips.com" />
        <input type="hidden" name="item[0].price" value="10.00" />
        <input type="hidden" name="item[0].description" value="Widget" />
        <input type="submit" value="Order Now!" />
      </form>

    Notice the names of the hidden and text fields? Keep this in mind.  Then
    you create a CGI script to receive the form variables after they're submitted:

    use strict;
    use CGI;
    use CGI::State;
    use Data::Dumper;

    # Simulate receiving CGI parameters
    my $cgi = CGI->new;

    # Un-Flatten the data structure
    my $state = CGI::State->state( $cgi );

    # Show us what the $state hashref now looks like...
    print $cgi->header('text/plain');
    print Data::Dumper->Dump([$state], [qw(state)])->Indent(3)->Quotekeys(0)->Dump;

    #Which would print out the following data structure:
    $state = {
               contact => {
                            first_name => 'Dan',
                            email => 'dan@mealtips.com'
                          },
               item => [
                         #0
                         {
                           description => 'Widget',
                           price => '10.00'
                         }
                       ]
             };

    #You can also change $state back into the original query string:
    my $cgi = CGI::State->cgi( $state );

    print $cgi->header('text/plain'), $cgi->query_string;

    #Which would print out:
    contact.first_name=Dan&contact.email=dan@mealtips.com&item[0].price=10.00&item[0].description=Widget

=head1 DESCRIPTION

This module was originally written because I always hated
receiving CGI parameters, putting them into a hash, and have
this hash contain 20 or 30 elements.  I think it is messy,
and very tedious writing code to group related items
together.  I wanted parameters to be put into a
"multi-dimensional" data structure automatically for me.

This module takes incoming CGI form variables, and translates
them into a multi-dimensional data structure.  It can create
hashes of hashes, hashes of arrays, arrays of hashes, etc,
any number of levels deep.

A downfall to CGI and HTML is their inability to naturally
group together submitted variables.  For example, you
can't have someone fill in an order form and have all
their contact and item information grouped seperate from
each other in the data structure, until now.

=head1 METHODS

=item CGI::State->state( \%hash )

This allows you to logically group together form
elements, so that when the CGI script receives them, it
has to do no logic of it's own to group things together.

This routine Cycles through all the form variables
and looks for the following format:

    $object_name[$index].$attribute <-- Multivalued

OR

    $object_name.$attribute         <-- Single valued

...And translates them into the following:

    $hashref = {

      $object_name => [

        #$index = 0
        { $attribute => $value },

        #$index = 1
        { $attribute => $value },

        #...

      ],
    }

It also puts all non-objects into the top-level
of the hashref.

The format I choose to describe the data structure closely
resembles Javascript's data structure.  I know, I know.
But the syntax is close to perl, and should be fairly
simple for most perl programmers to pick up.

=item CGI::State->cgi( $state )

This will return a "flattened" CGI object based on the values
referenced by $state.  Very useful for maintaining state
across various CGI invocations.

=head1 EXAMPLE USAGE

One major advantage to grouping parameters together in a
multi-dimensional data structure is that you have have
everything "map" into your database cleanly.

For example, let's say that I have a relational table
called  "contact", which stores information about
a customer.  Inside this table there are three
columns called first_name, last_name, and email.

Imagine there is a form where customer information
is collected, such as the following:

    <form action="save_customer.cgi">
      <input type="text" name="contact.first_name" />
      <input type="text" name="contact.last_name" />
      <input type="text" name="contact.email"  />
    </form>

When this form is submitted, we create a CGI.pm object
to capture the data, then pass this object off to
CGI::State->state, which returns a hash reference:

    my $cgi   = CGI->new;           #Create the CGI Object
    my $state = CGI::State( $cgi ); #$state is a hash reference

Assuming that I submit the form, the hash reference
would look like this, as shown by Data::Dumper:

    $state = {
               contact => {
                            first_name => 'Dan',
                            last_name  => 'Kubb'
                            email      => 'dan@mealtips.com'
                          },
    };

With this structure, it would be rather easy just
to pass off $state->{contact} to a subroutine
that inserts contact information into a database.
There's no sorting, grouping or hard-coding the
column names anywhere in your code!  I am a firm
believer that the database, table and column names
should dictate the HTML form parameter names, and
perl hash element names.  This module helps enforce
that and make it easier to write code that will
"map" HTML forms into a database with minimal effort.

=head1 LIMITATIONS

You will need to control the parameter  format passed into
your  script.  This means you will either need to edit the
HTML  yourself, or educate your web designers on  the
parameter format you are expecting.

Having the ability to manipulate the data structure with
simple HTML can either be seen as a benefit or a liability.
I find it a benefit, but I can imagine that others might not
see it that way, especially when non-programmers are
responsible for constructing all web forms.

Anything you expect to be a hash key should not have a number.

=head1 AUTHOR

Dan Kubb, dan@mealtips.com

=head1 SEE ALSO

CGI(1).

=cut