package Mojolicious::Plugin::Gzip;
use Mojo::Base 'Mojolicious::Plugin';
use Mojo::Util qw/gzip/;
use Scalar::Util qw/reftype/;

our $VERSION = '0.04';

sub register {
    my (undef, $app, $config) = @_;

    my $min_size;
    if (defined $config) {
        die 'config must be a hash reference'
            unless reftype($config) eq 'HASH';
        if (keys %$config) {
            $min_size = delete $config->{min_size} // '';
            die 'invalid key passed to Mojolicious::Plugin::Gzip (only min_size is allowed)'
                if keys %$config;
            die 'min_size must be a positive integer'
                unless $min_size =~ /^\d+$/ and $min_size > 0;
        }
    }
    $min_size //= 860;

    $app->hook(after_dispatch => sub {
        my ($c) = @_;
        my $req = $c->req;
        my $res = $c->res;

        my $accept_encoding = $req->headers->accept_encoding // '';
        my $body = $res->body;
        my $body_size = $res->body_size // 0;

        return unless $accept_encoding =~ /gzip/i
                and $body_size >= $min_size
                and $res->code == 200
                and not $res->headers->content_encoding;

        if (my $etag = $res->headers->etag) {
            if (length $etag > 2 and substr($etag, 0, 1) eq '"' and substr($etag, -1, 1) eq '"') {
                $etag = substr($etag, 1, length($etag) - 2);
            } else {
                $app->log->warn("Found either empty ETag or ETag not surrounded by quotes: '$etag'");
            }

            $res->headers->etag(qq{"$etag-gzip"});
        }

        my $zipped_body = gzip $body;
        $res->body($zipped_body);
        $res->headers->content_length(length $zipped_body);
        $res->headers->append(Vary => 'Accept-Encoding');
        $res->headers->content_encoding('gzip');
    });
}

1;
__END__

=encoding utf-8

=head1 NAME

Mojolicious::Plugin::Gzip - Plugin to Gzip Mojolicious responses

=head1 STATUS

=for html <a href="https://travis-ci.org/srchulo/Mojolicious-Plugin-Gzip"><img src="https://travis-ci.org/srchulo/Mojolicious-Plugin-Gzip.svg?branch=master"></a>

=head1 SYNOPSIS

  # Mojolicious::Lite
  plugin 'Gzip';

  # With minimum size in bytes required before gzipping. Default is 860.
  plugin Gzip => {min_size => 1500};

  # Mojolicious
  $app->plugin('Gzip');

  # With minimum size in bytes required before gzipping. Default is 860.
  $app->plugin(Gzip => {min_size => 1500});

=head1 WARNING

This module can be inefficient for large static files. It loads the entire file into memory and then gzips it, resulting in both the original file and
the gzipped one in memory at once. By default, L<Mojolicious> serves static files from disk, which is much more memory efficient. For static files, you may want
to compress them ahead of time and use something like L<Mojolicious::Static::Role::Compressed> to serve them, or a service like
L<Cloudlfare|https://www.cloudflare.com/>, which will compress files for you. For gzipping dynamic content, you should consider using L<Mojolicious::Renderer/compress>
instead.

=head1 DESCRIPTION

L<Mojolicious::Plugin::Gzip> gzips all responses equal to or greater than a L</min_size> by using the L<Mojolicious/after_dispatch> hook.
L<Mojolicious::Plugin::Gzip> will only gzip a response if all of these conditions are met:

=over 4

=item *

The L<Mojo::Headers/accept_encoding> header contains 'gzip'.

=item *

The L<Mojo::Content/body_size> of the response is greater than or equal to L</min_size>.

=item *

The L<Mojo::Message::Response/code> is 200.

=item *

The L<Mojo::Headers/content_encoding> for the response is not set.

=back

L<Mojolicious::Plugin::Gzip> will do these things if those conditions are met:

=over 4

=item *

Set L<Mojo::Message/body> to the gzipped version of the previous L<Mojo::Message/body>.

=item *

Set L<Mojo::Message/body_size> to the size of the gzipped content.

=item *

Set L<Mojo::Headers/content_encoding> to "gzip".

=item *

If L<Mojo::Headers/etag> was set, append "-gzip" to the existing L<Mojo::Headers/etag>. This is done according to L<RFC-7232|https://tools.ietf.org/html/rfc7232#section-2.3.3>, which
states that ETags should be content-coding aware.

=item *

Use L<Mojo::Headers/append> to append "Accept-Encoding" to the L<Mojo::Headers/vary> header.

=back

=head1 OPTIONS

=head2 min_size

  # Mojolicious::Lite
  plugin 'Gzip' => {min_size => 1500};

  # Mojolicious
  $app->plugin(Gzip => {min_size => 1500});

Sets the minimum L<Mojo::Content/body_size> required before response content will be gzipped. If the L<Mojo::Content/body_size> is greater than or equal to L</min_size>, then it will be
gzipped. Default is 860.

=head1 AUTHOR

Adam Hopkins E<lt>srchulo@cpan.orgE<gt>

=head1 COPYRIGHT

Copyright 2019- Adam Hopkins

=head1 LICENSE

This library is free software; you can redistribute it and/or modify
it under the same terms as Perl itself.

=head1 SEE ALSO

=over 4

=item *

L<MojoX::Encode::Gzip>

=item *

L<Mojolicious>

=item *

L<https://mojolicious.org>

=back

=cut
