package JSON::Validator::Error;
use Mojo::Base -base;

use overload q("") => \&to_string, bool => sub {1}, fallback => 1;

our $MESSAGES = {
  allOf => {type => '/allOf Expected %3 - got %4.'},
  anyOf => {type => '/anyOf Expected %3 - got %4.'},
  array => {
    additionalItems => 'Invalid number of items: %3/%4.',
    maxContains     => 'Contains too many items: %3/%4.',
    maxItems        => 'Too many items: %3/%4.',
    minContains     => 'Contains not enough items: %3/%4.',
    minItems        => 'Not enough items: %3/%4.',
    uniqueItems     => 'Unique items required.',
    contains        => 'No items contained.',
  },
  const   => {const => 'Does not match const: %3.'},
  enum    => {enum  => 'Not in enum list: %3.'},
  integer => {
    ex_maximum => '%3 >= maximum(%4)',
    ex_minimum => '%3 <= minimum(%4)',
    maximum    => '%3 > maximum(%4)',
    minimum    => '%3 < minimum(%4)',
    multipleOf => 'Not multiple of %3.',
  },
  not    => {not  => 'Should not match.'},
  null   => {type => 'Not null.'},
  number => {
    ex_maximum => '%3 >= maximum(%4)',
    ex_minimum => '%3 <= minimum(%4)',
    maximum    => '%3 > maximum(%4)',
    minimum    => '%3 < minimum(%4)',
    multipleOf => 'Not multiple of %3.',
  },
  object => {
    additionalProperties => 'Properties not allowed: %3.',
    maxProperties        => 'Too many properties: %3/%4.',
    minProperties        => 'Not enough properties: %3/%4.',
    required             => 'Missing property.',
    dependencies         => 'Missing property. Dependee: %3.',
  },
  oneOf => {
    all_rules_match => 'All of the oneOf rules match.',
    n_rules_match   => 'oneOf rules %3 match.',
    type            => '/oneOf Expected %3 - got %4.',
  },
  string => {
    pattern   => 'String does not match %3.',
    maxLength => 'String is too long: %3/%4.',
    minLength => 'String is too short: %3/%4.',
  }
};

has details => sub { [qw(generic generic)] };

has message => sub {
  my $self    = shift;
  my $details = $self->details;
  my $message;

  if (($details->[0] || '') eq 'format') {
    $message = '%3';
  }
  elsif (($details->[1] || '') eq 'type' and @$details == 3) {
    $message = 'Expected %1 - got %3.';
  }
  elsif (my $group = $MESSAGES->{$details->[0]}) {
    $message = $group->{$details->[1] || 'default'};
  }

  return join ' ', Failed => @$details unless defined $message;

  $message =~ s!\%(\d)\b!{$details->[$1 - 1] // ''}!ge;
  return $message;
};

has path => '/';

sub new {
  my $class = shift;
  return $class->SUPER::new unless @_;

  # Constructed with attributes
  return $class->SUPER::new($_[0]) if ref $_[0] eq 'HASH';

  # Constructed with ($path, ...)
  my $self = $class->SUPER::new;

  my $path = ref $_[0] ? join '/', '', map { s!~!~0!g; s!/!~1!g; $_ } @{shift(@_)} : shift || '/';
  $self->{path} = $path || '/';

  # Constructed with ($path, $message) or ($path, \@details)
  return !@_ ? $self : ref $_[0] ? $self->details(shift) : $self->message(shift);
}

sub to_string { sprintf '%s: %s', $_[0]->path, $_[0]->message }
sub TO_JSON   { {message => $_[0]->message, path => $_[0]->path} }

1;

=encoding utf8

=head1 NAME

JSON::Validator::Error - JSON::Validator error object

=head1 SYNOPSIS

  use JSON::Validator::Error;
  my $err = JSON::Validator::Error->new($path, $message);

=head1 DESCRIPTION

L<JSON::Validator::Error> is a class representing validation errors from
L<JSON::Validator>.

=head1 ATTRIBUTES

=head2 details

  my $error     = $error->details(["generic", "generic"]);
  my $error     = $error->details([qw(array type object)]);
  my $error     = $error->details([qw(format date-time Invalid)]);
  my $array_ref = $error->details;

Details about the error:

=over 2

=item 1.

Often the category of tests that was run. Example values: allOf, anyOf, array,
const, enum, format, integer, not, null, number, object, oneOf and string.

=item 2.

Often the test that failed. Example values: additionalItems,
additionalProperties, const, enum, maxItems, maxLength, maxProperties, maximum,
minItems, minLength.  minProperties, minimum, multipleOf, not, null, pattern,
required, type and uniqueItems,

=item 3.

The rest of the list contains parameters for the test that failed. It can be a
plain human-readable string or numbers indicating things such as max/min
values.

=back

=head2 message

  my $str = $error->message;

A human readable description of the error. Defaults to being being constructed
from L</details>. See the C<$MESSAGES> variable in the source code for more
details.

As an EXPERIMENTAL hack you can localize C<$JSON::Validator::Error::MESSAGES>
to get i18n support. Example:

  sub validate_i18n {
    local $JSON::Validator::Error::MESSAGES = {
      allOf => {type => '/allOf Forventet %3 - fikk %4.'},
    };

    my @error_norwegian = $jv->validate({age => 42});
  }

Note that the error messages might contain a mix of English and the local
language. Run some tests to see how it looks.

=head2 path

  my $str = $error->path;

A JSON pointer to where the error occurred. Defaults to "/".

=head1 METHODS

=head2 new

  my $error = JSON::Validator::Error->new(\%attributes);
  my $error = JSON::Validator::Error->new($path, \@details);
  my $error = JSON::Validator::Error->new($path, \@details);

Object constructor.

=head2 to_string

  my $str = $error->to_string;

Returns the "path" and "message" part as a string: "$path: $message".

=head1 OPERATORS

L<JSON::Validator::Error> overloads the following operators:

=head2 bool

  my $bool = !!$error;

Always true.

=head2 stringify

  my $str = "$error";

Alias for L</to_string>.

=head1 SEE ALSO

L<JSON::Validator>.

=cut