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   25029 use Carp qw(croak);
  13         37  
  13         707  
4 13     13   7359 use Const::Fast;
  13         20515  
  13         971  
5 13     13   2552 use Moo;
  13         14733  
  13         77  
6 13     13   6492 use MARC::Spec;
  13         46  
  13         306  
7 13     13   5102 use MARC::Spec::Field;
  13         48  
  13         762  
8             require MARC::Spec::Subfield;
9             require MARC::Spec::Comparisonstring;
10             require MARC::Spec::Subspec;
11 13     13   109 use namespace::clean;
  13         31  
  13         116  
12            
13             our $VERSION = '1.0.0';
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 27952 my ($class, @args) = @_;
49 47 50       233 if (@args % 2 == 1) { unshift @args, "spec" }
  47         146  
50 47         976 return { @args };
51             }
52            
53             sub BUILD {
54 47     47 0 911 my ($self) = @_;
55 47         199 my $field = $self->_match_field();
56 37         601 my $ms = MARC::Spec->new($field);
57 37 100       526 if($self->{_parsed}->{subfields}) {
58 23         114 my $subfields = $self->_match_subfields();
59 22         107 $ms->add_subfields($subfields);
60             }
61 36         790 $self->_set_marcspec($ms);
62             }
63            
64             sub _match_field {
65 47     47   343 my ($self) = @_;
66            
67 47         356 _do_checks($self->spec, $MIN_LENGTH_FIELD);
68            
69 44         461 $self->spec =~ $FIELD;
70            
71 13     13   17528 %{$self->{_parsed}} = %+;
  13         4695  
  13         19275  
  44         631  
  44         460  
72            
73 44 100       254 if(!$self->{_parsed}->{tag}) {
74 2         9 _throw("For fieldtag only '.', digits and lowercase alphabetic or digits and upper case alphabetics characters are allowed.", $self->spec);
75             }
76            
77 42 100       204 if( length $self->{_parsed}->{field} != length $self->spec ) {
78 3         10 _throw('Detected useless data fragment or invalid field spec.', $self->spec);
79             }
80            
81             # create a new Field
82 39         799 my $field = MARC::Spec::Field->new($self->{_parsed}->{tag});
83            
84 39 100       486 if(defined $self->{_parsed}->{indicators}) {
    100          
85 1         5 my $ind1 = substr $self->{_parsed}->{indicators}, 0, 1;
86 1 50       6 if('_' ne $ind1) { $field->indicator1($ind1) }
  1         6  
87            
88 1 50       17 if(2 == length($self->{_parsed}->{indicators})) {
89 1         3 my $ind2 = substr $self->{_parsed}->{indicators}, 1, 1;
90 1 50       4 if('_' ne $ind2) { $field->indicator2($ind2) }
  1         4  
91             }
92             } elsif(defined $self->{_parsed}->{charpos}) {
93 7         51 $field->set_char_start_end($self->{_parsed}->{charpos});
94             }
95            
96 38 100       150 if(defined $self->{_parsed}->{index}) {
97 9         68 $field->set_index_start_end($self->{_parsed}->{index});
98             }
99            
100 38 100 100     817 if(defined $field->char_start && defined $self->{_parsed}->{subfields}) {
101 1         15 _throw("Either characterSpec for field or subfields are allowed.", $self->spec);
102             }
103            
104 37         1074 $self->{field_base} = $field->base;
105            
106 37 100       246 if($self->{_parsed}->{subspecs}) {
107 3         17 my $field_subspecs = $self->_match_subspecs($self->{_parsed}->{subspecs});
108 3         49 $self->_populate_subspecs($field, $field_subspecs, [$self->{field_base}]);
109             }
110 37         135 return $field;
111             }
112            
113             sub _populate_subspecs {
114 33     33   102 my ($self, $spec, $subspecs, $base) = @_;
115 33         55 foreach my $subspec (@{$subspecs}) {
  33         76  
116             # check if array length is above 1
117 62 100       260 if(1 < scalar @{$subspec}) {
  62         156  
118             # alternatives to array (OR)
119 29         61 my @or = ();
120 29         43 foreach my $or_subspec (@{$subspec}) {
  29         55  
121 58         137 push @or, $self->_match_subterms($or_subspec, $base);
122             }
123 29         133 $spec->add_subspecs([\@or]);
124             }
125             else {
126 33         94 $spec->add_subspec( $self->_match_subterms($subspec->[0], $base ) );
127             }
128             }
129             }
130            
131             sub _match_subfields {
132 23     23   69 my ($self) = @_;
133            
134 23         113 _do_checks($self->{_parsed}->{subfields}, $MIN_LENGTH_SUBFIELD);
135            
136 23         61 my $subfields = [];
137 23         53 my $i = 0;
138 23         289 while($self->{_parsed}->{subfields} =~ /$SUBFIELD/g) {
139 26 100       212 if(defined $+{range}) {
140 1         6 my $from = substr $+{range},0,1;
141 1         4 my $to = substr $+{range},2,1;
142 1         6 for my $code ( $from .. $to) {
143 26         49 push @{$subfields}, $self->_create_subfield($code,%+);
  26         187  
144             }
145             } else {
146 25         60 push @{$subfields}, $self->_create_subfield(undef,%+);
  25         277  
147             }
148 26         220 $i++;
149             }
150            
151 23 100       92 if(0 == $i) {
152 1         4 _throw("Invalid subfield spec detected.", $self->{_parsed}->{subfields});
153             }
154            
155 22         61 return $subfields;
156             }
157            
158             sub _create_subfield {
159 51     51   377 my ($self,$code,%args) = @_;
160             # create a new Subfield
161 51   66     939 my $subfield = MARC::Spec::Subfield->new($code // $args{code});
162            
163 51 100       341 if(defined $args{index}) {
164 4         28 $subfield->set_index_start_end($args{index});
165             }
166            
167 51 100       141 if(defined $args{charpos}) {
168 1         9 $subfield->set_char_start_end($args{charpos});
169             }
170            
171             # handle subspecs
172 51 100       141 if(defined $args{subspecs}) {
173 30         103 my $subfield_subspecs = $self->_match_subspecs($args{subspecs});
174 30         467 $self->_populate_subspecs($subfield, $subfield_subspecs, [$self->{field_base}, $subfield->base]);
175             }
176 51         406 return $subfield;
177             }
178            
179             sub _match_subspecs {
180 33     33   73 my ($self, $subspecs) = @_;
181 33         52 my @subspecs;
182            
183 33         237 foreach ($subspecs =~ /$SUBSPEC/g) {
184 62         262 push @subspecs, [split /(?
185             }
186 33         85 return \@subspecs;
187             }
188            
189             sub _match_subterms {
190 91     91   178 my ($self,$subTerms,$context) = @_;
191            
192 91 50       395 if($subTerms =~ $UNESCAPED) {
193 0         0 _throw("Unescaped character detected.", $subTerms);
194             }
195            
196 91 50       1114 if($subTerms !~ /$SUBTERMS/sg) {
197 0         0 _throw("Assuming invalid spec.", $subTerms);
198             }
199            
200             # create a new Subspec
201 91         1288 my $subSpec = MARC::Spec::Subspec->new;
202            
203 91         352 foreach my $side (('left', 'right')) {
204 182 100       1387 if(defined $+{$side}) {
    50          
205 175 100       675 if('\\' ne substr $+{$side},0,1) {
206 91         267 my $spec = _spec_context($+{$side},$context);
207            
208             # this prevents the spec parsed again
209 91 100       258 if($cache{$spec}) {
210 79         1146 $subSpec->$side( $cache{$spec} );
211             } else {
212 12         323 $subSpec->$side( MARC::Spec::Parser->new($spec)->marcspec );
213            
214 12         293 $cache{$spec} = $subSpec->$side;
215             }
216             } else {
217 84         1265 $subSpec->$side( MARC::Spec::Comparisonstring->new(substr $+{$side},1) );
218             }
219             } elsif($side eq 'left') {
220 7         20 my $spec = _spec_context(@{$context}[$#{$context}],$context);
  7         31  
  7         15  
221 7 100       26 if($cache{$spec}) {
222 2         35 $subSpec->left( $cache{$spec} );
223             } else {
224 5         118 $subSpec->left( MARC::Spec::Parser->new($spec)->marcspec );
225 5         153 $cache{$spec} = $subSpec->left;
226             }
227             } else {
228 0         0 _throw("Right hand subTerm is missing.", $subTerms);
229             }
230             }
231            
232 91 100       1005 if(defined $+{operator}) { $subSpec->operator( $+{operator} )}
  84         309  
233            
234 91         338 return $subSpec;
235             }
236            
237             sub _spec_context {
238 98     98   284 my ($spec, $context) = @_;
239 98         169 my $fieldContext = @{$context}[0];
  98         190  
240 98         165 my $fullcontext = join '', @{$context};
  98         209  
241            
242 98 100       243 if($spec eq $fullcontext) { return $spec }
  2         6  
243            
244 96         185 my $firstChar = substr $spec,0,1;
245 96 50       209 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       220 if($firstChar eq '$') { return $fieldContext.$spec }
  9         31  
257            
258 87 50       299 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         213 return $spec;
270             }
271            
272             sub _do_checks {
273 70     70   203 my ($spec, $min_length) = @_;
274            
275 70 100       295 if(ref \$spec ne 'SCALAR') {
276 1         5 _throw("Argument must be of type SCALAR.", ref \$spec);
277             }
278            
279 69 100       315 if($spec =~ /\s/s) {
280 1         5 _throw("Whitespaces are not allowed.", $spec);
281             }
282            
283 68 100       222 if($min_length > length $spec) {
284 1         6 _throw("Spec must be at least ".$min_length." chracters long.", $spec);
285             }
286            
287 67         148 return;
288             }
289            
290             sub _throw {
291 10     10   26 my ($message, $hint) = @_;
292 10         110 croak 'MARCspec Parser exception. '.$message.' Tried to parse: '.$hint;
293             }
294            
295             1;
296             __END__