package JSON::Patch;
use 5.006;
use strict;
use warnings FATAL => 'all';
use parent 'Exporter';
use Carp qw(croak);
use Struct::Diff 0.96;
use Struct::Path 0.82 qw(path);
use Struct::Path::JsonPointer 0.04 qw(path2str str2path);
our @EXPORT_OK = qw(
diff
patch
);
=head1 NAME
JSON::Patch - JSON Patch (rfc6902) for perl structures
=begin html
=end html
=head1 VERSION
Version 0.04
=cut
our $VERSION = '0.04';
=head1 SYNOPSIS
use Test::More tests => 2;
use JSON::Patch qw(diff patch);
my $old = {foo => ['bar']};
my $new = {foo => ['bar', 'baz']};
my $patch = diff($old, $new);
is_deeply(
$patch,
[
{op => 'add', path => '/foo/1', value => 'baz'}
]
);
patch($old, $patch);
is_deeply($old, $new);
=head1 EXPORT
Nothing is exported by default.
=head1 SUBROUTINES
=head2 diff
Calculate patch for two arguments:
$patch = diff($old, $new);
Convert L diff to JSON Patch when single arg passed:
require Struct::Diff;
$patch = diff(Struct::Diff::diff($old, $new));
=cut
sub diff($;$) {
my $diff = @_ == 2
? Struct::Diff::diff($_[0], $_[1], noO => 1, noU => 1, trimR => 1)
: $_[0];
my @stask = Struct::Diff::list_diff($diff, sort => 1);
my ($hunk, @patch, $path);
while (@stask) {
($path, $hunk) = splice @stask, -2, 2;
if (exists ${$hunk}->{A}) {
push @patch, {op => 'add', value => ${$hunk}->{A}};
} elsif (exists ${$hunk}->{N}) {
push @patch, {op => 'replace', value => ${$hunk}->{N}};
} elsif (exists ${$hunk}->{R}) {
push @patch, {op => 'remove'};
} else {
next;
}
$patch[-1]->{path} = path2str($path);
}
return \@patch;
}
=head2 patch
Apply patch.
patch($target, $patch);
=cut
sub patch($$) {
croak "Arrayref expected for patch" unless (ref $_[1] eq 'ARRAY');
for my $hunk (@{$_[1]}) {
croak "Hashref expected for patch item" unless (ref $hunk eq 'HASH');
croak "Undefined op value" unless (defined $hunk->{op});
croak "Path parameter missing" unless (exists $hunk->{path});
my $path = eval { str2path($hunk->{path}) }
or croak "Failed to parse 'path' pointer";
if ($hunk->{op} eq 'add' or $hunk->{op} eq 'replace') {
croak "Value parameter missing" unless (exists $hunk->{value});
path(
$_[0],
$path,
assign => $hunk->{value},
expand => 1,
insert => $hunk->{op} eq 'add',
strict => 1,
);
} elsif ($hunk->{op} eq 'remove') {
eval { path($_[0], $path, delete => 1) } or
croak "Path does not exist";
} elsif ($hunk->{op} eq 'move' or $hunk->{op} eq 'copy') {
my $from = eval { str2path($hunk->{from}) } or
croak "Failed to parse 'from' pointer";
my @found = path(
$_[0],
$from,
delete => $hunk->{op} eq 'move',
deref => 1
);
croak "Source path does not exist" unless (@found);
path($_[0], $path, assign => $found[0], expand => 1);
} elsif ($hunk->{op} eq 'test') {
croak "Value parameter missing" unless (exists $hunk->{value});
my @found = path($_[0], $path, deref => 1) or
croak "Path does not exist";
my $diff = Struct::Diff::diff($found[0], $hunk->{value}, noU => 1);
croak "Test failed" if (keys %{$diff});
} else {
croak "Unsupported op '$hunk->{op}'";
}
}
}
=head1 AUTHOR
Michael Samoglyadov, C<< >>
=head1 BUGS
Please report any bugs or feature requests to C,
or through the web interface at
L. I will be
notified, and then you'll automatically be notified of progress on your bug as
I make changes.
=head1 SUPPORT
You can find documentation for this module with the perldoc command.
perldoc JSON::Patch
You can also look for information at:
=over 4
=item * RT: CPAN's request tracker (report bugs here)
L
=item * AnnoCPAN: Annotated CPAN documentation
L
=item * CPAN Ratings
L
=item * Search CPAN
L
=back
=head1 SEE ALSO
L,
L, L
=head1 LICENSE AND COPYRIGHT
Copyright 2018 Michael Samoglyadov.
This program is free software; you can redistribute it and/or modify it under
the terms of either: the GNU General Public License as published by the Free
Software Foundation; or the Artistic License.
See L for more information.
=cut
1; # End of JSON::Patch