package File::DataClass::Storage::JSON;

use boolean;
use namespace::autoclean;

use File::DataClass::Functions qw( extension_map throw );
use File::DataClass::Types     qw( Object );
use JSON::MaybeXS              qw( JSON );
use Try::Tiny;
use Moo;

extends q(File::DataClass::Storage);

extension_map 'JSON' => '.json';

# Private functions
my $_build_transcoder = sub {
   my $options = shift; my $json = JSON->new;

   for (grep { $_ ne 'reboolify' } keys %{ $options }) {
      $json = $json->$_( $options->{ $_ } );
   }

   return $json;
};

my $_reboolify; $_reboolify = sub {
   my $in = shift; my $ref = ref $in;

   if (not $ref) { return $in }
   elsif  ($ref eq 'HASH') {
      return { map { $_ => $_reboolify->( $in->{ $_ } ) } keys %{ $in } };
   }
   elsif  ($ref eq 'ARRAY') { return [ map { $_reboolify->( $_ ) } @{ $in } ] }
   elsif  ($ref =~ m{ ::Boolean \z }mx) { return ${ $in } ? true : false }

   return $in;
};

# Public attributes
has '+extn'          => default => '.json';

has '+read_options'  => builder => sub { { utf8 => false, } };

has '+write_options' => builder => sub { {
   canonical         => true, convert_blessed => true,
   pretty            => true, utf8            => false, } };

# Private attributes
has '_decoder'       => is => 'lazy', isa => Object,
   builder           => sub { $_build_transcoder->( $_[ 0 ]->read_options  ) };

has '_encoder'       => is => 'lazy', isa => Object,
   builder           => sub { $_build_transcoder->( $_[ 0 ]->write_options ) };

# Public methods
sub read_from_file {
   my ($self, $rdr) = @_; my $json = $self->_decoder; my $data;

   $self->encoding and $rdr->encoding( $self->encoding );
   $rdr->is_empty  and return {};

   try   {
      $data = $json->decode( $rdr->all );
      $self->read_options->{reboolify} and $data = $_reboolify->( $data );
   }
   catch { s{ at \s [^ ]+ \s line \s\d+\. }{}mx; throw "${_} in file ${rdr}" };

   return $data;
}

sub write_to_file {
   my ($self, $wtr, $data) = @_; my $json = $self->_encoder;

   $self->encoding and $wtr->encoding( $self->encoding );
   $wtr->print( $json->encode( $data ) );
   return $data;
}

1;

__END__

=pod

=head1 Name

File::DataClass::Storage::JSON - Read/write JSON data storage model

=head1 Synopsis

   use Moo;

   extends 'File::DataClass::Schema';

   has '+storage_class' => default => 'JSON';

=head1 Description

Uses L<JSON::MaybeXS> to read and write JSON files

=head1 Configuration and Environment

Defines the following attributes;

=over 3

=item C<extn>

The extension appended to filenames. Defaults to F<.json>

=item C<read_options>

This hash reference is used to customise the JSON decoder object used when
reading the file. It defaults to C<reboolify> false (causes booleans to be
inflated to objects) and C<utf8> false (the io object does the encoding).  This
filter would cause the data to be untainted (running C<suid>). I shit you not

   filter_json_object => sub { $_[ 0 ] }

=item C<write_options>

This hash reference is used to customise the JSON encoder object used when
writing the file. It defaults to C<canonical> true (sorts the keys in the
hashes), C<convert_blessed> true (looks for and uses the C<TO_JSON> method),
C<pretty> true (uses whitespace for indentation), and C<utf8> false (the io
object does the encoding)

=back

=head1 Subroutines/Methods

=head2 read_from_file

API required method. Calls L<JSON::MaybeXS/decode> to parse the input

=head2 write_to_file

API required method. Calls L<JSON::MaybeXS/encode> to generate the output

=head1 Diagnostics

None

=head1 Dependencies

=over 3

=item L<File::DataClass::Storage>

=item L<JSON::MaybeXS>

=back

=head1 Incompatibilities

There are no known incompatibilities in this module

=head1 Bugs and Limitations

There are no known bugs in this module.
Please report problems to the address below.
Patches are welcome

Using the module L<JSON::XS> causes the round trip test to fail

=head1 Author

Peter Flanigan, C<< <pjfl@cpan.org> >>

=head1 License and Copyright

Copyright (c) 2015 Peter Flanigan. All rights reserved

This program is free software; you can redistribute it and/or modify it
under the same terms as Perl itself. See L<perlartistic>

This program is distributed in the hope that it will be useful,
but WITHOUT WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE

=cut

# Local Variables:
# mode: perl
# tab-width: 3
# End:
