File Coverage

blib/lib/PICA/Modification.pm
Criterion Covered Total %
statement 13 15 86.6
branch n/a
condition n/a
subroutine 5 5 100.0
pod n/a
total 18 20 90.0


line stmt bran cond sub pod time code
1             package PICA::Modification;
2             {
3             $PICA::Modification::VERSION = '0.16';
4             }
5             #ABSTRACT: Idempotent modification of an identified PICA+ record
6              
7 4     4   77341 use strict;
  4         9  
  4         173  
8 4     4   21 use warnings;
  4         10  
  4         110  
9 4     4   47 use v5.10;
  4         16  
  4         167  
10              
11 4     4   3455 use parent 'Exporter';
  4         1366  
  4         19  
12              
13 4     4   22179 use PICA::Record 0.584;
  0            
  0            
14             use Scalar::Util qw(blessed);
15             use Text::Diff ();
16              
17             our @ATTRIBUTES = qw(id iln epn del add);
18              
19              
20             sub new {
21             my $class = shift;
22             my $attributes = @_ % 2 ? (blessed $_[0] ? $_[0]->attributes : $_[0]) : {@_};
23              
24             no strict 'refs';
25             my $self = bless {
26             map { $_ => $attributes->{$_} } @{ $class.'::ATTRIBUTES' }
27             }, $class;
28              
29             $self->check;
30             }
31              
32              
33             sub attributes {
34             my $self = shift;
35              
36             no strict 'refs';
37             return { map { $_ => $self->{$_} } @{ ref($self).'::ATTRIBUTES' } };
38             }
39              
40              
41             sub error {
42             my $self = shift;
43              
44             return (scalar keys %{$self->{errors}}) unless @_;
45            
46             my $attribute = shift;
47             return $self->{errors}->{$attribute} unless @_;
48              
49             my $message = shift;
50             $self->{errors}->{$attribute} = $message;
51              
52             return $message;
53             }
54              
55              
56             sub check {
57             my $self = shift;
58              
59             $self->{errors} = { };
60              
61             foreach my $attr (@ATTRIBUTES) {
62             my $value = $self->{$attr} // '';
63             $value =~ s/^\s+|\s+$//g;
64             $self->{$attr} = $value;
65             }
66              
67             $self->{ppn} = '';
68             $self->{dbkey} = '';
69             if ($self->{id} =~ /^(([a-z]([a-z0-9-]?[a-z0-9]+))*):ppn:(\d+\d*[Xx]?)$/) {
70             $self->{ppn} = uc($4) if defined $4;
71             $self->{dbkey} = lc($1) if defined $1;
72             } elsif ($self->{id} eq '') {
73             $self->error( id => 'missing record identifier' );
74             } else {
75             $self->error( id => 'malformed record identifier' );
76             }
77              
78             $self->error( iln => "malformed ILN" ) unless $self->{iln} =~ /^\d*$/;
79             $self->error( epn => "malformed EPN" ) unless $self->{epn} =~ /^\d*$/;
80              
81             my %must_delete;
82              
83             if ($self->{add}) {
84             my $pica = eval { PICA::Record->new( $self->{add} ) };
85             if ($pica) {
86             $self->error( iln => 'missing ILN for add' )
87             if !$self->{iln} and $pica->field(qr/^1/);
88             $self->error( epn => 'missing EPN for add' )
89             if !$self->{epn} and $pica->field(qr/^2/);
90             $pica->sort;
91             foreach ($pica->fields) {
92             my $tag = $_->tag;
93             # TODO: remove occurrence from level 2 tags
94             $must_delete{$tag} = 1;
95             }
96             $self->{add} = "$pica";
97             chomp $self->{add};
98             } else {
99             $self->error( add => "malformed fields to add" );
100             }
101             }
102              
103             my @del = grep { $_ !~ /^\s*$/ } split(/\s*,\s*/, $self->{del});
104              
105             $self->error( del => 'malformed fields to remove' )
106             if grep { $_ !~ qr{^[012]\d\d[A-Z@](/\d\d)?$} } @del;
107              
108             $self->error( epn => 'missing EPN for remove' )
109             if !$self->{epn} and grep { /^2/ } @del;
110              
111             $self->error( iln => 'missing ILN for remove' )
112             if !$self->{iln} and grep { /^1/ } @del;
113              
114             delete $must_delete{$_} for @del;
115             if (%must_delete) {
116             $self->error( del => 'fields to add must also be deleted' );
117             }
118              
119             $self->{del} = join (',', sort @del);
120              
121             if (!$self->{add} and !$self->{del} and !$self->error('del')) {
122             $self->error( del => 'edit must not be empty' );
123             }
124              
125             if ( !$self->error('del') ) {
126             my @bad = grep { /^(003@|101@|203@)/; } @del;
127             if (@bad) {
128             $self->error( del => 'must not modify field: '.join(', ',@bad) );
129             }
130             }
131              
132             return $self;
133             }
134              
135              
136             sub apply {
137             my ($self, $pica, %args) = @_;
138              
139             return if $self->error;
140              
141             if (!$pica) {
142             $self->error( id => 'record not found' );
143             return;
144             }
145             if ( defined $pica->ppn and $pica->ppn ne $self->{ppn} ) {
146             $self->error( id => 'PPN does not match' );
147             return;
148             }
149              
150             my $add = PICA::Record->new( $self->{add} || '' );
151             my $del = [ split ',', $self->{del} ];
152              
153             my @level0 = grep /^0/, @$del;
154             my @level1 = grep /^1/, @$del;
155             my @level2 = grep /^2/, @$del;
156              
157             my $iln = $self->{iln};
158             my $epn = $self->{epn};
159              
160             # Level 0
161             my $result = $pica->main;
162             $result->remove( @level0 ) if @level0;
163             $result->append( $add->main );
164              
165             # Level 1
166             if (@level1 and !$pica->holdings($iln)) {
167             $self->error('iln', 'ILN not found');
168             return;
169             }
170              
171             foreach my $h ( $pica->holdings ) {
172             if ($iln and $iln eq ($h->iln // '')) {
173             @level1 = map { $_ =~ qr{/} ? $_ : ($_,"$_/..") } @level1;
174             $h->remove( @level1 );
175             $h->append( $add->field(qr/^1/) );
176             }
177             $result->append( $h->fields );
178              
179             # TODO: Level 2
180             }
181            
182             $result->sort;
183              
184             return $result;
185             }
186              
187              
188              
189             sub diff {
190             my ($self, $record, $context) = @_;
191              
192             my $result = $self->apply( $record ) or return;
193              
194             $context //= (scalar $record->fields + scalar $result->fields);
195            
196             my $diff = Text::Diff::diff(
197             \($record->string),
198             \($result->string),
199             {CONTEXT => $context}
200             );
201              
202             $diff =~ s/^@.*$ \n//xgm;
203              
204             return $diff;
205             }
206              
207             1;
208              
209              
210              
211              
212             __END__