FU: Output compression, XMLWriter integration, import options, some docs
This commit is contained in:
parent
63f524622b
commit
d0665ef7c2
1 changed files with 141 additions and 32 deletions
173
FU.pm
173
FU.pm
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue