Some fixes and framework docs
This commit is contained in:
parent
b2d676b1ed
commit
18e642290d
7 changed files with 313 additions and 17 deletions
307
FU.pm
307
FU.pm
|
|
@ -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
2
FU.xs
|
|
@ -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:
|
||||
|
|
|
|||
3
FU/Pg.pm
3
FU/Pg.pm
|
|
@ -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
|
||||
|
||||
|
|
|
|||
1
c/pgst.c
1
c/pgst.c
|
|
@ -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;
|
||||
|
|
|
|||
13
c/pgtypes.c
13
c/pgtypes.c
|
|
@ -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 )\
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue