Permalink
Browse files

Move group emptiness checking method to Form role

Add human_name to Field

Add secure_results_hash and all_errors methods to ResultSet

Add Storable hooks to fields

Add Chloro::Role::Error role for type constraint checking
  • Loading branch information...
1 parent 25a200d commit 0251c85ffef7e6c4573f57917004ebfe7514fc24 @autarch committed Apr 3, 2011
View
@@ -69,3 +69,202 @@ sub group {
}
1;
+
+# ABSTRACT: Form Processing So Easy It Will Knock You Out
+
+__END__
+
+=head1 SYNOPSIS
+
+ package MyApp::Form::Login;
+
+ use Chloro;
+
+ field username => (
+ isa => 'Str',
+ required => 1,
+ );
+
+ field password => (
+ isa => 'Str',
+ required => 1,
+ );
+
+ ...
+
+ sub login {
+ ...
+
+ my $form = MyApp::Form::Login->new();
+
+ my $resultset = $form->process( params => $submitted_params );
+
+ if ( $resultset->is_valid() ) {
+ my %login = $resultset->results_hash();
+
+ # Do something with $login{username} & $login{password}
+ }
+ else {
+ # Errors that are not specific to just one field
+ my @form_errors = $resulset->form_errors();
+
+ # Errors keyed by specific field names
+ my %field_errors = $resultset->field_errors();
+
+ # Do something with these errors
+ }
+ }
+
+=head1 DESCRIPTION
+
+B<This software is still very alpha, and the API may change without warning in
+future versions.>
+
+For a walkthrough of all this module's features, see L<Chloro::Manual>.
+
+Chloro is yet another in a long line of form processing libraries. It differs
+from other libraries in that it is entirely focused on defining forms in
+programmer terms. Field types are Moose type constraints, not HTML widgets
+("Str" not "Select").
+
+Chloro is focused on taking a browser's submission, doing basic validation,
+and returning a data structure that you can use for further processing.
+
+Out of the box, it does not talk to your database, nor does it know anything
+about rendering HTML. However, it is designed so that these features could be
+provided by extensions.
+
+=head1 DEFINING A BASIC FORM
+
+A form is defined as a unique class, so you might have C<MyApp::Form::Login>,
+C<MyApp::Form::User>, etc. To make a form class, just C<use Chloro>.
+
+This will make your class a form, and you also get all the exports from Moose,
+meaning you can define regular attributes, consume roles, etc.
+
+A form consists of one or more fields. A field is a name plus a data type, as
+well as some other optional parameters.
+
+ package MyApp::Form::User;
+
+ use Chloro;
+
+ field username => (
+ isa => 'Str',
+ required => 1,
+ );
+
+ field email_address => (
+ isa => 'EmailAddress',
+ required => 1,
+ );
+
+ field password => (
+ isa => 'Str',
+ secure => 1,
+ );
+
+ field password2 => (
+ isa => 'Str',
+ secure => 1,
+ );
+
+ sub _validate_form {
+ my $self = shift;
+ my $params = shift;
+
+ # Use a bare return if form is valid.
+ return if ...;
+
+ # Check that passwords are the same. Maybe check that password is
+ # present if required. Return a list of error messages.
+
+ return 'The two password fields must match.';
+ }
+
+=head2 Fields from Roles
+
+Currently, Chloro expects each form class to define all of its fields in the
+class itself. Allowing fields to be added by consuming roles is on my todo
+list.
+
+=head1 FIELDS
+
+A field requires a name and a type. The type is defined as a Moose type
+constraint, not as an HTML widget type. So for example, a field can be a
+C<Str>, C<Int>, C<ArrayRef[Int]>, or a L<DateTime>.
+
+Field values are extracted from the user-submitted params for processing. By
+default, the extractor looks for a key matching the field's name, but you can
+define your own extraction logic. For example, you could define a L<DateTime>
+field that looked for three separate keys, C<day>, C<month>, C<year>, and used
+those to construct a L<DateTime> object.
+
+Fields are declared with the C<field()> subroutine exported by Chloro. This
+subroutine allows the following parameters:
+
+=over 4
+
+=item * isa
+
+This must be a L<Moose> type constraint. This can be passed as a string, a
+type constraint object, or as a L<MooseX::Types> type.
+
+This type will be used to validate the field when it is submitted.
+
+=item * default
+
+The default value for the field. This can either be a non-reference scalar, or
+a subroutine reference.
+
+If this is a subroutine reference, it will be called as a method on the field
+object. It will also receive the parameters being processed and the field
+prefix as arguments.
+
+Field prefixes only matter for field groups, which are documented later.
+
+=item * required
+
+A field can be made required. If a required field is missing, the form
+submission is not valid.
+
+=item * secure
+
+XXX - do something with this
+
+=item * extractor
+
+This is a subroutine reference that defines how the field's value is extracted
+from the hash reference of parameters that a form processes.
+
+This subroutine will be called as a method on the L<Chloro::Field> object
+itself. It will receive three additional parameters.
+
+The first is a string containing the field's expected name in the
+parameters. For a simple field, this is the same as the field's name. For a
+field in a group, this key includes a prefix to uniquely identify that field.
+
+The second parameter is the hash reference of data submitted to the form. The
+third parameter is the form object itself.
+
+By default, the extractor simply return the given from the form data. You can
+override this to implement a more complex extraction strategy. For example,
+you might extract a date from three separate field (year, month, day).
+
+=item * validator
+
+This is a subroutine reference that defines how the field's value is validated.
+
+This subroutine will be called as a method on the L<Chloro::Field> object
+itself. It will receive three additional parameters.
+
+The first is the value being validated. The second parameter is the hash
+reference of data submitted to the form. The third parameter is the form
+object itself.
+
+By default, this simply uses the field's associated
+L<Moose::Meta::TypeConstraint> object to do validation, but you can add
+additional logic here. For example, you could validate that a start date is
+earlier than an end date.
+
+=back
@@ -8,11 +8,12 @@ use namespace::autoclean;
use Chloro::Field;
use Chloro::Types qw( NonEmptyStr );
+with 'Chloro::Role::Error';
+
has field => (
is => 'ro',
isa => 'Chloro::Field',
required => 1,
- weak_ref => 1,
);
has error => (
@@ -7,6 +7,8 @@ use namespace::autoclean;
use Chloro::Field;
+with 'Chloro::Role::Error';
+
has error => (
is => 'ro',
does => 'Chloro::Role::ErrorMessage',
View
@@ -39,13 +39,13 @@ has is_secure => (
has extractor => (
is => 'ro',
- isa => NonEmptySimpleStr | CodeRef,
+ isa => NonEmptySimpleStr,
default => 'extract_field_value',
);
has validator => (
is => 'ro',
- isa => NonEmptySimpleStr | CodeRef,
+ isa => NonEmptySimpleStr,
default => 'errors_for_field_value',
);
@@ -85,6 +85,36 @@ sub generate_default {
: $default;
}
+# The Storable hooks are needed because the Moose::Meta::TypeConstraint object
+# contains a code reference, and Storable will just die if it tries to
+# serialize it. So we save the type's _name_ and look that up when thawing.
+#
+# Unfortunately, this requires poking around in the object guts a little bit.
+sub STORABLE_freeze {
+ my $self = shift;
+
+ my %copy = %{$self};
+
+ my $type = delete $copy{type};
+
+ return q{}, \%copy, \( $type->name() );
+}
+
+sub STORABLE_thaw {
+ my $self = shift;
+ shift;
+ shift;
+ my $obj = shift;
+ my $type = shift;
+
+ %{$self} = %{$obj};
+
+ $self->{type}
+ = Moose::Util::TypeConstraints::find_or_create_type_constraint( ${$type} );
+
+ return;
+}
+
__PACKAGE__->meta()->make_immutable();
1;
View
@@ -5,8 +5,7 @@ use MooseX::StrictConstructor;
use namespace::autoclean;
-use Chloro::Types qw( ArrayRef CodeRef Str );
-use List::AllUtils qw( all );
+use Chloro::Types qw( ArrayRef CodeRef NonEmptySimpleStr Str );
with 'Chloro::Role::FormComponent';
@@ -27,31 +26,12 @@ has repetition_field => (
required => 1,
);
-my $_is_empty_checker = sub {
- my $self = shift;
- my $params = shift;
- my $prefix = shift;
- my $form = shift;
-
- return all { !( defined $params->{$_} && length $params->{$_} ) }
- map { join q{.}, $prefix, $_->name() } $self->fields();
-};
-
has is_empty_checker => (
is => 'ro',
- isa => CodeRef,
- default => sub {$_is_empty_checker},
+ isa => NonEmptySimpleStr,
+ default => 'group_is_empty',
);
-sub has_data_in_params {
- my $self = shift;
- my $params = shift;
- my $prefix = shift;
- my $form = shift;
-
- return ! $self->is_empty_checker()->( $self, $params, $prefix, $form );
-}
-
sub dump {
my $self = shift;
@@ -46,9 +46,12 @@ sub _build_is_valid {
}
sub key_value_pairs {
- my $self = shift;
+ my $self = shift;
+ my $skip_secure = shift;
- return map { $_->key_value_pairs() } $self->_result_values();
+ return map { $_->key_value_pairs() }
+ grep { $skip_secure ? !$_->field()->secure() : 1 }
+ $self->_result_values();
}
__PACKAGE__->meta()->make_immutable();
@@ -50,17 +50,32 @@ sub _build_is_valid {
sub results_hash {
my $self = shift;
+ return $self->_results_hash();
+}
+
+sub secure_results_hash {
+ my $self = shift;
+
+ return $self->_results_hash('skip secure');
+}
+
+sub _results_hash {
+ my $self = shift;
+ my $skip_secure = shift;
+
my %hash;
for my $result ( $self->_result_values() ) {
if ( $result->can('group') ) {
$hash{ $result->group()->name() }{ $result->key() }
- = { $result->key_value_pairs() };
+ = { $result->key_value_pairs($skip_secure) };
$hash{ $result->group()->repetition_field() }
= $self->_params()->{ $result->group()->repetition_field() };
}
else {
+ next if $skip_secure && $result->field()->is_secure();
+
%hash = ( %hash, $result->key_value_pairs() );
}
}
@@ -91,6 +106,14 @@ sub field_errors {
return %errors;
}
+sub all_errors {
+ my $self = shift;
+
+ my %field_errors = $self->field_errors();
+
+ return $self->form_errors(), map { @{$_} } values %field_errors;
+}
+
__PACKAGE__->meta()->make_immutable();
1;
@@ -0,0 +1,7 @@
+package Chloro::Role::Error;
+
+use Moose::Role;
+
+use namespace::autoclean;
+
+1;
Oops, something went wrong.

0 comments on commit 0251c85

Please sign in to comment.