From dff3e686a4ce0573e5d79477c2ceb266cac87678 Mon Sep 17 00:00:00 2001 From: bbrtj Date: Fri, 5 Jul 2024 17:29:54 +0200 Subject: [PATCH] Improve context support in Kelp --- Changes | 7 ++ lib/Kelp.pm | 47 ++++++++++---- lib/Kelp/Context.pm | 93 +++++++++++++++++++++++++-- t/lib/CustomContext/Context.pm | 18 ++++++ t/lib/CustomContext/Controller.pm | 40 ++++++++++++ t/lib/CustomContext/Controller/Foo.pm | 14 ++++ t/lib/MyApp3.pm | 25 +++++++ t/routes_custom_controller.t | 19 ++++++ 8 files changed, 244 insertions(+), 19 deletions(-) create mode 100644 t/lib/CustomContext/Context.pm create mode 100644 t/lib/CustomContext/Controller.pm create mode 100644 t/lib/CustomContext/Controller/Foo.pm create mode 100644 t/lib/MyApp3.pm create mode 100644 t/routes_custom_controller.t diff --git a/Changes b/Changes index 2bc9f4e..3093311 100644 --- a/Changes +++ b/Changes @@ -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] diff --git a/lib/Kelp.pm b/lib/Kelp.pm index 68b425a..b6698ab 100644 --- a/lib/Kelp.pm +++ b/lib/Kelp.pm @@ -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; @@ -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 { {} }; @@ -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 { @@ -546,14 +556,19 @@ L 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. + =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. =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. =head2 config_module @@ -711,8 +726,8 @@ initializations. C -Used to load a module. All modules must be under the C -namespace. +Used to load a module. All modules should be under the C +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 @@ -722,6 +737,11 @@ C hash in the config. Precedence is given to the inline options. See L 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 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 @@ -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 though this can be +overwritten via the respone_obj attribute. Much like L, 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 @@ -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 though this can be -overwritten via the respone_obj attribute. Much like L, the -response can also be overridden to use a custom response object if you need -something completely custom. =head2 run diff --git a/lib/Kelp/Context.pm b/lib/Kelp/Context.pm index 9cca1fa..de74cba 100644 --- a/lib/Kelp/Context.pm +++ b/lib/Kelp/Context.pm @@ -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 @@ -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 @@ -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; @@ -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 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 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, but does not have any special checks for correctness and +only accepts a full class name. It also modifies the L 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. diff --git a/t/lib/CustomContext/Context.pm b/t/lib/CustomContext/Context.pm new file mode 100644 index 0000000..48ede2e --- /dev/null +++ b/t/lib/CustomContext/Context.pm @@ -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; + diff --git a/t/lib/CustomContext/Controller.pm b/t/lib/CustomContext/Controller.pm new file mode 100644 index 0000000..f734242 --- /dev/null +++ b/t/lib/CustomContext/Controller.pm @@ -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; + diff --git a/t/lib/CustomContext/Controller/Foo.pm b/t/lib/CustomContext/Controller/Foo.pm new file mode 100644 index 0000000..d8547a9 --- /dev/null +++ b/t/lib/CustomContext/Controller/Foo.pm @@ -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; + diff --git a/t/lib/MyApp3.pm b/t/lib/MyApp3.pm new file mode 100644 index 0000000..c5ddb49 --- /dev/null +++ b/t/lib/MyApp3.pm @@ -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; + diff --git a/t/routes_custom_controller.t b/t/routes_custom_controller.t new file mode 100644 index 0000000..bf83520 --- /dev/null +++ b/t/routes_custom_controller.t @@ -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; +