# This file documents the revision history for Perl extension Catalyst.
-5.90090 - 2014-04-29
+5.90093 - 2015-05-29
+ - Fixed a bug where if you used $res->write and then $res->body, the
+ contents of body would be double encoded (gshank++).
+
+5.90092 - 2015-05-19
+ - Allows you to use a namespace suffix for request, response and stats
+ class traits. Docs and tests for this.
+ - Refactor the change introduced in 5.90091 to solve reported issues (for
+ example Catalyst::Controller::DBIC::API fails its tests) and to be a more
+ conservative refactor (new code more closely resembles the orginal code
+ that has proven to work for years.)
+
+5.90091 - 2015-05-08
+ - Fixed a bug where if an injected component expanded sub components, those
+ sub components would not show up in the startup debug dev console (
+ even though they were actually created).
+
+5.90090 - 2015-04-29
- Updated some documention in Catalyst::Request::Upload to clarify behavior
that RT ticket reported as confusing or unexpected
- Merged all changes from 5.90089_XXX development cycle.
sub composed_request_class {
my $class = shift;
my @traits = (@{$class->request_class_traits||[]}, @{$class->config->{request_class_traits}||[]});
+
+ # For each trait listed, figure out what the namespace is. First we try the $trait
+ # as it is in the config. Then try $MyApp::TraitFor::Request:$trait. Last we try
+ # Catalyst::TraitFor::Request::$trait. If none load, throw error.
+
+ my $trait_ns = 'TraitFor::Request';
+ my @normalized_traits = map {
+ Class::Load::load_first_existing_class($_, $class.'::'.$trait_ns.'::'. $_, 'Catalyst::'.$trait_ns.'::'.$_)
+ } @traits;
+
return $class->_composed_request_class ||
- $class->_composed_request_class(Moose::Util::with_traits($class->request_class, @traits));
+ $class->_composed_request_class(Moose::Util::with_traits($class->request_class, @normalized_traits));
}
has response => (
sub composed_response_class {
my $class = shift;
my @traits = (@{$class->response_class_traits||[]}, @{$class->config->{response_class_traits}||[]});
+
+ my $trait_ns = 'TraitFor::Response';
+ my @normalized_traits = map {
+ Class::Load::load_first_existing_class($_, $class.'::'.$trait_ns.'::'. $_, 'Catalyst::'.$trait_ns.'::'.$_)
+ } @traits;
+
return $class->_composed_response_class ||
- $class->_composed_response_class(Moose::Util::with_traits($class->response_class, @traits));
+ $class->_composed_response_class(Moose::Util::with_traits($class->response_class, @normalized_traits));
}
has namespace => (is => 'rw');
our $DETACH = Catalyst::Exception::Detach->new;
our $GO = Catalyst::Exception::Go->new;
-#I imagine that very few of these really need to be class variables. if any.
+#I imagine that very few of these really
+#need to be class variables. if any.
#maybe we should just make them attributes with a default?
__PACKAGE__->mk_classdata($_)
for qw/components arguments dispatcher engine log dispatcher_class
sub composed_stats_class {
my $class = shift;
my @traits = (@{$class->stats_class_traits||[]}, @{$class->config->{stats_class_traits}||[]});
+
+ my $trait_ns = 'TraitFor::Stats';
+ my @normalized_traits = map {
+ Class::Load::load_first_existing_class($_, $class.'::'.$trait_ns.'::'. $_, 'Catalyst::'.$trait_ns.'::'.$_)
+ } @traits;
+
return $class->_composed_stats_class ||
- $class->_composed_stats_class(Moose::Util::with_traits($class->stats_class, @traits));
+ $class->_composed_stats_class(Moose::Util::with_traits($class->stats_class, @normalized_traits));
}
__PACKAGE__->_encode_check(Encode::FB_CROAK | Encode::LEAVE_SRC);
# Remember to update this in Catalyst::Runtime as well!
-our $VERSION = '5.90090';
+our $VERSION = '5.90093';
$VERSION = eval $VERSION if $VERSION =~ /_/; # numify for warning-free dev releases
sub import {
: $class->log->debug(q/Couldn't find home/);
my $column_width = Catalyst::Utils::term_width() - 8 - 9;
+
my $t = Text::SimpleTable->new( [ $column_width, 'Class' ], [ 8, 'Type' ] );
for my $comp ( sort keys %{ $class->components } ) {
my $type = ref $class->components->{$comp} ? 'instance' : 'class';
# Set the charset if necessary. This might be a bit bonkers since encodable response
# is false when the set charset is not the same as the encoding mimetype (maybe
# confusing action at a distance here..
- # Don't try to set the charset if one already exists
+ # Don't try to set the charset if one already exists or if headers are already finalized
$c->res->content_type($c->res->content_type . "; charset=" . $c->encoding->mime_name)
- unless($c->res->content_type_charset);
+ unless($c->res->content_type_charset ||
+ ($c->res->_context && $c->res->finalized_headers && !$c->res->_has_response_cb));
}
}
=head2 $app->request_class_traits
-An arrayref of L<Moose::Role>s which are applied to the request class.
+An arrayref of L<Moose::Role>s which are applied to the request class. You can
+name the full namespace of the role, or a namespace suffix, which will then
+be tried against the following standard namespace prefixes.
+
+ $MyApp::TraitFor::Request::$trait_suffix
+ Catalyst::TraitFor::Request::$trait_suffix
+
+So for example if you set:
+
+ MyApp->request_class_traits(['Foo']);
+
+We try each possible role in turn (and throw an error if none load)
+
+ Foo
+ MyApp::TraitFor::Request::Foo
+ Catalyst::TraitFor::Request::Foo
+The namespace part 'TraitFor::Request' was choosen to assist in backwards
+compatibility with L<CatalystX::RoleApplicator> which previously provided
+these features in a stand alone package.
+
=head2 $app->composed_request_class
This is the request class which has been composed with any request_class_traits.
=head2 $app->response_class_traits
-An arrayref of L<Moose::Role>s which are applied to the response class.
+An arrayref of L<Moose::Role>s which are applied to the response class. You can
+name the full namespace of the role, or a namespace suffix, which will then
+be tried against the following standard namespace prefixes.
+
+ $MyApp::TraitFor::Response::$trait_suffix
+ Catalyst::TraitFor::Response::$trait_suffix
+
+So for example if you set:
+
+ MyApp->response_class_traits(['Foo']);
+
+We try each possible role in turn (and throw an error if none load)
+
+ Foo
+ MyApp::TraitFor::Response::Foo
+ Catalyst::TraitFor::Responset::Foo
+
+The namespace part 'TraitFor::Response' was choosen to assist in backwards
+compatibility with L<CatalystX::RoleApplicator> which previously provided
+these features in a stand alone package.
+
=head2 $app->composed_response_class
# of named components in the configuration that are not actually existing (not a
# real file).
- my @injected_components = $class->setup_injected_components;
+ my @injected = $class->setup_injected_components;
# All components are registered, now we need to 'init' them.
- foreach my $component_name (@comps, @injected_components) {
+ foreach my $component_name (@comps, @injected) {
$class->components->{$component_name} = $class->components->{$component_name}->() if
(ref($class->components->{$component_name}) || '') eq 'CODE';
}
$class->config->{inject_components}->{$injected_comp_name});
}
- return @injected_components;
+ return map { $class ."::" . $_ }
+ @injected_components;
}
=head2 $app->setup_injected_component( $injected_component_name, $config )
=head2 $app->composed_stats_class
-this is the stats_class composed with any 'stats_class_traits'.
+this is the stats_class composed with any 'stats_class_traits'. You can
+name the full namespace of the role, or a namespace suffix, which will then
+be tried against the following standard namespace prefixes.
+
+ $MyApp::TraitFor::Stats::$trait_suffix
+ Catalyst::TraitFor::Stats::$trait_suffix
+
+So for example if you set:
+
+ MyApp->stats_class_traits(['Foo']);
+
+We try each possible role in turn (and throw an error if none load)
+
+ Foo
+ MyApp::TraitFor::Stats::Foo
+ Catalyst::TraitFor::Stats::Foo
+
+The namespace part 'TraitFor::Stats' was choosen to assist in backwards
+compatibility with L<CatalystX::RoleApplicator> which previously provided
+these features in a stand alone package.
=head2 $c->use_stats
This is an overview of the user-visible changes to Catalyst between major
Catalyst releases.
+=head2 VERSION 5.90091
+
+=head3 'case_sensitive' configuration
+
+At one point in time we allowed you to set a 'case_sensitive' configuraion value so
+that you could find actions by their private names using mixed case. We highly
+discourage that. If you are using this 'feature' you should be on notice that we
+plan to remove the code around it in the near future.
+
=head2 VERSION 5.90090+
-=head2 Type constraints on Args and CaptureArgs.
+=head3 Type constraints on Args and CaptureArgs.
You may now use a type constraint (using L<Moose>, L<MooseX::Types> or preferably
L<Type::Tiny> in your Args or CaptureArgs action attributes. This can be used
See L<Catalyst::RouteMatching> for more.
-=head2 Move CatalystX::InjectComponent into core
+=head3 Move CatalystX::InjectComponent into core
L<Catalyst::Utils> has a new method 'inject_component' which works the same as the method of
the same name in L<CatalystX::InjectComponent>.
-=head2 inject_components
+=head3 inject_components
New configuration key allows you to inject components directly into your application without
any subclasses. For example:
Injected components are useful to reduce the ammount of nearly empty boilerplate classes
you might have, particularly when first starting an application.
-=head2 Component setup changes.
+=head3 Component setup changes.
Previously you could not depend on an application scoped component doing setup_components
since components were setup 'in order'. Now all components are first registered and then
}
else {
- # Case where body was set afgter calling ->write. We'd prefer not to
+ # Case where body was set after calling ->write. We'd prefer not to
# support this, but I can see some use cases with the way most of the
- # views work.
-
- $self->write($c, $body );
+ # views work. Since body has already been encoded, we need to do
+ # an 'unencoded_write' here.
+ $self->unencoded_write( $c, $body );
}
}
$c->response->write($buffer);
}
+=head2 $self->unencoded_write($c, $buffer)
+
+Writes the buffer to the client without encoding. Necessary for
+already encoded buffers. Used when a $c->write has been done
+followed by $c->res->body.
+
+=cut
+
+sub unencoded_write {
+ my ( $self, $c, $buffer ) = @_;
+
+ $c->response->unencoded_write($buffer);
+}
+
=head2 $self->read($c, [$maxlength])
Reads from the input stream by calling C<< $self->read_chunk >>.
return $len;
}
+sub unencoded_write {
+ my ( $self, $buffer ) = @_;
+
+ # Finalize headers if someone manually writes output
+ $self->_context->finalize_headers unless $self->finalized_headers;
+
+ $buffer = q[] unless defined $buffer;
+
+ my $len = length($buffer);
+ $self->_writer->write($buffer);
+
+ return $len;
+}
+
sub finalize_headers {
my ($self) = @_;
return;
# Remember to update this in Catalyst as well!
-our $VERSION = '5.90090';
+our $VERSION = '5.90093';
$VERSION = eval $VERSION if $VERSION =~ /_/; # numify for warning-free dev releases
=head1 NAME
response. However since you've already started streaming this will not show up as an HTTP error
status code, but rather error information in your body response and an error in your logs.
+B<NOTE> If you use ->body AFTER using ->write (for example you may do this to write your HTML
+HEAD information as fast as possible) we expect the contents to body to be encoded as it
+normally would be if you never called ->write. In general unless you are doing weird custom
+stuff with encoding this is likely to just already do the correct thing.
+
The second way to stream a response is to get the response writer object and invoke methods
on that directly:
sub a { 'a' }
sub b { 'b' }
+
+ package Catalyst::TraitFor::Request::Foo;
+ use Moose::Role;
+
+ sub c { 'c' }
+
+ package TestApp::TraitFor::Request::Bar;
+ use Moose::Role;
+
+ sub d { 'd' }
+
+ package Catalyst::TraitFor::Response::Foo;
+ use Moose::Role;
+
+ sub c { 'c' }
+
+ package TestApp::TraitFor::Response::Bar;
+ use Moose::Role;
+
+ sub d { 'd' }
}
{
use Catalyst;
- __PACKAGE__->request_class_traits([qw/TestRole/]);
- __PACKAGE__->response_class_traits([qw/TestRole/]);
+ __PACKAGE__->request_class_traits([qw/TestRole Foo Bar/]);
+ __PACKAGE__->response_class_traits([qw/TestRole Foo Bar/]);
__PACKAGE__->stats_class_traits([qw/TestRole/]);
__PACKAGE__->setup;
is $c->req->a, 'a';
is $c->req->b, 'b';
+is $c->req->c, 'c';
+is $c->req->d, 'd';
is $c->res->a, 'a';
is $c->res->b, 'b';
+is $c->res->c, 'c';
+is $c->res->d, 'd';
done_testing;
use strict;
use warnings;
use Test::More;
-use Catalyst::Utils;
use FindBin;
use lib "$FindBin::Bin/lib";
BEGIN {
-package RoleTest1;
+ package RoleTest1;
+ use Moose::Role;
-use Moose::Role;
+ sub aaa { 'aaa' }
-sub aaa { 'aaa' }
+ $INC{'RoleTest1.pm'} = __FILE__;
-package RoleTest2;
+ package RoleTest2;
+ use Moose::Role;
-use Moose::Role;
+ sub bbb { 'bbb' }
-sub bbb { 'bbb' }
+ $INC{'RoleTest2.pm'} = __FILE__;
-package Model::Banana;
-
-use base qw/Catalyst::Model/;
+ package Model::Banana;
+ use base qw/Catalyst::Model/;
-package Model::BananaMoose;
-
-use Moose;
-extends 'Catalyst::Model';
+ $INC{'Model/Banana.pm'} = __FILE__;
-Model::BananaMoose->meta->make_immutable;
+ package Model::BananaMoose;
-package TestCatalyst; $INC{'TestCatalyst.pm'} = 1;
-
-use Catalyst::Runtime '5.70';
-
-use Moose;
-BEGIN { extends qw/Catalyst/ }
-
-use Catalyst;
-
-after 'setup_components' => sub {
- my $self = shift;
- Catalyst::Utils::inject_component( into => __PACKAGE__, component => 'Model::Banana' );
- Catalyst::Utils::inject_component( into => __PACKAGE__, component => 'Test::Apple' );
- Catalyst::Utils::inject_component( into => __PACKAGE__, component => 'Model::Banana', as => 'Cherry' );
- Catalyst::Utils::inject_component( into => __PACKAGE__, component => 'Model::BananaMoose', as => 'CherryMoose', traits => ['RoleTest1', 'RoleTest2'] );
- Catalyst::Utils::inject_component( into => __PACKAGE__, component => 'Test::Apple', as => 'Apple' );
- Catalyst::Utils::inject_component( into => __PACKAGE__, component => 'Test::Apple', as => 'Apple2', traits => ['RoleTest1', 'RoleTest2'] );
-};
-
-TestCatalyst->config( 'home' => '.' );
-
-TestCatalyst->setup;
-
+ use Moose;
+ extends 'Catalyst::Model';
+
+ Model::BananaMoose->meta->make_immutable;
+ $INC{'Model/BananaMoose.pm'} = __FILE__;
+}
+
+{
+ package TestCatalyst;
+ $INC{'TestCatalyst.pm'} = __FILE__;
+
+ use Moose;
+ use Catalyst;
+ use Catalyst::Utils;
+
+ after 'setup_components' => sub {
+ my $self = shift;
+ Catalyst::Utils::inject_component( into => __PACKAGE__, component => 'Model::Banana' );
+ Catalyst::Utils::inject_component( into => __PACKAGE__, component => 'Test::Apple' );
+ Catalyst::Utils::inject_component( into => __PACKAGE__, component => 'Model::Banana', as => 'Cherry' );
+ Catalyst::Utils::inject_component( into => __PACKAGE__, component => 'Model::BananaMoose', as => 'CherryMoose', traits => ['RoleTest1', 'RoleTest2'] );
+ Catalyst::Utils::inject_component( into => __PACKAGE__, component => 'Test::Apple', as => 'Apple' );
+ Catalyst::Utils::inject_component( into => __PACKAGE__, component => 'Test::Apple', as => 'Apple2', traits => ['RoleTest1', 'RoleTest2'] );
+ };
+
+ TestCatalyst->config( 'home' => '.' );
+ TestCatalyst->setup;
}
-
-package main;
use Catalyst::Test qw/TestCatalyst/;
$c->response->body($contents);
}
+ sub write_then_body :Local {
+ my ($self, $c) = @_;
+
+ $c->res->content_type('text/html');
+ $c->res->write("<p>This is early_write action ♥</p>");
+ $c->res->body("<p>This is body_write action ♥</p>");
+ }
+
sub file_upload :POST Consumes(Multipart) Local {
my ($self, $c) = @_;
Test::More::is $c->req->body_parameters->{'♥'}, '♥♥';
}
{
+ my $res = request "/root/write_then_body";
+
+ is $res->code, 200, 'OK';
+ is decode_utf8($res->content), "<p>This is early_write action ♥</p><p>This is body_write action ♥</p>";
+ is $res->content_charset, 'UTF-8';
+}
+
+{
ok my $path = File::Spec->catfile('t', 'utf8.txt');
ok my $req = POST '/root/file_upload',
Content_Type => 'form-data',