Skip to content

Commit

Permalink
Improve context support in Kelp
Browse files Browse the repository at this point in the history
  • Loading branch information
bbrtj committed Jul 5, 2024
1 parent 903212e commit dff3e68
Show file tree
Hide file tree
Showing 8 changed files with 244 additions and 19 deletions.
7 changes: 7 additions & 0 deletions Changes
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
Revision history for Kelp

{{$NEXT}}
[New Interface]
- Added context_obj attribute to Kelp
- Added build_context method to Kelp
- Added build_controller method to Kelp::Context

[New Documentation]
- Kelp::Context package is now documented

2.15 - 2024-07-03
[Changes]
Expand Down
47 changes: 34 additions & 13 deletions lib/Kelp.pm
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ attr -path => $FindBin::Bin;
attr -name => sub { (ref($_[0]) =~ /(\w+)$/) ? $1 : 'Noname' };
attr request_obj => 'Kelp::Request';
attr response_obj => 'Kelp::Response';
attr context_obj => 'Kelp::Context';

# Debug
attr long_error => $ENV{KELP_LONG_ERROR} // 0;
Expand All @@ -39,8 +40,9 @@ attr __config => undef;

attr -loaded_modules => sub { {} };

# Current context data of the application
attr context => sub { Kelp::Context->new(app => $_[0]) };
# Current context of the application - tracks the state application is in,
# especially when it comes to managing controller instances.
attr context => \&build_context;

# registered application encoder modules
attr encoder_modules => sub { {} };
Expand Down Expand Up @@ -164,6 +166,14 @@ sub build
{
}

# Override to use a custom context object
sub build_context
{
return Kelp::Util::load_package($_[0]->context_obj)->new(
app => $_[0],
);
}

# Override to use a custom request object
sub build_request
{
Expand Down Expand Up @@ -546,14 +556,19 @@ L<Kelp::Module::Config> for more information.
# conf/config.pl and conf/development.pl are merged with priority
# given to the second one.
=head2 context_obj
Provide a custom package name to define the ::Context object. Defaults to
L<Kelp::Context>.
=head2 request_obj
Provide a custom package name to define the global ::Request object. Defaults to
Provide a custom package name to define the ::Request object. Defaults to
L<Kelp::Request>.
=head2 response_obj
Provide a custom package name to define the global ::Response object. Defaults to
Provide a custom package name to define the ::Response object. Defaults to
L<Kelp::Response>.
=head2 config_module
Expand Down Expand Up @@ -711,8 +726,8 @@ initializations.
C<load_module($name, %options)>
Used to load a module. All modules must be under the C<Kelp::Module::>
namespace.
Used to load a module. All modules should be under the C<Kelp::Module::>
namespace. If they are not, their class name must be prepended with C<+>.
$self->load_module("Redis", server => '127.0.0.1');
# Will look for and load Kelp::Module::Redis
Expand All @@ -722,6 +737,11 @@ C<modules_init> hash in the config. Precedence is given to the
inline options.
See L<Kelp::Module> for more information on making and using modules.
=head2 build_context
This method is used to build the context. By default, it's used lazily by
L</context_obj> attribute. It can be overridden to modify how context is built.
=head2 build_request
This method is used to create the request object for each HTTP request. It
Expand All @@ -741,6 +761,14 @@ the class of the object used.
# Now each request will be handled by MyApp::Request
=head2 build_response
This method creates the response object, e.g. what an HTTP request will return.
By default the object created is L<Kelp::Response> though this can be
overwritten via the respone_obj attribute. Much like L</build_request>, the
response can also be overridden to use a custom response object if you need
something completely custom.
=head2 before_dispatch
Override this method to modify the behavior before a route is handled. The
Expand Down Expand Up @@ -777,13 +805,6 @@ finalized.
The above is an example of how to insert a custom header into the response of
every route.
=head2 build_response
This method creates the response object, e.g. what an HTTP request will return.
By default the object created is L<Kelp::Response> though this can be
overwritten via the respone_obj attribute. Much like L</build_request>, the
response can also be overridden to use a custom response object if you need
something completely custom.
=head2 run
Expand Down
93 changes: 87 additions & 6 deletions lib/Kelp/Context.pm
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ attr -_controllers => sub { {} };
attr persistent_controllers => sub { $_[0]->app->config('persistent_controllers') };
attr current => sub { shift->app };

sub build_controller
{
my ($self, $controller_class) = @_;
return $self->app->_clone($controller_class);
}

# loads the class, reblesses and returns - can be used to get controller on
# demand with partial or unloaded class name
sub controller
Expand All @@ -28,7 +34,7 @@ sub controller
unless $controller->isa($base);

return $self->_controllers->{$controller} //=
$self->app->_clone($controller);
$self->build_controller($controller);
}

# reblesses, remembers and sets the current controller - used internally
Expand All @@ -38,7 +44,7 @@ sub set_controller

# the controller class should already be loaded by the router
my $current = $self->_controllers->{$controller} //=
$self->app->_clone($controller);
$self->build_controller($controller);

$self->current($current);
return $current;
Expand All @@ -56,8 +62,83 @@ sub clear

1;

# Advanced usage only. Should not be instantiated manually.
# This is a small helper object which keeps track of the context in which the
# app currently is. It also remembers all the constructed controllers until it
# is cleared - which usually is at the start of the request.
__END__
=pod
=head1 NAME
Kelp::Context - Tracks Kelp application's current execution context
=head1 SYNOPSIS
# get current controller
$app->context->current;
# get the application
$app->context->app;
# get the named controller
$app->context->controller('Controller');
=head1 DESCRIPTION
This is a small helper object which keeps track of the context in which the
app currently is. It also remembers all the constructed controllers until it
is cleared - which usually is at the start of the request.
Advanced usage only.
It can be subclassed to change how controllers are built and handled. This
would usually involve overriding the C<build_controller> method.
=head1 ATTRIBUTES
=head2 app
Main application object. This will always be the main app, not a controller.
=head2 current
Current controller object. This will be automatically set to a proper
controller by the router.
=head2 req
=head2 res
Current request and response objects, also accessible from C<< $app->req >> and
C<< $app->res >>.
=head2 persistent_controllers
A configuration field which defines whether L</clear> destroys constructed
controllers. By default it is taken from app's configuration field of the same
name.
=head1 METHODS
=head2 build_controller
Defines how a controller is built. Can be overridden to introduce a custom
controller object instead of reblessed application.
=head2 controller
Returns a controller of a given name. The name will be mangled according to the
base route class of the application. Contains extra checks to ensure the input
is valid and loads the controller class if it wasn't loaded yet.
If the controller name is undef, the base controller is returned.
=head2 set_controller
Like L</controller>, but does not have any special checks for correctness and
only accepts a full class name. It also modifies the L</current> to the
controller after constructing it. It's optimized for speed and used only
internally.
=head2 clear
Clears context in anticipation of the next request.
18 changes: 18 additions & 0 deletions t/lib/CustomContext/Context.pm
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package CustomContext::Context;

use Kelp::Base 'Kelp::Context';
use Kelp::Util;

attr persistent_controllers => !!1;

sub build_controller
{
my ($self, $controller_class) = @_;

$controller_class->new(
app => $self->app,
);
}

1;

40 changes: 40 additions & 0 deletions t/lib/CustomContext/Controller.pm
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package CustomContext::Controller;

use Kelp::Base;
use Carp;

attr -app => sub { croak 'app is required' };

sub before_dispatch
{
my $self = shift;
$self->app->before_dispatch(@_);
}

sub before_finalize
{
my $self = shift;
$self->app->before_finalize(@_);
}

sub build
{
my $self = shift;
my $app = $self->app;

$app->add_route(
'/a' => {
to => 'bridge',
bridge => 1,
}
);
$app->add_route('/a/b/c' => 'foo#test');
}

sub bridge
{
return ref shift() eq __PACKAGE__;
}

1;

14 changes: 14 additions & 0 deletions t/lib/CustomContext/Controller/Foo.pm
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package CustomContext::Controller::Foo;

use Kelp::Base 'CustomContext::Controller';

sub test
{
my ($self) = @_;

$self->app->res->text;
return ref $self;
}

1;

25 changes: 25 additions & 0 deletions t/lib/MyApp3.pm
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package MyApp3;
use Kelp::Base 'Kelp';

attr context_obj => 'CustomContext::Context';

sub build
{
my $self = shift;

$self->routes->base('CustomContext::Controller');
$self->routes->rebless(1);

$self->add_route(
'/a/b' => {
to => sub {
return ref shift() eq __PACKAGE__;
},
bridge => 1,
}
);
$self->context->controller()->build;
}

1;

19 changes: 19 additions & 0 deletions t/routes_custom_controller.t
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
use lib 't/lib';
use Kelp::Base -strict;
use MyApp3;
use Kelp::Test;
use HTTP::Request::Common;
use Test::More;

# Get the app
my $app = MyApp3->new();

# Test object
my $t = Kelp::Test->new(app => $app);

$t->request_ok(GET '/a/b/c')
->content_type_is('text/plain')
->content_is('CustomContext::Controller::Foo');

done_testing;

0 comments on commit dff3e68

Please sign in to comment.