=pod

=head1 NAME

Thunderhorse::Manual - Thunderhorse reference

=head1 SYNOPSIS

First ...

	# app.pl
	use v5.40;

	package MyApp;
	use Mooish::Base;
	extends 'Thunderhorse::App';

	sub build ($self)
	{
		$self->routes->add('/hello/:name', { to => 'greet' });
	}

	sub greet ($self, $ctx, $name)
	{
		return "Hello, $name!";
	}

	MyApp->new->run;

Then ...

	> pagi-server app.pl

=head1 DESCRIPTION

Thunderhorse is a web framework which supports L<PAGI> protocol natively. It
builds around tools delivered by PAGI to achieve a simple, capable, and
async-ready framework. The same ideas were used to build L<Kelp>, which was
based on L<PSGI> and L<Plack>. Thunderhorse is the spiritual successor of Kelp
and carries its legacy into the world of real-time web.

Thunderhorse was designed to be light, extensible and reusable. It can
seamlessly integrate with PAGI apps or middlewares, which makes it very easy to
build a web server from available components. It has a very powerful and
cache-friendly router. It is built on top of L<Gears>, which means its parts
are very hackable and easy to reuse in other projects.

Unlike other frameworks, Thunderhorse neither reinvents all of its wheels, nor
depends on numerous CPAN dependencies to work. It leverages a moderate,
hand-picked set of distributions to deliver concise, performant and
well-organized system. It is also based on perl 5.40 and uses modern syntax
features in its core, which allows it to further reduce the set of required
dependencies and keep the core small.

=head2 The application class

Each Thunderhorse application is required to define an application class, which
is a package subclass of L<Thunderhorse::App>. The mechanism of subclassing can
be chosen at will, but docs and examples will suggest L<Mooish::Base>, which is
also what the core of Thunderhorse uses. Mooish::Base imports Moo, but
will also ensure all known performance-enhancing modules are loaded.

Most minimal Thunderhorse application (which does nothing) looks like this:

	# lib/MyApp.pm
	package MyApp;

	use v5.40;
	use Mooish::Base;

	extends 'Thunderhorse::App';

To run it, a perl file is required, which will load, instantiate and run it (as
the last expression):

	use lib 'lib';
	use MyApp;

	MyApp->new->run;

That file can then be run using C<pagi-server>, which will take the last
expression of the file and run it when requests arrive. In the above example,
all these requests will return HTTP code C<404>, since we haven't declared any
routing yet.

=head2 Routing

To make our application do anything useful, we need to obtain the router
(L<Thunderhorse::Router>) and call C<add> method on it. This is commonly done
in the C<build> method of the application, which is called automatically when
the application object is created:

	# in lib/MyApp.pm
	sub build ($self)
	{
		my $router = $self->router;

		$router->add(
			'/request/path' => {
				to => sub ($self, $ctx) {
					return 'Hello?';
				},
			}
		);
	}

The code above lets us now visit C<localhost:5000/request/path> and see text
C<Hello?> in the browser. This is called I<a routing location>, and typically
points at a subroutine or its name using C<to>, also called destination. The
code above could be rewritten to the following form, which would yield the
exact same result:

	sub build ($self)
	{
		my $router = $self->router;

		$router->add(
			'/request/path' => {
				to => 'hello', # name of a method in this controller
			}
		);
	}

	sub hello ($self, $ctx)
	{
		return 'Hello?';
	}

Destination can be a C<sub> or C<async sub> (using L<Future::AsyncAwait>). It
accepts two base arguments: C<$self> which is the instance of the controller
(L<Thunderhorse::Controller>), and C<$ctx> which is current request's context
(L<Thunderhorse::Context>). In Thunderhorse, controllers are persistent and
shared across all requests, which is why a context object is defined.
Controllers can never hold request-specific state, because there are multiple
concurrent requests being handled at the same time due to asynchronous nature
of PAGI. If destination is C<async>, then it must C<await> all asynchronous
calls as defined by PAGI specification.

Return value of the destination sub is by default sent to the requestor as
C<text/html> with status code C<200>. This is a common and handy shortcut, but
it is equally easy to do something else. Take the following destination example:

	async sub send_custom ($self, $ctx)
	{
		await $ctx->res->text('Plaintext response');
		return 'this will not get rendered';
	}

This takes response (L<Thunderhorse::Response>) from context, and sends
plaintext manually. This action I<consumes> the context, marking it as
finished. In this case, return value of the destination is ignored. Note that
the await call on C<< ->text >> method is mandatory.

Another example:

	sub send_custom2 ($self, $ctx)
	{
		$ctx->res->status(400)->content_type('text/plain');
		return 'this is rendered as plaintext and status 400';
	}

This time, the return value of the destination is not ignored, since only
setting response metadata does not cause the context to be consumed. Status and
I<Content-Type> header will not be overridden, so the response will be sent as
plaintext. In this case, there is no need to await anything.

=head3 Placeholders

Routes can contain placeholders which match parts of the URL path and make
those parts available to the destination handler. Placeholders are specified
using sigils in the pattern:

	$router->add(
		'/user/:id' => {
			to => sub ($self, $ctx, $id) {
				return "User ID: $id";
			}
		}
	);

This location matches C</user/123> and passes C<123> as the C<$id> parameter.
Each matched placeholder is passed as additional arguments to the destination,
after C<$self> and C<$ctx>.

Thunderhorse supports four types of placeholders:

=over

=item * C<:name> - required placeholder

Matches any characters except slash. The placeholder must be present in the URL
for the route to match.

	# matches /user/123 but not /user/ or /user
	'/user/:id'

=item * C<?name> - optional placeholder

Matches any characters except slash. If the placeholder is not present, it will
be passed as C<undef> to the destination. If it follows a slash with no curly
braces, that slash becomes optional as well.

	# matches both /post/my-slug and /post
	# in second case, $slug will be undef
	'/post/?slug'

=item * C<*name> - wildcard placeholder

Matches any characters including slashes. Always required.

	# matches /files/path/to/file.txt
	# $path will be 'path/to/file.txt'
	'/files/*path'

=item * C<E<gt>name> - slurpy placeholder

Optional wildcard that matches everything including slashes. If it follows a
slash with no curly braces, the slash is made optional as well.

	# matches both /api and /api/v1/users
	'/api/>rest'

=back

Placeholders can be enclosed in curly braces to separate them from surrounding
text:

	'/user-{:id}-profile'

Placeholders can be validated using C<checks> parameter, which maps placeholder
names to regular expressions:

	$router->add(
		'/user/:id' => {
			to => 'show_user',
			checks => { id => qr/\d+/ },
		}
	);

Optional placeholders can be given default values using C<defaults> parameter:

	$router->add(
		'/post/?page' => {
			to => 'list_posts',
			defaults => { page => 1 },
		}
	);

When a default is specified, the destination will receive that value instead of
C<undef> when the placeholder is not present in the URL.

=head3 Bridges

Bridges are routes that have children. They are useful for implementing
authentication, authorization, or any other pre-processing logic that should
apply to multiple routes. They may also be used to group routes together. A
bridge is created when you call C<add> on the result of another C<add>:

	my $admin_bridge = $router->add(
		'/admin' => {
			to => 'check_admin',
		}
	);

	$admin_bridge->add(
		'/users' => {
			to => 'list_users',
		}
	);

When C</admin/users> is requested, both C<check_admin> and C<list_users> will
be called in sequence. The bridge destination receives the same arguments as
regular destinations. If the bridge consumes the context (by sending a
response), further matching stops. Otherwise, the next matching location is
called. For this reason, bridge destinations should return C<undef> explicitly
to avoid consuming the context by accident:

	sub check_admin ($self, $ctx)
	{
		await $self->render_error($ctx, 403)
			unless $ctx->req->session->{is_admin};

		# if context is not consumed, continue to next match
		return undef;
	}

=head3 Actions

Actions allow routes to be restricted to specific request types. By default,
routes match all HTTP methods and scopes. Actions are specified using
the C<action> parameter:

	$router->add(
		'/api/data' => {
			to => 'get_data',
			action => 'http.get',
		}
	);

Action format is C<scope.method> where scope is one of C<http>, C<sse>, or
C<websocket>, and method is an HTTP method for C<http> or C<sse> scope, or
omitted for C<websocket>. Either part can be C<*> to match anything.

