=pod

=for vim
   vim: tw=72 ts=3 sts=3 sw=3 et ai :

=encoding utf8

=head1 NAME

Protocol::Tus - Tus protocol handling


=head1 VERSION

This document describes Protocol::Tus version 0.001.

=begin html


<a href="https://www.perl.org/">
<img alt="Perl Version" src="https://img.shields.io/badge/perl-5.24+-brightgreen.svg">
</a>
<a href="https://badge.fury.io/pl/Protocol-Tus">
<img alt="Current CPAN version" src="https://badge.fury.io/pl/Protocol-Tus.svg">
</a>
<a href="http://cpants.cpanauthors.org/dist/Protocol-Tus">
<img alt="Kwalitee" src="http://cpants.cpanauthors.org/dist/Protocol-Tus.png">
</a>
<a href="http://www.cpantesters.org/distro/O/Protocol-Tus.html?distmat=1">
<img alt="CPAN Testers" src="https://img.shields.io/badge/cpan-testers-blue.svg">
</a>
<a href="http://matrix.cpantesters.org/?dist=Protocol-Tus">
<img alt="CPAN Testers Matrix" src="https://img.shields.io/badge/matrix-@testers-blue.svg">
</a>

=end html

=head1 SYNOPSIS

   use Protocol::Tus;

   my $tus = Protocol::Tus->new(
      model => {
         class => 'Protocol::Tus::LocalDir',
         args  => { root => '/path/to/somewhere' },
      }
   );

   # assume we have some way of getting requests... This method takes
   # care of everything, including X-HTTP-Method-Override
   my $request = get_some_input_request();

   my $response = $tus->HTTP_request(
      $request->{method},   # POST, PATCH, ...
      $request->{headers},  # hash reference
      $request->{id},       # id of the upload or undef/''
      $request->{body},     # or, better, passed as a ref to a scalar
   );
   # The $response is-a Protocol::Tus::Response

   # or you can decide to call the relevant methods directly. 
   $response = $tus->HTTP_HEAD($request->{headers}, $request->{id});
   $response = $tus->HTTP_OPTIONS($request->{headers});
   $response = $tus->HTTP_PATCH($request->@{qw< headers id body >});
   $response = $tus->HTTP_POST($request->@{qw< headers id body >});
   $response = $tus->HTTP_DELETE($request->{headers}, $request->{id});
   # Again, the $response is-a Protocol::Tus::Response


=head1 DESCRIPTION

Implement handling for the Tus protocol in Perl.

=head2 Usage Overview

The constructor requires psasing a I<model>, either as an instance or
with a specification useful for creating an instance:

   my $tus = Protocol::Tus->new(
      model => {
         class => 'Protocol::Tus::LocalDir',
         args  => { root => '/path/to/somewhere' },
      }
   );

The C<model> can be retrieved through the C<model> accessor. It SHOULD
be an instance of a class derived from L<Protocol::Tus::AbstractModel>
and it sure MUST implement its whole interface.

The main method is C<HTTP_request>, which accepts data gathered from an
input request and figures out the best way to address it. This is the
suggested way of using the module, because the Tus specification
requires honoring the C<X-HTTP-Method-Override> header before any action
is done.

Ancillary methods for addressing each specific request method are
available too.

=head2 Example Usage in Mojolicious

A possible usage in a Mojolicious application might start from defining
the base path for the endpoint and how to represent the different
uploads; one possible way is in the example below (note: untested):

   use v5.24;
   use warnings;
   use Mojolicious::Lite -signatures;
   use Protocol::Tus;
   use Ouch qw< bleep >;

   # /tus is the endpoint for creating new uploads or getting general
   # info about the API, while /tus/:id is to interact with one upload.
   # Both are folded onto the same callback that will provide a wrapper
   # around Protocol::Tus->HTTP_request.
   any '/tus'     => \&call_tus;
   any '/tus/:id' => \&call_tus;

   app->start;

   sub call_tus ($c) {
      state $tus = Protocol::Tus->new(
         model => {
            class => 'Protocol::Tus::LocalDir',
            args  => { root => '/path/to/storage' },
         }
      );

      my $request = $c->req;
      my $body = $request->body;
      my $tus_response = $tus->HTTP_request(
         $request->method,
         $request->headers->to_hash,
         $c->param('id'), # possibly undefined, it's OK
         \$body,
      );

      # log any exception. It's a Ouch object, so you might want to
      # take a look into $exception->data too.
      if (defined(my $exception = $tus_response->exception)) {
         $c->app->log->error(bleep($exception));
      }

      # prepare the response to the client
      my $response = $c->res;

      # transfer headers from the $tus_response
      my $response_headers = $response->headers;
      my $thdrs = $tus_response->headers; # hash ref
      $response_headers->header($_ => $thdrs->{$_}) for keys($thdrs->%*);

      # rendering depends on the status code
      my $status = $tus_response->status;
      if ($status == 201) {  # set the Location header to the upload URL
         my $id = $tus_response->id;
         $response_headers->header(Location => "/tus/$id");
         $c->rendered(201);
      }
      elsif ($status == 204) {
         $c->rendered(204);
      }
      else {
         my $message = $tus_response->body // '';
         $c->render(status => $status, text => $message);
      );

      return;
   }



