FU: Output compression, XMLWriter integration, import options, some docs

This commit is contained in:
Yorhel 2025-02-19 16:39:44 +01:00
parent 63f524622b
commit d0665ef7c2

173
FU.pm
View file

@ -1,19 +1,27 @@
package FU 0.1; package FU 0.1;
use v5.36; use v5.36;
use Carp 'confess'; use Carp 'confess', 'croak';
use IO::Socket; use IO::Socket;
use POSIX; use POSIX;
use FU::Util; use FU::Util;
use FU::XMLWriter;
my %export_funcs = do { no strict 'refs'; +(
(map +($_, \*{'FU::Util::'.$_}), @FU::Util::EXPORT_OK),
(map +($_, \*{'FU::XMLWriter::'.$_}), @FU::XMLWriter::EXPORT_OK),
) };
my %export_tags = %FU::XMLWriter::EXPORT_TAGS;
sub import($pkg, @opt) { sub import($pkg, @opt) {
my %opt = map +($_,1), @opt;
_spawn() if $opt{-spawn};
no strict 'refs';
my $c = caller; my $c = caller;
no strict 'refs';
*{$c.'::fu'} = \&fu; *{$c.'::fu'} = \&fu;
for (map /^:(.+)$/ && $export_tags{$1} ? $export_tags{$1}->@* : ($_), @opt) {
if ($_ eq '-spawn') { _spawn() }
elsif ($export_funcs{$_}) { *{$c.'::'.$_} = $export_funcs{$_} }
else { croak "Unknown import option: '$_'" }
}
} }
@ -34,7 +42,7 @@ sub mime_types() { state $v = {qw{
avif image/avif avif image/avif
bin application/octet-stream bin application/octet-stream
bmp image/bmp bmp image/bmp
bz application/x-bzip2 bz2 application/x-bzip2
css text/css css text/css
csv text/csv csv text/csv
gif image/gif gif image/gif
@ -73,6 +81,28 @@ sub mime_types() { state $v = {qw{
xsd application/xml xsd application/xml
xsl application/xml xsl application/xml
zip application/zip zip application/zip
zst application/zstd
}} }
# XML & JSON generally don't need a charset parameter
sub utf8_mimes { state $v = {map +($_,1), qw{
application/javascript
text/css
text/html
text/plain
}} }
sub compress_mimes { state $v = {map +($_,1), qw{
application/atom+xml
application/javascript
application/json
application/rss+xml
application/xml
image/svg+xml
text/css
text/csv
text/html
text/plain
}} } }} }
@ -246,7 +276,10 @@ sub _log_err($e) {
} }
sub _do_req($c) { sub _do_req($c) {
local $REQ = { hdr => {} }; local $REQ = {
hdr => {},
xml => FU::XMLWriter::_new(),
};
local $fu = bless {}, 'FU::obj'; local $fu = bless {}, 'FU::obj';
$REQ->{ip} = $c->{client_sock} isa 'IO::Socket::INET' ? $c->{client_sock}->peerhost : '127.0.0.1'; $REQ->{ip} = $c->{client_sock} isa 'IO::Socket::INET' ? $c->{client_sock}->peerhost : '127.0.0.1';
@ -554,17 +587,19 @@ sub formdata {
sub done { die bless [200,'Done'], 'FU::err' } sub done { die bless [200,'Done'], 'FU::err' }
sub error($,$code,$msg=$code) { die bless [$code,$msg], 'FU::err' } sub error($,$code,$msg=$code) { die bless [$code,$msg], 'FU::err' }
sub reset { sub status($, $code) { $FU::REQ->{status} = $code }
my $r = $FU::REQ; sub set_body($, $data) {
$r->{status} = 200; $FU::REQ->{xml}->_done;
$r->{reshdr} = { $FU::REQ->{resbody} = $data;
'content-type', 'text/html; charset=UTF-8',
};
$r->{resbody} = '';
} }
sub status($, $code) { $FU::REQ->{status} = $code } sub reset {
sub set_body($, $data) { $FU::REQ->{resbody} = $data } # TODO: replace with a regular 'print' fu->status(200);
fu->set_body('');
$FU::REQ->{reshdr} = {
'content-type', 'text/html',
};
}
sub _validate_header($hdr, $val) { sub _validate_header($hdr, $val) {
@ -610,18 +645,49 @@ sub _error_page($, $code, $title, $msg) {
fu->set_body($body); fu->set_body($body);
} }
sub _flush($, $sock) { sub _finalize {
state $haszlib = eval { require Compress::Raw::Zlib; 1 };
state $haszstd = eval { require Compress::Zstd; 1 };
my $r = $FU::REQ; my $r = $FU::REQ;
# TODO: output compression would be nice
my $xml = $r->{xml}->_done;
$r->{resbody} = $xml if length $xml;
if ($r->{status} == 204) { if ($r->{status} == 204) {
fu->set_header('content-length', undef); delete $r->{reshdr}{'content-length'};
fu->set_header('content-encoding', undef); delete $r->{reshdr}{'content-encoding'};
} else { $r->{resbody} = '';
fu->set_header('content-length', length $r->{resbody});
}
$r->{resbody} = '' if (fu->method//'') eq 'HEAD' || $r->{status} == 204;
} else {
if (($haszlib || $haszstd) && !defined $r->{reshdr}{'content-encoding'} && FU::compress_mimes->{$r->{reshdr}{'content-type'}}) {
$r->{reshdr}{'vary'} = ($r->{reshdr}{'vary'} ? $r->{reshdr}{'vary'}.', ' : '').'accept-encoding'
if ($r->{reshdr}{'vary'}||'') !~ /accept-encoding/i;
if ($haszstd && ($r->{hdr}{'accept-encoding'}||'') =~ /zstd/) {
$r->{resbody} = Compress::Zstd::compress($r->{resbody});
$r->{reshdr}{'content-encoding'} = 'zstd';
} elsif ($haszlib && ($r->{hdr}{'accept-encoding'}||'') =~ /gzip/) {
# Use lower-level API because the higher-level Compress::Zlib loads a whole bunch of other modules.
my $z = Compress::Raw::Zlib::Deflate->new(-WindowBits => Compress::Raw::Zlib::WANT_GZIP(), -Level => 3, -AppendOutput => 1);
$z->deflate($r->{resbody}, my $buf);
$z->flush($buf);
$r->{resbody} = $buf;
$r->{reshdr}{'content-encoding'} = 'gzip';
}
}
$r->{reshdr}{'content-length'} = length $r->{resbody};
$r->{resbody} = '' if (fu->method//'') eq 'HEAD';
}
$r->{reshdr}{'content-type'} .= '; charset=UTF-8' if FU::utf8_mimes->{$r->{reshdr}{'content-type'}};
}
sub _flush($, $sock) {
_finalize;
my $r = $FU::REQ;
if ($sock isa 'FU::fcgi') { if ($sock isa 'FU::fcgi') {
$sock->print('Status: '); $sock->print('Status: ');
$sock->print($r->{status}); $sock->print($r->{status});
@ -663,10 +729,10 @@ FU - Framework Ultimatum: A Lean and Efficient Zero-Dependency Web Framework.
=head1 SYNOPSIS =head1 SYNOPSIS
use v5.36; use v5.36;
use FU -spawn; use FU -spawn, ':html5_';
FU::get qr{/hello/(.+)}, sub($who) { FU::get qr{/hello/(.+)}, sub($who) {
fu->set_body("<h1>Hello, $who!</h1>"); h1_ "Hello, $who!";
}; };
FU::run; FU::run;
@ -675,10 +741,10 @@ FU - Framework Ultimatum: A Lean and Efficient Zero-Dependency Web Framework.
=head2 Distribution Overview =head2 Distribution Overview
This top-level C<FU> module is a web framework. The C<FU> distribution also This top-level C<FU> module is a web development framework. The C<FU>
includes a bunch of modules that the framework depends on or which are distribution also includes a bunch of modules that the framework depends on or
otherwise useful when building web backends. These modules are standalone and which are otherwise useful when building web backends. These modules are
can be used independently of the framework: standalone and can be used independently of the framework:
=over =over
@ -686,6 +752,8 @@ can be used independently of the framework:
=item * L<FU::Pg> - PostgreSQL client. =item * L<FU::Pg> - PostgreSQL client.
=item * L<FU::XMLWriter> - Dynamic XML generation, easy and fast.
=back =back
Note that everything in this distribution requires a moderately recent version Note that everything in this distribution requires a moderately recent version
@ -694,6 +762,38 @@ is).
=head2 Framework Overview =head2 Framework Overview
C<FU> is a mostly straightforward and conventional backend web framework. It
doesn't try to be particularly innovative, but it does attempt to implement
existing ideas in a convenient, coherent and efficient way. There are a few
inherent properties of C<FU>'s design that you will want to be aware of before
digging further:
=over
=item FU is synchronous
C<FU> is an entirely synchronous framework, meaning that a single Perl process
can only handle a single request at a time. This is great in that it simplifies
the implementation, makes debugging easy and performance predictable.
The downside is that you will want to avoid long-running requests as much as
possible. Potentially slow network operations are best delegated to a
background queue. C<FU> intentionally does not support websockets, long-polling
might work but is a bad idea because you'll need to run as many processes as
there are concurrent clients, which gets wasteful very fast. If some UI latency
is acceptable, interval-based polling tends to be simpler to reason about and
more reliable. If such latency is not acceptable, you'll want to run a separate
daemon for asynchronous tasks.
=item FU is buffered
The entire request is read into memory before your code even runs, and the
generated response is buffered in full before a single byte is sent off to the
client. This is, once again, great for simple and predictable code, but
certainly not great if you plan to transfer large files.
=back
=head2 Importing FU =head2 Importing FU
You'll usually want to add the following statement somewhere near the top of You'll usually want to add the following statement somewhere near the top of
@ -705,7 +805,16 @@ The C<-spawn> option tells C<FU> to read running configuration from environment
variables and command-line arguments during early startup, see L</"Running the variables and command-line arguments during early startup, see L</"Running the
Site"> below. Site"> below.
I<TODO: more import options> C<FU> additionally re-exports all tags and functions from L<FU::Util> and
L<FU::XMLWriter>, so you can shorten your C<use> statements:
use FU -spawn, 'json_format', ':html5_';
Is equivalent to:
use FU -spawn;
use FU::Util 'json_format';
use FU::XMLWriter ':html5_';
=head2 Framework Configuration =head2 Framework Configuration