Common action patterns:

	# Match only HTTP POST requests
	action => 'http.post'

	# Match any HTTP method
	action => 'http.*'

	# Match only WebSocket connections
	action => 'websocket'

	# Match only Server-Sent GET Events
	action => 'sse.get'

	# Match any request type (default)
	action => '*.*'

Multiple routes with the same pattern but different actions can coexist,
allowing different handlers for different request types:

	$router->add('/api/data' => { to => 'get_data', action => 'http.get' });
	$router->add('/api/data' => { to => 'post_data', action => 'http.post' });
	$router->add('/api/data' => { to => 'stream_data', action => 'websocket' });

=head2 Controllers

By default, all routes defined in the application's C<build> method belong to
the application controller (L<Thunderhorse::AppController>). However, as
applications grow, it becomes useful to organize routes and their handlers into
separate controller classes. Each controller is a self-contained unit with its
own routes and methods.

Controllers are subclasses of L<Thunderhorse::Controller> and typically live in
a namespace under your application. Each controller has its own C<build> method
where routes are defined:

	# lib/MyApp/Controller/User.pm
	package MyApp::Controller::User;

	use v5.40;
	use Mooish::Base;

	extends 'Thunderhorse::Controller';

	sub build ($self)
	{
		my $r = $self->router;

		$r->add('/users' => { to => 'list' });
		$r->add('/user/:id' => { to => 'show' });
	}

	sub list ($self, $ctx)
	{
		return "List of users";
	}

	sub show ($self, $ctx, $id)
	{
		return "User $id";
	}

Controllers have access to C<< $self->app >> to reach the application object,
and C<< $self->router >> which automatically sets the controller context for
route definitions. Do not use C<< $self->app->router >>, as this will yield
router configured for use in base app.

=head3 Loading controllers

Controllers are loaded in the application's C<build> method using
C<load_controller>:

	# in MyApp
	sub build ($self)
	{
		$self->load_controller('User');
	}

The C<load_controller> method takes a short name and automatically prepends
your application's namespace. In the above example, it loads
C<MyApp::Controller::User>. The controller's C<build> method is called
automatically, registering all its routes.

To load a controller from a different namespace, prefix the name with C<^>:

	$self->load_controller('^Some::Other::Controller::Class');

Controllers can also be loaded from configuration files, which is covered in
the L</Configuration> section.

=head2 Modules

Modules are reusable, configurable parts of Thunderhorse that have great power
over the system. They can add new methods and wrap application in middlewares.
Creation of modules is an advanced topic, discussed in L<Thunderhorse::Module>.
Here, we will focus on modules available in base Thunderhorse.

To load a module, the following call must be made in the application:

	$self->load_module('Name' => { config_key => config_value });

This loads C<Thunderhorse::Module::Name> and initializes it with the given hash
configuration. If C<Name> is a full name of the module, it should instead be
passed as C<^Name> to avoid adding the namespace prefix.

=head3 Logger

The Logger module (L<Thunderhorse::Module::Logger>) adds logging capabilities
to the application. It wraps the entire application to catch and log errors,
and adds a C<log> method to controllers.

Loading the module:

	$self->load_module('Logger' => {
		outputs => [
			screen => {
				'utf-8' => true,
			},
		],
	});

Configuration is passed to L<Gears::Logger::Handler>, which handles the actual
logging using L<Log::Handler>. Common configuration keys:

=over

=item * C<outputs> - hash of Log::Handler output destinations (file, screen, etc.)

=item * C<date_format> - strftime date format in logs, mimicing apache format by default

=item * C<log_format> - sprintf log format, mimicing apache format by default

=back

The default C<log_format> is C<[%s] [%s] %s>, where placeholders are: date,
level and message. Log format can be specified on Log::Handler level in
C<outputs> (per output), but it would cause duplication of formatting. In that
case C<log_format> must be set to C<undef> to avoid an exception on startup.

Once loaded, logging can be done from any controller method:

	sub some_action ($self, $ctx)
	{
		$self->log(info => 'Processing request');
		$self->log(error => 'Something went wrong');

		return "Done";
	}

The Logger module also automatically logs any unhandled exceptions that occur
during request processing.

=head3 Template

The Template module (L<Thunderhorse::Module::Template>) adds template
rendering capabilities using L<Template::Toolkit>. It adds a C<render> method to
controllers.

