use strict;
use warnings;
package Test::Deep::JType;
# ABSTRACT: Test::Deep helpers for JSON::Typist data
$Test::Deep::JType::VERSION = '0.002';
use Test::Deep 1.123 (); # for LeafWrapper and obj~~re diagnostics

use Exporter 'import';
our @EXPORT = qw( jcmp_deeply jstr jnum jbool jtrue jfalse );

#pod =head1 OVERVIEW
#pod
#pod L<Test::Deep> is a very useful library for testing data structures.
#pod Test::Deep::JType extends it with routines for testing
#pod L<JSON::Typist>-annotated data.
#pod
#pod By default, Test::Deep's C<cmp_deeply> will interpret plain numbers and strings
#pod as shorthand for C<shallow(...)> tests, meaning that the corresponding input
#pod data will also need to be a plain number or string.  That means that this test
#pod won't work:
#pod
#pod   my $json  = q[ { "key": "value" } ];
#pod   my $data  = decode_json($json);
#pod   my $typed = JSON::Typist->new->apply_types( $data );
#pod
#pod   cmp_deeply($typed, { key => "value" });
#pod
#pod ...because C<"value"> will refuse to match an object.  You I<could> wrap each
#pod string or number to be compared in C<str()> or C<num()> respectively, but this
#pod can be a hassle, as well as a lot of clutter.
#pod
#pod C<jcmp_deeply> is exported by Test::Deep::JType, and behaves just like
#pod C<cmp_deeply>, but plain numbers and strings are wrapped in C<str()> tests
#pod rather than shallow ones, so they always compare with C<eq>.
#pod
#pod To test that the input data matches the right type, other routines are exported
#pod that check type as well as content.
#pod
#pod =cut

#pod =func jcmp_deeply
#pod
#pod This behaves just like Test::Deep's C<jcmp_deeply> but wraps plain scalar and
#pod number expectations in C<str>, meaning they're compared with C<eq> only,
#pod instead of also asserting that the found value must not be an object.
#pod
#pod =cut

sub jcmp_deeply {
  local $Test::Builder::Level = $Test::Builder::Level + 1;
  local $Test::Deep::LeafWrapper = \&Test::Deep::str;
  Test::Deep::cmp_deeply(@_);
}

#pod =func jstr
#pod
#pod =func jnum
#pod
#pod =func jbool
#pod
#pod =func jtrue
#pod
#pod =func jfalse
#pod
#pod These routines are plain old Test::Deep-style assertions that check not only
#pod for data equivalence, but also that the data is the right type.
#pod
#pod C<jstr>, C<jnum>, and C<jbool> take arguments, which are passed to the non-C<j>
#pod version of the test used in building the C<j>-style version.  In other words,
#pod you can write:
#pod
#pod   jcmp_deeply(
#pod     $got,
#pod     {
#pod       name => jstr("Ricardo"),
#pod       age  => jnum(38.2, 0.01),
#pod       calm => jbool(1),
#pod       cool => jbool(),
#pod       collected => jfalse(),
#pod     },
#pod   );
#pod
#pod C<jtrue> and C<jfalse> are shorthand for C<jbool(1)> and C<jbool(0)>,
#pod respectively.
#pod
#pod =cut

sub jstr  { Test::Deep::all( Test::Deep::obj_isa('JSON::Typist::String'),
                             Test::Deep::str(@_)) }

sub jnum  { Test::Deep::all( Test::Deep::obj_isa('JSON::Typist::Number'),
                             Test::Deep::num(@_)) }

sub jbool {
  Test::Deep::all(
    Test::Deep::any(
      Test::Deep::obj_isa('JSON::XS::Boolean'),
      Test::Deep::obj_isa('JSON::PP::Boolean'),
    ),
    (@_ ? Test::Deep::bool(@_) : ()),
  );
}

sub jtrue  { jbool(1) }
sub jfalse { jbool(0) }

1;

__END__

=pod

=encoding UTF-8

=head1 NAME

Test::Deep::JType - Test::Deep helpers for JSON::Typist data

=head1 VERSION

version 0.002

=head1 OVERVIEW

L<Test::Deep> is a very useful library for testing data structures.
Test::Deep::JType extends it with routines for testing
L<JSON::Typist>-annotated data.

By default, Test::Deep's C<cmp_deeply> will interpret plain numbers and strings
as shorthand for C<shallow(...)> tests, meaning that the corresponding input
data will also need to be a plain number or string.  That means that this test
won't work:

  my $json  = q[ { "key": "value" } ];
  my $data  = decode_json($json);
  my $typed = JSON::Typist->new->apply_types( $data );

  cmp_deeply($typed, { key => "value" });

...because C<"value"> will refuse to match an object.  You I<could> wrap each
string or number to be compared in C<str()> or C<num()> respectively, but this
can be a hassle, as well as a lot of clutter.

C<jcmp_deeply> is exported by Test::Deep::JType, and behaves just like
C<cmp_deeply>, but plain numbers and strings are wrapped in C<str()> tests
rather than shallow ones, so they always compare with C<eq>.

To test that the input data matches the right type, other routines are exported
that check type as well as content.

=head1 FUNCTIONS

=head2 jcmp_deeply

This behaves just like Test::Deep's C<jcmp_deeply> but wraps plain scalar and
number expectations in C<str>, meaning they're compared with C<eq> only,
instead of also asserting that the found value must not be an object.

=head2 jstr

=head2 jnum

=head2 jbool

=head2 jtrue

=head2 jfalse

These routines are plain old Test::Deep-style assertions that check not only
for data equivalence, but also that the data is the right type.

C<jstr>, C<jnum>, and C<jbool> take arguments, which are passed to the non-C<j>
version of the test used in building the C<j>-style version.  In other words,
you can write:

  jcmp_deeply(
    $got,
    {
      name => jstr("Ricardo"),
      age  => jnum(38.2, 0.01),
      calm => jbool(1),
      cool => jbool(),
      collected => jfalse(),
    },
  );

C<jtrue> and C<jfalse> are shorthand for C<jbool(1)> and C<jbool(0)>,
respectively.

=head1 AUTHOR

Ricardo Signes <rjbs@cpan.org>

=head1 COPYRIGHT AND LICENSE

This software is copyright (c) 2016 by Ricardo Signes.

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

=cut
