CatalystX-RequestModel

 view release on metacpan or  search on metacpan

lib/CatalystX/RequestModel.pm  view on Meta::CPAN

  my ($target, $type, @add) = @_;
  my $store = $Meta_Data{$target}{$type} ||= do {
    my @data;
    if (Moo::Role->is_role($target) or $target->can("${type}_metadata")) {
      $target->can('around')->("${type}_metadata", sub {
        my ($orig, $self) = (shift, shift);
        ($self->$orig(@_), @data);
      });
    } else {
      require Sub::Util;
      my $method = Sub::Util::set_subname "${target}::${type}_metadata" => sub { @data };
      no strict 'refs';
      *{"${target}::${type}_metadata"} = $method;
    }
    \@data;
  };

  push @$store, @add;
  return;
}

1;

=head1 NAME

CatalystX::RequestModel - Inflate Models from a Request Content Body or from URL Query Parameters

=head1 SYNOPSIS

An example Catalyst Request Model:

    package Example::Model::RegistrationRequest;

    use Moose;
    use CatalystX::RequestModel;

    extends 'Catalyst::Model';

    namespace 'person';
    content_type 'application/x-www-form-urlencoded';

    has username => (is=>'ro', property=>1);   
    has first_name => (is=>'ro', property=>1);
    has last_name => (is=>'ro', property=>1);
    has password => (is=>'ro', property=>1);
    has password_confirmation => (is=>'ro', property=>1);

    __PACKAGE__->meta->make_immutable();

Using it in a controller:

    package Example::Controller::Register;

    use Moose;
    use MooseX::MethodAttributes;

    extends 'Catalyst::Controller';

    sub root :Chained(/root) PathPart('register') CaptureArgs(0)  { }

    sub update :POST Chained('root') PathPart('') Args(0) Does(RequestModel) BodyModel(RegistrationRequest) {
      my ($self, $c, $request_model) = @_;
      ## Do something with the $request_model (instance of 'Example::Model::RegistrationRequest').
    }

    sub list :GET Chained('root') PathPart('') Args(0) Does(RequestModel) QueryModel(PagingModel) {
      my ($self, $c, $paging_model) = @_;
    }


    __PACKAGE__->meta->make_immutable;

Now if the incoming POST /update looks like this:

    .-------------------------------------+--------------------------------------.
    | Parameter                           | Value                                |
    +-------------------------------------+--------------------------------------+
    | person.username                     | jjn                                  |
    | person.first_name [multiple]        | 2, John                              |
    | person.last_name                    | Napiorkowski                         |
    | person.password                     | abc123                               |
    | person.password_confirmation        | abc123                               |
    '-------------------------------------+--------------------------------------'

The object instance C<$request_model> would look like:

    say $request_model->username;       # jjn
    say $request_model->first_name;     # John
    say $request_model->last_name;      # Napiorkowski

And if the incoming is GET /list looks like

    [debug] Query Parameters are:
    .-------------------------------------+--------------------------------------.
    | Parameter                           | Value                                |
    +-------------------------------------+--------------------------------------+
    | page                                | 2                                    |
    | status                              | active                               |
    '-------------------------------------+--------------------------------------'

The object instance C<$paging_model> would look like:

    say $paging_model->page;       # 2
    say $paging_model->status;     # 'active'


And C<$request_model> has additional helper public methods to query attributes marked as request
fields (via the C<property> attribute field) which you can read about below.

See L<CatalystX::RequestModel::ContentBodyParser::JSON> for an example of using this with JSON
request content.

=head1 DESCRIPTION

