File Coverage

blib/lib/MARC/Spec/Parser.pm
Criterion Covered Total %
statement 149 162 91.9
branch 57 74 77.0
condition 5 6 83.3
subroutine 18 18 100.0
pod 0 2 0.0
total 229 262 87.4


line stmt bran cond sub pod time code
1             package MARC::Spec::Parser;
2            
3 13     13   13489 use Carp qw(croak);
  13         13  
  13         518  
4 13     13   5479 use Const::Fast;
  13         14540  
  13         848  
5 13     13   1716 use Moo;
  13         8019  
  13         739  
6 13     13   3597 use MARC::Spec;
  13         17  
  13         222  
7 13     13   4107 use MARC::Spec::Field;
  13         26  
  13         607  
8             require MARC::Spec::Subfield;
9             require MARC::Spec::Comparisonstring;
10             require MARC::Spec::Subspec;
11 13     13   70 use namespace::clean;
  13         15  
  13         68  
12            
13             our $VERSION = '0.1.4';
14            
15             has spec => (
16             is => 'rw',
17             required => 1
18             );
19            
20             has marcspec => (
21             is => 'rwp'
22             );
23            
24             const my $FIELDTAG => q{^(?(?:[a-z0-9\.]{3,3}|[A-Z0-9\.]{3,3}|[0-9\.]{3,3}))?};
25             const my $POSITIONORRANGE => q{(?:(?:(?:[0-9]+|#)\-(?:[0-9]+|#))|(?:[0-9]+|#))};
26             const my $INDEX => qq{(?:\\[(?$POSITIONORRANGE)\\])?};
27             const my $CHARPOS => qq{\\/(?$POSITIONORRANGE)};
28             const my $INDICATORS => q{_(?(?:[_a-z0-9][_a-z0-9]{0,1}))};
29             const my $SUBSPECS => q{(?(?:\\{.+?(?
30             const my $SUBFIELDS => q{(?\$.+)?};
31             const my $FIELD => qr/(?(?:$FIELDTAG$INDEX(?:$CHARPOS|$INDICATORS)?$SUBSPECS$SUBFIELDS))/s;
32             const my $SUBFIELDRANGE => q{(?(?:[0-9a-z]\-[0-9a-z]))};
33             const my $SUBFIELDTAG => q{(?[\!-\?\[-\\{\\}-~])};
34             const my $SUBFIELD => qr/(?\$(?:$SUBFIELDRANGE|$SUBFIELDTAG)$INDEX(?:$CHARPOS)?$SUBSPECS)/s;
35             const my $LEFTSUBTERM => q{^(?(?:\\\(?:(?<=\\\)[\!\=\~\?]|[^\!\=\~\?])+)|(?:(?<=\$)[\!\=\~\?]|[^\!\=\~\?])+)?};
36             const my $OPERATOR => q{(?\!\=|\!\~|\=|\~|\!|\?)};
37             const my $SUBTERMS => qq{(?:$LEFTSUBTERM$OPERATOR)?(?.+)}.q{$};
38             const my $SUBSPEC => qr/(?:\{(.+?)\})/s;
39             const my $UNESCAPED => qr/(?
40            
41             const my $MIN_LENGTH_FIELD => 3;
42             const my $MIN_LENGTH_SUBFIELD => 2;
43             const my $NO_LENGTH => -1;
44            
45             my %cache;
46            
47             sub BUILDARGS {
48 47     47 0 17004 my ($class, @args) = @_;
49 47 50       120 if (@args % 2 == 1) { unshift @args, "spec" }
  47         75  
50 47         637 return { @args };
51             }
52            
53             sub BUILD {
54 47     47 0 455 my ($self) = @_;
55 47         88 my $field = $self->_match_field();
56 37         418 my $ms = MARC::Spec->new($field);
57 37 100       279 if($self->{_parsed}->{subfields}) {
58 23         58 my $subfields = $self->_match_subfields();
59 22         60 $ms->add_subfields($subfields);
60             }
61 36         456 $self->_set_marcspec($ms);
62             }
63            
64             sub _match_field {
65 47     47   45 my ($self) = @_;
66            
67 47         188 _do_checks($self->spec, $MIN_LENGTH_FIELD);
68            
69 44         292 $self->spec =~ $FIELD;
70            
71 13     13   13722 %{$self->{_parsed}} = %+;
  13         3761  
  13         16392  
  44         405  
  44         293  
72            
73 44 100       146 if(!$self->{_parsed}->{tag}) {
74 2         5 _throw("For fieldtag only '.', digits and lowercase alphabetic or digits and upper case alphabetics characters are allowed.", $self->spec);
75             }
76            
77 42 100       113 if( length $self->{_parsed}->{field} != length $self->spec ) {
78 3         6 _throw('Detected useless data fragment or invalid field spec.', $self->spec);
79             }
80            
81             # create a new Field
82 39         502 my $field = MARC::Spec::Field->new($self->{_parsed}->{tag});
83            
84 39 100       298 if(defined $self->{_parsed}->{indicators}) {
    100          
85 1         3 my $ind1 = substr $self->{_parsed}->{indicators}, 0, 1;
86 1 50       14 if('_' ne $ind1) { $field->indicator1($ind1) }
  1         5  
87            
88 1 50       11 if(2 == length($self->{_parsed}->{indicators})) {
89 1         3 my $ind2 = substr $self->{_parsed}->{indicators}, 1, 1;
90 1 50       3 if('_' ne $ind2) { $field->indicator2($ind2) }
  1         2  
91             }
92             } elsif(defined $self->{_parsed}->{charpos}) {
93 7         30 $field->set_char_start_end($self->{_parsed}->{charpos});
94             }
95            
96 38 100       76 if(defined $self->{_parsed}->{index}) {
97 9         47 $field->set_index_start_end($self->{_parsed}->{index});
98             }
99            
100 38 100 100     528 if(defined $field->char_start && defined $self->{_parsed}->{subfields}) {
101 1         10 _throw("Either characterSpec for field or subfields are allowed.", $self->spec);
102             }
103            
104 37         714 $self->{field_base} = $field->base;
105            
106 37 100       123 if($self->{_parsed}->{subspecs}) {
107 3         12 my $field_subspecs = $self->_match_subspecs($self->{_parsed}->{subspecs});
108 3         76 $self->_populate_subspecs($field, $field_subspecs, [$self->{field_base}]);
109             }
110 37         61 return $field;
111             }
112            
113             sub _populate_subspecs {
114 33     33   44 my ($self, $spec, $subspecs, $base) = @_;
115 33         27 foreach my $subspec (@{$subspecs}) {
  33         43  
116             # check if array length is above 1
117 62 100       160 if(1 < scalar @{$subspec}) {
  62         104  
118             # alternatives to array (OR)
119 29         27 my @or = ();
120 29         19 foreach my $or_subspec (@{$subspec}) {
  29         33  
121 58         87 push @or, $self->_match_subterms($or_subspec, $base);
122             }
123 29         110 $spec->add_subspecs([\@or]);
124             }
125             else {
126 33         59 $spec->add_subspec( $self->_match_subterms($subspec->[0], $base ) );
127             }
128             }
129             }
130            
131             sub _match_subfields {
132 23     23   28 my ($self) = @_;
133            
134 23         54 _do_checks($self->{_parsed}->{subfields}, $MIN_LENGTH_SUBFIELD);
135            
136 23         35 my $subfields = [];
137 23         22 my $i = 0;
138 23         170 while($self->{_parsed}->{subfields} =~ /$SUBFIELD/g) {
139 26 100       120 if(defined $+{range}) {
140 1         4 my $from = substr $+{range},0,1;
141 1         4 my $to = substr $+{range},2,1;
142 1         3 for my $code ( $from .. $to) {
143 26         18 push @{$subfields}, $self->_create_subfield($code,%+);
  26         180  
144             }
145             } else {
146 25         21 push @{$subfields}, $self->_create_subfield(undef,%+);
  25         163  
147             }
148 26         126 $i++;
149             }
150            
151 23 100       45 if(0 == $i) {
152 1         2 _throw("Invalid subfield spec detected.", $self->{_parsed}->{subfields});
153             }
154            
155 22         33 return $subfields;
156             }
157            
158             sub _create_subfield {
159 51     51   247 my ($self,$code,%args) = @_;
160             # create a new Subfield
161 51   66     798 my $subfield = MARC::Spec::Subfield->new($code // $args{code});
162            
163 51 100       221 if(defined $args{index}) {
164 4         20 $subfield->set_index_start_end($args{index});
165             }
166            
167 51 100       79 if(defined $args{charpos}) {
168 1         11 $subfield->set_char_start_end($args{charpos});
169             }
170            
171             # handle subspecs
172 51 100       84 if(defined $args{subspecs}) {
173 30         55 my $subfield_subspecs = $self->_match_subspecs($args{subspecs});
174 30         427 $self->_populate_subspecs($subfield, $subfield_subspecs, [$self->{field_base}, $subfield->base]);
175             }
176 51         291 return $subfield;
177             }
178            
179             sub _match_subspecs {
180 33     33   40 my ($self, $subspecs) = @_;
181 33         33 my @subspecs;
182            
183 33         191 foreach ($subspecs =~ /$SUBSPEC/g) {
184 62         202 push @subspecs, [split /(?
185             }
186 33         49 return \@subspecs;
187             }
188            
189             sub _match_subterms {
190 91     91   100 my ($self,$subTerms,$context) = @_;
191            
192 91 50       322 if($subTerms =~ $UNESCAPED) {
193 0         0 _throw("Unescaped character detected.", $subTerms);
194             }
195            
196 91 50       916 if($subTerms !~ /$SUBTERMS/sg) {
197 0         0 _throw("Assuming invalid spec.", $subTerms);
198             }
199            
200             # create a new Subspec
201 91         1228 my $subSpec = MARC::Spec::Subspec->new;
202            
203 91         250 foreach my $side (('left', 'right')) {
204 182 100       1036 if(defined $+{$side}) {
    50          
205 175 100       462 if('\\' ne substr $+{$side},0,1) {
206 91         183 my $spec = _spec_context($+{$side},$context);
207            
208             # this prevents the spec parsed again
209 91 100       191 if($cache{$spec}) {
210 79         1152 $subSpec->$side( $cache{$spec} );
211             } else {
212 12         180 $subSpec->$side( MARC::Spec::Parser->new($spec)->marcspec );
213            
214 12         228 $cache{$spec} = $subSpec->$side;
215             }
216             } else {
217 84         1173 $subSpec->$side( MARC::Spec::Comparisonstring->new(substr $+{$side},1) );
218             }
219             } elsif($side eq 'left') {
220 7         9 my $spec = _spec_context(@{$context}[$#{$context}],$context);
  7         17  
  7         6  
221 7 100       29 if($cache{$spec}) {
222 2         27 $subSpec->left( $cache{$spec} );
223             } else {
224 5         84 $subSpec->left( MARC::Spec::Parser->new($spec)->marcspec );
225 5         96 $cache{$spec} = $subSpec->left;
226             }
227             } else {
228 0         0 _throw("Right hand subTerm is missing.", $subTerms);
229             }
230             }
231            
232 91 100       789 if(defined $+{operator}) { $subSpec->operator( $+{operator} )}
  84         238  
233            
234 91         266 return $subSpec;
235             }
236            
237             sub _spec_context {
238 98     98   197 my ($spec, $context) = @_;
239 98         82 my $fieldContext = @{$context}[0];
  98         119  
240 98         75 my $fullcontext = join '', @{$context};
  98         130  
241            
242 98 100       161 if($spec eq $fullcontext) { return $spec }
  2         4  
243            
244 96         103 my $firstChar = substr $spec,0,1;
245 96 50       125 if($firstChar eq '_') {
246 0         0 my $refPos = index $fullcontext, $firstChar;
247            
248 0 0       0 if(0 <= $refPos) {
249 0 0       0 if('$' ne substr $fullcontext,$refPos - 1,1) {
250 0         0 return substr($fullcontext,0,$refPos).$spec;
251             }
252             }
253 0         0 return $fullcontext.$spec;
254             }
255            
256 96 100       132 if($firstChar eq '$') { return $fieldContext.$spec }
  9         17  
257            
258 87 50       228 if($firstChar =~ /\[|\//) {
259 0         0 my $refPos = rindex $fullcontext, $firstChar;
260            
261 0 0       0 if(0 <= $refPos) {
262 0 0       0 if('$' ne substr $fullcontext,$refPos - 1,1) {
263 0         0 return substr($fullcontext,0,$refPos).$spec;
264             }
265             }
266 0         0 return $fullcontext.$spec;
267             }
268            
269 87         117 return $spec;
270             }
271            
272             sub _do_checks {
273 70     70   78 my ($spec, $min_length) = @_;
274            
275 70 100       160 if(ref \$spec ne 'SCALAR') {
276 1         3 _throw("Argument must be of type SCALAR.", ref \$spec);
277             }
278            
279 69 100       179 if($spec =~ /\s/s) {
280 1         2 _throw("Whitespaces are not allowed.", $spec);
281             }
282            
283 68 100       116 if($min_length > length $spec) {
284 1         6 _throw("Spec must be at least ".$min_length." chracters long.", $spec);
285             }
286            
287 67         66 return;
288             }
289            
290             sub _throw {
291 10     10   14 my ($message, $hint) = @_;
292 10         87 croak 'MARCspec Parser exception. '.$message.' Tried to parse: '.$hint;
293             }
294            
295             1;
296             __END__