Loading the module:

	$self->load_module('Template' => {
		paths => ['views'],
		conf => {
			EVAL_PERL => true,
		},
	});

Configuration is passed to L<Gears::Template::TT>, which wraps Template
Toolkit.

=over

=item * C<conf> - hash of Template::Toolkit configuration values

=item * C<paths> - array ref of paths to search for templates

=item * C<encoding> - encoding of template files, UTF-8 by default

=back

C<paths> and C<encoding> will be automatically set as proper keys in
Template::Toolkit config, unless it was specified there separately, in which
case they will be ignored.

Once loaded, templates can be rendered from controller methods:

	sub show_page ($self, $ctx)
	{
		return $self->render('page', {
			title => 'My Page',
			content => 'Hello, World!',
		});
	}

The first argument is the template name (C<.tt> suffix will be added
automatically), and the second is a hash reference of variables to pass to the
template. The method returns the rendered content, which is then sent to the
client as HTML (if the context is not already consumed).

=head3 Middleware

The Middleware module (L<Thunderhorse::Module::Middleware>) allows loading any
PAGI middleware into the application. It wraps the entire PAGI application
with specified middlewares.

Loading the module:

	$self->load_module('Middleware' => {
		Static => {
			path => '/static',
			root => 'public',
		},
		Session => {
			store => 'file',
		},
	});

Each key in the configuration is a middleware class name (will be prefixed with
C<PAGI::Middleware::> unless it starts with C<^>). The value is a hash
reference of configuration passed to that middleware's constructor.

Middlewares are applied in deterministic order (sorted by key name). To control
the order explicitly, use the C<_order> key in middleware configuration:

	$self->load_module('Middleware' => {
		Static => { path => '/static', root => 'public', _order => 1 },
		Session => { store => 'file', _order => 2 },
	});

Lower C<_order> values are applied first, higher values are
applied last.

=head2 Configuration

Thunderhorse applications can be configured using configuration files or by
passing a hash to the constructor. Configuration is managed by
L<Thunderhorse::Config>, which extends L<Gears::Config>.

=head3 Loading configuration from files

By default, Thunderhorse does not look for any configuration files. A string
can be passed to C<initial_config>, specifying the directory in which to look:

	MyApp->new(initial_config => 'conf')->run;

This will load configuration from the C<conf> directory. Configuration files
are loaded in order:

=over

=item 1. C<config.$ext> - base configuration

=item 2. C<$env.$ext> - environment-specific configuration

=back

Where C<$ext> is any extension handled by available config readers (C<.pl> for
Perl scripts by default), and C<$env> is the current environment (production,
development, or test). The environment can be set via the C<PAGI_ENV>
environment variable or the C<env> constructor parameter. C<pagi-server -E
production> also sets C<PAGI_ENV>.

Configuration files are merged together, with environment-specific settings
overriding base settings. Example structure:

	# conf/config.pl
	{
		modules => {
			Logger => {
				outputs => [ screen => { ... } ],
			},
		},
	}

	# conf/production.pl
	{
		modules => {
			Logger => {
				'=outputs' => [ file => { ... } ],
			},
		},
	}

In production environment, the Logger module will use C<file> output instead of
the C<screen> output from base config.

=head3 Configuration merging

When multiple configuration sources are loaded, they are merged together using
a smart merge system. By default, configuration keys are merged intelligently
based on their types:

=over

=item * C<key> - Smart merge (default)

Without any prefix, configuration values are merged based on their type. Hash
references are merged recursively, applying new keys and updating existing ones
from the new configuration. Array references are extended with new values.
Scalar values and mismatched reference types replace the old value.

	# base config
	{ controllers => ['User', 'Admin'] }

	# override config
	{ controllers => ['Admin', 'API'] }

	# result: all controllers are loaded
	{ controllers => ['User', 'Admin', 'API'] }

=item * C<=key> - Replace

The equals sign prefix forces complete replacement of the value, regardless of
type. This is useful when you want to completely override a complex structure
instead of merging it.

	# base config
	{ controllers => ['User', 'Admin'] }

	# override config
	{ '=controllers' => ['Admin', 'API'] }

	# result: duplicates are applied
	{ controllers => ['Admin', 'API'] }

=item * C<+key> - Add

The plus sign prefix explicitly adds to the existing value. For arrays, new
elements are appended. For hashes, new keys are added and existing keys are
merged recursively.

	# base config
	{ controllers => ['User', 'Admin'] }

	# override config
	{ '+controllers' => ['Admin', 'API'] }

	# result: duplicates are applied
	{ controllers => ['User', 'Admin', 'Admin', 'API'] }

=item * C<-key> - Remove

The minus sign prefix removes values from arrays. It compares the array in the
new configuration with the existing array and removes matching elements. This
only works for arrays.

	# base config
	{ controllers => ['User', 'Admin'] }

	# override config
	{ '-controllers' => ['Admin', 'API'] }

	# result: the set is reduced by matched keys
	{ controllers => ['User'] }

=back

Type mismatches (such as trying to merge a hash into an array) raise an error.
The C<=> prefix can be used to force replacement when changing types.

Prefixes apply to the immediate key only and do not affect nested structures.
To control merging of nested keys, apply prefixes to those keys explicitly:

	{
		modules => {
			Logger => {
				'=outputs' => ['file'],
				'+extra' => { new_key => 'value' },
			},
		},
	}

=head3 Loading configuration from hash

Configuration can be provided directly as a hash reference:

	MyApp->new(initial_config => {
		modules => {
			Logger => { outputs => [ screen => {} ] },
		},
	})->run;

This approach is useful for testing or when configuration comes from other
sources.

=head3 Loading controllers and modules from configuration

Controllers and modules can be specified in configuration files instead of
calling C<load_controller> and C<load_module> in code:

	# in config file
	{
		controllers => ['User', 'Admin', 'API'],
		modules => {
			Logger => {
				outputs => [ screen => {} ],
			},
			Template => {
				paths => ['views'],
			},
		},
	}

The C<controllers> key is an array of controller names to load. The C<modules>
key is a hash where keys are module names and values are configuration hashes
for each module. Both controllers and modules are loaded during application
initialization, before the C<build> method is called.

=head2 Hooks

Thunderhorse is easily extensible using hooks. Hooks are methods, usually in
App or Controller, which can be overridden to do something differently.

=head3 Lifespan hooks

Lifespan hooks are called during PAGI application lifecycle, when worker
processes are started and shut down. They are defined in the application class:

	async sub on_startup ($self, $state)
	{
		# Called once when worker starts
		# Initialize resources, connect to databases, etc.
		say "Application starting up";
	}

	async sub on_shutdown ($self, $state)
	{
		# Called once when worker is shutting down
		# Clean up resources, close connections, etc.
		say "Application shutting down";
	}

Both hooks are C<async> and receive a C<$state> hash reference that is shared
across the system. This can be used to store handles or other resources that
need to be cleaned up, on PAGI level. This is usually not needed, as
Thunderhorse has access to other means of managing persistent data.

These hooks are run when a worker is spawned and killed.

=head3 Controller hooks

Controller hooks allow customization of error handling on a per-controller
basis. They can be defined in any controller or in the application class (which
all controllers call by default).

=head4 on_error

The C<on_error> hook is called when an exception occurs during request
processing:

	async sub on_error ($self, $ctx, $error)
	{
		if ($error isa 'Gears::X::HTTP') {
			# HTTP error with status code
			await $self->render_error($ctx, $error->code);
		}
		else {
			# Some other error
			die $error;
		}
	}

This hook receives the controller instance, the context, and the error object.
It should consume the context by sending a response. The default implementation
handles L<Gears::X::HTTP> exceptions by calling C<render_error>.

=head4 render_error

The C<render_error> hook is called to render error pages:

	async sub render_error ($self, $ctx, $code, $message = undef)
	{
		my $text = $message // HTTP::Status::status_message($code);
		await $ctx->res->status($code)->html(
			"<h1>Error $code</h1><p>$text</p>"
		);
	}

This hook receives the controller instance, the context, the HTTP status code,
and an optional error message. If no message is provided and the application is
in production mode, a generic status message is used instead. The default
implementation sends a plain text response. The default implementation checks
C<is_production> method of the application to avoid rendering the original
error message which may contain sensitive information.

Both hooks prioritize controller-specific implementations over application-level
ones. If a controller defines its own C<on_error> or C<render_error>, those
will be called instead of the application's versions. This allows fine-grained
control over error handling for different parts of the application.