Dealing with incoming POSTed (or PUTed/ PATCHed, etc) request content bodies is one of the most common
code issues we have to deal with.  L<Catalyst> has generic capacities for handling common incoming
content types such as form URL encoded (common with HTML forms) and JSON as well as the ability to
add in parsing for other types of contents (see L<Catalyst#DATA-HANDLERS>).   However these parsers
only check that a given body content is well formed and not that it's valid for your given problem
domain.  Additionally I find that we spend a lot of code lines in controllers that are doing nothing
but munging and trying to wack incoming parameters into a form that can be actually used.

lib/CatalystX/RequestModel.pm  view on Meta::CPAN


    package Example::Model::AccountRequest::CreditCard;

    use Moose;
    use CatalystX::RequestModel;

    extends 'Catalyst::Model';

    has id => (is=>'ro', property=>1);
    has card_number => (is=>'ro', property=>1);
    has expiration => (is=>'ro', property=>1);

Now if your incoming request looks like this it will be parsed into a deep structure by the
correct body parser and mapped to the request object:

    .-------------------------------------+--------------------------------------.
    | Parameter                           | Value                                |
    +-------------------------------------+--------------------------------------+
    | person.username                     | jjn                                  |
    | person.first_name                   | John                                 |
    | person.last_name                    | Napiorkowski                         |
    | person.credit_cards[0].card_number  | 123123123123123                      |
    | person.credit_cards[0].expiration   | 3000-01-01                           |
    | person.credit_cards[0].id           | 1                                    |
    | person.credit_cards[1].card_number  | 4444445555556666                     |
    | person.credit_cards[1].expiration   | 4000-01-01                           |
    | person.credit_cards[1].id           | 2                           
    '-------------------------------------+--------------------------------------'

It would parse and inflate a request model like

    my $request_model = $c->model('AccountRequest');

    $request_model->username;                       # jjn
    $request_model->first_name;                     # John
    $request_model->last_name;                      # Napiorkowski
    $request_model->credit_cards->[0]->card_number; # 123123123123123
    $request_model->credit_cards->[0]->expiration;  # 3000-01-01
    $request_model->credit_cards->[1]->card_number; # 4444445555556666
    $request_model->credit_cards->[1]->expiration;  # 4000-01-01

Please note the difference between a request property that is marked as C<indexed> versus
C<always_array>.  An C<indexed> property is required to have an array value while C<always_array>
merely coerces a scalar to an array if the value isn't already an array.  You cannot use
C<indexed> and C<always_array> in the same request property.

B<NOTE> You can use the C<indexed> attribute property with simple scalar values as well as
deep structured objects.  See test cases for more.

Please see L<CatalystX::RequestModel::ContentBodyParser::JSON> for an example JSON request body
with nesting.  JSON is actually easier since we don't need a parsing convention to turn the
flat list you get with HTML Form post into a deep structure, nor deal with some of form posting's
idiocracies.

=head2 Endpoints with more than one request model

If an endpoint can handle more than one type of incoming content type you can define that
via the subroutine attribute and the code will pick the right one or throw an exception if none match
(See L</EXCEPTIONS> for more).

    sub update :POST Chained('root') PathPart('') Args(0) 
      Does(RequestModel) 
      RequestModel(RegistrationRequestForm) 
      RequestModel(RegistrationRequesJSON)
    {
      my ($self, $c, $request_model) = @_;
      ## Do something with the $request_model
    }

Also see L<Catalyst::ActionRole::RequestModel>.

=head1 QUERY PARAMETERS

See L<CatalystX::QueryModel>.

=head2 Requests with mixed query and body models

You might have a request that has both query parameters (via the URL) as well as a content body request.
In that case you make the content body request in the same way as you normally do and then add a second
request model that specifies the query parameters.  For example you might have a form post with mixed
query and body parameters.  You create your models as normal:

    package Example::Model::InfoQuery;

    use Moose;
    use CatalystX::QueryModel;

    extends 'Catalyst::Model';

    has page => (is=>'ro', required=>1, property=>1);  
    has offset => (is=>'ro', property=>1);
    has search => (is=>'ro', property=>1);

    __PACKAGE__->meta->make_immutable();

    package Example::Model::LoginRequest;

    use Moose;
    use CatalystX::RequestModel;

    extends 'Catalyst::Model';
    content_type 'application/x-www-form-urlencoded';

    has username => (is=>'ro', required=>1, property=>1);  
    has password => (is=>'ro', property=>1);

    __PACKAGE__->meta->make_immutable();

And in your action you list the request models:

    sub postinfo :Chained(/) Args(0) Does(RequestModel) RequestModel(LoginRequest) QueryModel(InfoQuery)  {
      my ($self, $c, $login_request, $info_query) = @_;
    }

Now if you get a request like this:

    [debug] Query Parameters are:
    .-------------------------------------+--------------------------------------.
    | Parameter                           | Value                                |
    +-------------------------------------+--------------------------------------+
    | offset                              | 100                                  |



( run in 1.648 second using v1.01-cache-2.11-cpan-39bf76dae61 )