Some fixes and framework docs

This commit is contained in:
Yorhel 2025-02-23 14:05:43 +01:00
parent b2d676b1ed
commit 18e642290d
7 changed files with 313 additions and 17 deletions

307
FU.pm
View file

@ -30,9 +30,8 @@ FU::Log::set_fmt(sub($msg) {
);
});
sub debug { state $v = 0; $v = $_[0] if @_; $v }
sub log_slow_pages { state $v = 0; $v = $_[0] if @_; $v }
sub log_queries { state $v = 0; $v = $_[0] if @_; $v }
sub debug { state $v = 0; $v = $_[0] if @_; $v }
sub log_slow_reqs { state $v = 0; $v = $_[0] if @_; $v }
sub mime_types() { state $v = {qw{
7z application/x-7z-compressed
@ -108,7 +107,22 @@ sub compress_mimes { state $v = {map +($_,1), qw{
our $INIT_DB;
our $DB;
sub _connect_db { $DB = ref $INIT_DB eq 'CODE' ? $INIT_DB->() : FU::Pg->connect($INIT_DB) }
sub query_trace($st,@) {
$REQ->{trace_nsql}++;
$REQ->{trace_nsqlprep}++ if $st->prepare_time;
$REQ->{trace_nsqldirect}++ if !defined $st->prepare_time;
$REQ->{trace_sqlexec} += $st->exec_time;
$REQ->{trace_sqlprep} += $st->prepare_time if $st->prepare_time;
push $REQ->{trace_sql}->@*, {
query => $st->query, nrows => $st->nrows,
param_types => $st->param_types, param_values => $st->param_values,
exec_time => $st->exec_time, prepare_time => $st->prepare_time,
} if FU::debug;
}
sub _connect_db {
$DB = ref $INIT_DB eq 'CODE' ? $INIT_DB->() : FU::Pg->connect($INIT_DB);
$DB->query_trace(\&query_trace);
}
sub init_db($info) {
require FU::Pg;
$INIT_DB = $info;
@ -119,7 +133,7 @@ sub init_db($info) {
my @before_request;
my @after_request;
sub before_request :prototype(&) ($f) { push @before_request, $f }
sub after_request :prototype(&) ($f) { push @after_request, $f }
sub after_request :prototype(&) ($f) { unshift @after_request, $f }
my %path_routes;
@ -281,7 +295,7 @@ sub _log_err($e) {
log_write join "\n",
'IP: '.($REQ->{ip}||'-'),
'Headers:', (map " $_: $REQ->{hdr}{$_}", sort keys $REQ->{hdr}->%*),
'ERROR:', $e =~ s/(^|\n)/ /rg;
'ERROR:', $e =~ s/(^|\n)/\n /rg;
# TODO: decoded body, if we have that.
} else {
log_write $e;
@ -332,8 +346,9 @@ sub _do_req($c) {
}
if ($err) {
fu->reset;
my($code, $msg) = ref $err eq 'FU::err' ? $err->@* : (500, $err);
fu->reset;
fu->status($code);
eval {
($onerr{$code} || $onerr{500})->($code, $msg);
1;
@ -344,11 +359,14 @@ sub _do_req($c) {
fu->_flush($c->{fcgi_obj} || $c->{client_sock});
my $proc_ms = (time - $REQ->{trace_start}) * 1000;
log_write(sprintf "%.0fms %s-%s %s-%d\n", $proc_ms,
log_write(sprintf "%.0fms%s %s-%s %s-%d\n", $proc_ms,
$REQ->{trace_nsql} ?
sprintf ' (sql %.0f+%.0fms, %d/%d/%d)',
($REQ->{trace_sqlexec}||0)*1000, ($REQ->{trace_sqlprep}||0)*1000,
$REQ->{trace_nsqldirect}||0, $REQ->{trace_nsqlprep}||0, $REQ->{trace_nsql} : '',
$REQ->{status}, ($REQ->{reshdr}{'content-type'}//'-') =~ s/;.+$//r,
$REQ->{reshdr}{'content-encoding'}//'bytes', length($REQ->{resbody}),
# TODO: SQL timings
) if FU::debug || $proc_ms > (FU::log_slow_pages||1e10);
) if FU::debug || $proc_ms > (FU::log_slow_reqs||1e10);
}
@ -568,8 +586,8 @@ sub db {
};
}
sub sql { fu->db->q(@_) }
sub SQL { fu->db->Q(@_) }
sub sql { shift->db->q(@_) }
sub SQL { shift->db->Q(@_) }
@ -803,6 +821,7 @@ Zstandard.
=back
=head2 Framework Overview
C<FU> is a mostly straightforward and conventional backend web framework. It
@ -840,10 +859,45 @@ certainly not great if you plan to transfer large files.
The rest of this document is reference documentation; there's no easy
introductionary cookbook-style docs yet, sorry about that.
Unless specifically mentioned otherwise, all methods and functions taking or
returning strings deal with perl Unicode strings, not raw bytes.
=head2 Framework Configuration
=over
=item FU::init_db($info)
Set database configuration. C<$info> can either be a connection string for C<<
FU::Pg->connect() >> or a subroutine that returns a L<FU::Pg> connection. The
latter can be useful to set default parameters such as C<cache()>,
C<text_params()>, C<client_encoding>, etc.
A C<query_trace()> callback is registered after connection to collect
per-request performance metrics. If you want to register your own trace
callback, you'll want to have it call C<FU::query_trace($st)> to keep the
functionality.
The configured database is used for C<< fu->db >> and related methods; you can
of course still manage alternative database connections in your own code if you
need that, but then that won't benefit from FU's integrated transaction
handling and performance tracing.
=item FU::debug($enable)
Enable or disable debug mode. Returns the current mode when no argument is
given.
Debug mode currently only enables more verbose logging, but it may influence
other features in the future as well. You're of course free to use the debug
setting to enable or disable debugging features in your own code.
=item FU::log_slow_reqs($ms)
Enable logging of requests that took longer than C<$ms> milliseconds to
process. Can be set to 0 to disable such logging.
=item FU::monitor_path(@paths)
Add filesystem paths to be monitored for changes when running in monitor mode
@ -870,14 +924,243 @@ restart loop.
=back
=head2 Handlers & Routing
=over
=item FU::get($path, $sub)
=item FU::post($path, $sub)
=item FU::delete($path, $sub)
=item FU::options($path, $sub)
=item FU::put($path, $sub)
=item FU::patch($path, $sub)
=item FU::query($path, $sub)
Register a route handler for the given HTTP method and C<$path>. C<$path> can
either be a string, which is matched for equality with C<< fu->path >>, or a
regex that must fully match the request path. If the regex contains capture
groups, its contents are passed to C<$sub> as arguments.
FU::get '/', sub {
# Here goes the code for the root path.
};
FU::post '/sub/path', sub {
# POST requests to '/sub/path' go here.
};
FU::get qr{/hello/(.+)}, sub($name) {
# "GET /hello/world" goes to this code, with $name='world'.
};
It is an error to register multiple handlers for the same method and path. This
is verified for exact paths, but if you register handlers with overlapping
regexes, it's not defined which one is actually called.
=item FU::before_request($sub)
Register a callback to be run when a request has been received but before it's
being routed to the main handler function. Callbacks are run in the order that
they are registered. If C<$sub> throws an error or calls C<< fu->done >>, any
later C<before_request> callbacks are not run and no routing handler is called.
=item FU::after_request($sub)
Register a callback to be run after the routing handler has finished but before
the response is sent back to the client. Callbacks are run in reverse order
that they are registered. These callbacks are always run, even when a previous
C<before_request> or the routing handler threw an error.
=item FU::on_error($code, $sub)
Register a callback to be run when the given error code (HTTP status code) is
generated. C<$sub> is called with the error code as arguments and should
generate a suitable error page to send to the client. Only one callback can be
registered for each code, calling this function another time with the same
C<$code> overwrites a previous callback.
Internally, C<FU> can generate errors with code C<400>, C<404> and C<500>, but
C<< fu->error() >> can be used to generate other errors. If no callback exists
for a certain error code, C<500> is used as fallback.
=back
=head2 The 'fu' Object
While the C<FU::> namespace is used for global configuration and utility
functions, the C<fu> object is intended for methods that deal with request
processing (although some are useful used outside of request handlers as well).
The C<fu> object itself can be used to store request-local data. For example,
the following is a valid approach to handle user authentication:
FU::before {
fu->{user} = authenticate_user_from_cookie_or_something();
};
FU::get '/registered-users-only', sub {
fu->error(403) if !fu->{user};
};
In addition to the request information and response generation methods
described in the sections below, it has a few utility methods:
=over
=item fu->debug
Read-only alias of C<FU::debug>.
=item fu->db_conn
Returns the current database handle, as set with C<FU::init_db()>. This is
mainly useful for configuration, you generally shouldn't use this for running
queries inside a request handler, see C<< fu->db >> for that instead.
=item fu->db
Returns the database transaction for the current request. Starts a new
transaction if none is active.
Transactions initiated this way are automatically committed when the request
has successfully been processed, or rolled back if there was an error.
=item fu->sql($query, @params)
Convenient short-hand for C<< fu->db->q($query, @params) >>.
=item fu->SQL(@args)
Convenient short-hand for C<< fu->db->Q(@args) >>.
=back
=head2 Request Information
=over
=item fu->path
The path component of the request. E.g. if the request is for
C<https://example.com/some/path?query>, this returns C</some/path>.
=item fu->method
Upper-case request method, e.g. 'POST' or 'GET'.
=item fu->header($name)
Return the request header by the given C<$name>, or undef if the requests did
not have that header. Header name matching is case-insensitive. If the request
includes multiple headers with the same name, these are merged into a single
comma-separated value.
=item fu->headers
Return a hashref with all request headers. Keys are lower-cased header names.
=item fu->ip
Return the client IP address.
=item fu->query()
Return the raw query part of the request URI, e.g.
C<https://example.com/some/path?query> this returns C<query>.
=item fu->query($name)
Parses the raw query string with C<query_decode> in L<FU::Util> and returns the
value with the given $name.
=item fu->query($schema)
I<TODO>
=item fu->formdata($name)
=item fu->formdata($schema)
Like C<< fu->query() >> but returns data from the POST request body.
=back
I<TODO:> Support C<multipart/form-data> and file uploads.
I<TODO:> Support JSON bodies.
I<TODO:> Cookie parsing.
=head2 Generating Responses
=over
=item fu->done
Throw an exception to indicate that the response is "done", i.e. the current
function will return and no further handlers (if any) are run. Only works if
you're not catching the exception elsewhere, of course.
=item fu->error($code, $message)
Throw an exception with a status code. If the exception is not caught
elsewhere, this ends up in running the appropriate C<FU::on_error> handler.
C<$message> is optional and currently only used for logging.
=item fu->reset
Reset the response to an empty state, basically undoing all effects of the
methods below.
=item fu->status($code)
Set the HTTP status code for the response. Defaults to C<200> if not set and no
error is thrown.
=item fu->add_header($name, $value)
Add a response header, can be used to add multiple headers with the same name.
=item fu->set_header($name, $value)
Add a response header or overwrite the header with a new value if it already
exists. Set C<$value> to undef to remove a previously set header.
=item fu->set_body($data)
Set the (raw, binary) body of the response to C<$data>. This method is not very
convenient for writing dynamic responses, so usually you'll want to use a
templating system or L<FU::XMLWriter>:
use FU::XMLWriter ':html5_';
fu->set_body(html_ sub {
body_ sub {
h1_ "Hello, world!";
};
});
=back
I<TODO:> Setting cookies.
I<TODO:> JSON output.
I<TODO:> Redirection responses.
I<TODO:> Sending files.
=head2 Running the Site
When your script is done setting L</"Framework Configuration"> and registering

2
FU.xs
View file

@ -351,7 +351,7 @@ void columns(fupg_st *st)
void nrows(fupg_st *st)
CODE:
ST(0) = st->result ? fupg_exec_result(aTHX_ st->result) : &PL_sv_undef;
ST(0) = st->result ? sv_2mortal(newSViv(PQntuples(st->result))) : &PL_sv_undef;
void query(fupg_st *st)
CODE:

View file

@ -372,8 +372,7 @@ returns.
=item $st->nrows
Returns the number of rows the query returned, may return C<undef> for
C<exec()>-style queries.
Number of rows returned by the query.
=item $st->exec_time

View file

@ -63,6 +63,7 @@ static SV *fupg_exec(pTHX_ fupg_conn *c, const char *sql) {
fupg_st *st = safecalloc(1, sizeof(*st));
st->conn = c;
SvREFCNT_inc(c->self);
st->cookie = c->cookie;
st->query = savepv(sql);
st->stflags = c->stflags;
st->result = r;

View file

@ -80,6 +80,16 @@ SENDFN(bool) {
fustr_write_ch(out, SvTRUE(val) ? 1 : 0);
}
RECVFN(void) {
RLEN(0);
(void)buf;
return &PL_sv_undef;
}
SENDFN(void) {
(void)val; (void)out;
}
RECVFN(int2) {
RLEN(2);
return newSViv(fu_frombeI(16, buf));
@ -562,8 +572,8 @@ SENDFN(timestamp) {
/* 603 box */\
/* 604 polygon */\
/* 628 line */\
B( 650, "cidr", inet )\
A( 629, "_line", 628 )\
B( 650, "cidr", inet )\
A( 651, "_cidr", 650 )\
B( 700, "float4", float4)\
B( 701, "float8", float8)\
@ -635,6 +645,7 @@ SENDFN(timestamp) {
A( 2209, "_regoperator", 2204 )\
A( 2210, "_regclass", 2205 )\
A( 2211, "_regtype", 2206 )\
B( 2278, "void", void )\
A( 2949, "_txid_snapshot", 2970 )\
B( 2950, "uuid", uuid )\
A( 2951, "_uuid", 2950 )\

View file

@ -428,7 +428,7 @@ subtest 'Tracing', sub {
is_deeply $st->param_types, [];
is_deeply $st->param_values, [];
is_deeply $st->columns, [];
ok !defined $st->nrows;
is $st->nrows, 0;
is $st->query, 'SET client_encoding=UTF8';
ok $st->exec_time > 0 && $st->exec_time < 1;
ok !defined $st->prepare_time;

View file

@ -40,6 +40,8 @@ sub f($type, $p_in) {
ok !eval { $conn->q("SELECT \$1::$type", $p_in)->val; 1 }, "$test fail";
}
ok !defined $conn->q('SELECT pg_sleep(0)')->val; # void
v bool => true, undef, 1, 't';
v bool => false, undef, 0, 'f';