=pod

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

=encoding utf8

=head1 NAME

Data::Resolver::Asset - representation of resolved asset


=head1 SYNOPSIS

   use Data::Resolver::Asset;

   # can create assets from files, filehandles, raw data. Add a key
   # in the latter two cases if we want to eventually get a file.
   my $asset_from_file = Data::Resolver::Asset->new(file => $path);
   my $asset_from_fh   = Data::Resolver::Asset->new(
      fh => $fh, key => 'foo-bar.pdf');
   my $asset_from_raw  = Data::Resolver::Asset->new(
      raw => $raw_octets, key => 'galook.png');

   # get a filehandle
   my $filehandle = $asset->filehandle; # also ->$fh

   # if filehandles from in-memory scalars are not viable...
   my $file_filehandle = $asset->filehandle(not_from_memory => 1);

   # binmode can be passed too
   my $utf8_fh = $asset->filehandle(binmode => ':encoding(utf8)');

   # get a file
   my $path = $asset->file;
   my $alt_path = $asset->save_as('/path/to/somefile.txt');

   # get a reference to raw data
   my $rref = $asset->raw_ref;

   # get the raw data
   my $raw_octets = $asset->raw_data;

   # get slightly less raw data
   my $characters_not_octets = $asset->decoded_as_utf8;
   my $chars_not_octets = $asset->decoded_as($some_encoding);

   # parse JSON on the fly
   my $data_structure = $asset->parsed_as_json;

   # initializing with a filehandle and then getting a filehandle back makes
   # the asset unuseable for further actions
   my $asset_from_fh   = Data::Resolver::Asset->new(
      fh => $fh, key => 'foo-bar.pdf');
   my $fh_back = $asset->fh;
   die 'whatever' if ! $asset->is_useable; # dies
   $asset->assert_useable;                 # dies too

   # call ->file or ->raw_ref as the first method to cache data if you need
   # the object to stay useable



=head1 DESCRIPTION

This class, based on L<Moo>, provides a representation for a resolved
I<asset>.  As such, it's not generally meant to be used directly, although
it can be leveraged to provide transformation across different
representations (in-memory data, file in the filesystem, filehandles) in
case of need.

=head2 Input formats

An I<asset> supports three main input interfaces: B<file>, B<filehandle>,
and B<raw_data> (provided either as a reference to the raw data, or as a
plain string of octets).


=head2 Output formats

The class provides a wide range of ways to access the data in the asset.

At a basic level there are the following:

=over

=item *

A L</file>, as a path in the filesystem.

=item *

A L</filehandle>, which can be opened either from in-memory data or from the
filesystem (with some control over it, see L</filehandle>).

=item *

L</raw_data> as a plain scalar. If it has too much data, it's better to
avoid too much copying around with:

=item *

L</raw_ref> as a reference to a buffer with the raw data.

=back

The class also supports some additional convenience accessors for
interpreting the raw data in a specific way:

=over

=item *

L</decoded_as> helps getting a string of characters decoded according to a
specific encoding standard instead of a stream of octets;

=item *

L</decoded_as_utf8> provides back a string of characters obtained by
decoding the raw data as C<UTF-8>

=item *

L</parsed_as_json> decodes the data as JSON and provides the resulting data
structure back (L<JSON::PP> is used for parsing).

=back

=head2 Useability

When an object is created starting from a filehandle, getting the filehandle
as the first representation makes the object I<unuseable>. This is because
the requesting code might consume some of the filehandle and this class is
not meant to do any kind of synchronization to this regard.

If it is important that the object continues to be I<useable>, it's
necessary to first cache the data from the filehandle, either in-memory (by
calling L</raw_ref> or L</raw_data>) or in the filesystem (with L</file>).
All following calls to L</filehandle> will leverage the cache from this
point on. It's possible to query the useability status with L</is_useable>
or the exception-throwing counterpart L</assert_useable>.

=head2 Setting a key

When it's necessary to get a L</file> back, it might be important that a
L</key> is set in L</new>. This is inferred directly from the filename in case
the object is initialized with a file; otherwise, the saved file will have a
random name without a specific extension, which might upset some libraries
when they expect to infer the file type from the file name.

In these cases, it's possible to pass a value for C<key> upon initialization
with L</new>.

=head2 Exceptions

Errors generally lead to an exception. By default it is thrown with
L<Carp>'s C<croak> function; it's possible to optionally use L<Ouch> by
passing a true value to the C<ouch> initialization option, or by setting the
package variable C<$Data::Resolver::RoleComplain::use_ouch>.

If the value passed/set is an array reference, it is used to call L<Ouch>'s
C<import> method, passing the array contents as the initialization list.
Otherwise, any true value just loads the module. In both cases, L<Ouch>'s
C<ouch> function will be used to throw excptions.


=head1 INTERFACE

The module provides an object-oriented interface, supporting the following
methods.

=head2 B<< assert_useable >>

   $asset->assert_useable; # might throw an exception

Throw an exception if the asset object is not useable (see L</Useability>
and L</Exceptions>).

=head2 B<< complain >>

   $asset->complain($code, $message, $data);

Throw an exception, possibly through L<Ouch>. See L</Exceptions>.

=head2 B<< decoded_as >>

   my $characters_not_octets = $asset->decoded_as($specific_encoding);

Decode the raw data according to the C<$specific_encoding> and get back a
string of characters (not plain octets).

=head2 B<< decoded_as_utf8 >>

   my $characters_not_octets = $asset->decoded_as_utf8;

Decode the raw data according to the C<UTF-8> encoding and get back a string
of characters (not plain octets).

=head2 B<< fh >>

Alias for L</filehandle>.

=head2 B<< file >>

   my $path = $asset->file;

Get the asset as a file path in the filesystem.

If the asset is initialized with something different than a file, the file
name will be determined according to L</key>:

=over

=item *

if defined, it is used as the file name, saved in a temporary directory;

=item *

otherwise, a temporary file name is used.

=back

See also L</Setting a key>.


=head2 B<< filehandle >>

   my $fh = $asset->filehandle(%options);  # OR
   my $fh = $asset->filehandle(\%options);

Get a filehandle suitable for reading data.

Supported options:

=over

=item *

C<binmode>, a string to call C<bindmode()> on the filehandle;

=item *

C<not_from_memory>, a boolean option that prevents generating a filehandle
by opening the L</raw_ref>, if available. This might be important if your
consumer needs a filehandle from the filesystem and would complain
otherwise.

=back

Getting a filehandle from an instance that has been initialized with a
filehandle might lead to L</Useability> problems. If you plan to reuse the
asset object instance multiple times, it's better to first call either
L</file> or L</raw_ref> (depending on your preference about where to
position the cache).


=head2 B<< key >>

   my $key = $asset->key;

Get the key associated with the asset. In case the data need to be saved on
the disk, this is used as the last part of the full path (i.e. the I<file
name> part).

See L</Setting a key>.

=head2 B<< is_useable >>

   my $bool = $asset->is_useable;

Test whether the object is useable or not; see L</Useability>.

=head2 B<< new >>

   my $asset = Data::Resolver::Asset->new(%args);   # OR
   my $asset = Data::Resolver::Asset->new(\%args);

Construct a new object instance. The constructor can receive either
key-value pairs, or a reference to to a hash.

Supported keys:

=over

=item *

C<file>, a path to a file in the filesystem. Initializing with a C<file>
also allows automatic inference of the C<key> (L</key>), using the
C<basename> of the file.

=item *

C<filehandle>, a handle that is supposed to support reading from. When
initializing an object with a C<filehandle>, make sure to read the notes in
L</Useability>.

=item *

C<key>, a string that provides a filename in case a file is needed. This is
inferred automatically when the object is initialized with a C<file>, but
SHOULD be passed in the constructor if a L</file> will be needed later and
it is important that the file has a specific filename (that is the same as
the C<key>). This might be e.g. important if a library uses the filename to
infer other metadata about the file, e.g. a specific graphic format.

See also L</Setting a key>.

=item *

C<raw>, a buffer of raw data. It is assumed to contain octets/bytes, not
decoded characters; no effort is done to check this, though, so don't pass
non-raw data.

The buffer can be passed either as a plain scalar, or as a reference to a
scalar; the latter approach helps avoiding too much copying around.

=item *

C<ouch>, an option to turn on using L<Ouch> instead of complaining with
plain L<Carp>.

Any true value will activate L<Ouch>; passing a reference to an array allows
calling L<Ouch>'s C<import> method with the provided elements, for finer
tuning of the import (e.g. to pass option C<:trytiny_var>).

See also L</Exceptions> for alternatives ways of setting this behaviour.

=back

There is no effort to check that only one representation is passed, nor that
different representations are consistent with one another. Just don't do it.


=head2 B<< not_found >>

   $asset->not_found($something);

Wrapper around L</complain> to raise an error 404 regarding the
unavailability of C<$something>.

=head2 B<< parsed_as_json >>

   my $perl_data_structure = $asset->parsed_as_json

Treat the raw data as a JSON string and get back the data it represents.

=head2 B<< raw_data >>

   my $octets = $self->raw_data;

Get the raw data representation (octets, not characters).

=head2 B<< raw_ref >>

   my $ref_to_octets = $self->raw_ref;

Get a reference to the raw data representation (see L</raw_data>). Getting a
reference helps avoiding copying data around, which might be important with
big assets.

=head2 B<< save_as >>

   my $save_path = $self->save_as($some_path);

Save a copy of the data at the path indicated by C<$some_path>. The function
returns the path itself, for easy chaining.

=head2 B<< use_ouch >>

   my $bool = $self->use_ouch;

Read-only accessor indicating whether L<Ouch> is used for L</Exceptions>:

=head1 AUTHOR

Flavio Poletti <flavio@polettix.it>

=head1 COPYRIGHT AND LICENSE

Copyright 2023 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