=head1 INTERFACE

The interface is designed to work in a framework-agnostic way, provided
that methods are fed with the correct inputs, which in general are:

=over

=item C<< HTTP method >>

the HTTP method that was used to send the request. It's a string
containing the HTTP method name, case insensitive (it will be turned
into a uppercase string).

=item C<< headers >>

a reference to a hash holding the request headers. It is assumed that
keys are unique in a case-insensitive way, i.e. there are no two keys of
interest (as per the Tus specification) that fold onto the same
lowercase representation; apart from that, the keys can have whatever
case in line with the HTTP specification.

=item C<< id >>

the identifier of an upload that the API is supposed to operate on. The
Tus specification does not dictate any specific path or query structure
and this module follows this pattern, it's up to the caller to figure
out the right *upload identifier* for a request, if any.

=item C<< body >>

the body of a request, e.g. in a C<PATCH> or C<POST> interaction. As the
data might be relevant and best not copied too much around, it's
possible and suggested to pass a reference to the scalar value that
holds the data.

=back

Each method receives the arguments that it needs as a positional
argument list, as detailed below. Method starting with C<HTTP_> return
an instance of L<Protocol::Tus::Response> and never raise exceptions.

You are encouraged to determine the structure of the identifier and, in
case, make sure that it is consistent with the I<model> adopted. As an
example, when using L<Protocol::Tus::LocalDir>, identifiers are
generated by the model and are always valid directory names. In any
case, it will be up to the caller of the different methods to figure out
how to represent these identifiers in the external API and how to
extract them from the requests coming from clients.

=head2 B<< new >>

   my $tus = Protocol::Tus->new(model => $spec_or_object);
   my $tus = Protocol::Tus->new({model => $spec_or_object});

Create a new instance of a Protocol::Tus.

The following keys are supported:

=over

=item C<< id_to_location >>

set the callback to turn an identifier into a C<Location> header.

=item C<< model >>

set the model object or the specification to instantiate one. Required
parameter.

If an object is passed, it's assumed to be already valid; otherwise, the
C<$spec_of_object> is supposed to be a hash reference with keys C<class>
and C<args> to create one.

=back

=head2 B<< id_to_location >>

   my $coderef = $tus->id_to_location;

Accessor to the optional callback to turn an upload identifier into a
C<Location> header needed in the C<201> response when creating a new
upload. If not set, the caller is supposed to generate the C<Location>
header independently.

=head2 B<< model >>

   my $model = $tus->model;

Accessor to the model object. Most probably it will be something derived
from L<Protocol::Tus::AbstractModel>, like L<Protocol::Tus::LocalDir>.

=head2 B<< HTTP_request >>

   my $response = $tus->HTTP_request(
      $method,   # POST, PATCH, ...
      $headers,  # hash reference
      $id,       # id of the upload or undef/''
      $body,     # or, better, passed as a ref to a scalar
   );

Handle an incoming request. Not all arguments will be used but they are
required to provide a single entry point for the whole Tus interface.

=head3 B<< HTTP_HEAD >>

   my $response = $tus->HTTP_HEAD($headers, $id);

=head2 B<< HTTP_OPTIONS >>

   my $response = $tus->HTTP_OPTIONS;

=head2 B<< HTTP_PATCH >>

   my $response = $tus->HTTP_PATCH($headers, $id, $body);

=head2 B<< HTTP_POST >>

   my $response = $tus->HTTP_POST($headers, $id, $body);

=head2 B<< HTTP_DELETE >>

   my $response = $tus->HTTP_DELETE($headers, $id);


=head1 BUGS AND LIMITATIONS

Minimul perl version 5.24.

Report bugs through Codeberg (patches welcome) at
L<https://codeberg.org/polettix/Protocol-Tus>.


=head1 AUTHOR

Flavio Poletti <flavio@polettix.it>

=head1 COPYRIGHT AND LICENSE

Copyright 2026 by Flavio Poletti <flavio@polettix.it>

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.


=cut
