File Coverage

blib/lib/Business/EDI/Spec.pm
Criterion Covered Total %
statement 234 260 90.0
branch 122 202 60.4
condition 38 72 52.7
subroutine 26 29 89.6
pod 1 21 4.7
total 421 584 72.0


line stmt bran cond sub pod time code
1             package Business::EDI::Spec;
2              
3 15     15   88 use base qw/Business::EDI/; # inherits AUTOLOAD for $self->{_permitted} keys
  15         27  
  15         1210  
4              
5 15     15   81 use strict;
  15         29  
  15         438  
6 15     15   83 use warnings;
  15         25  
  15         388  
7 15     15   108 use Carp;
  15         28  
  15         2719  
8              
9             our $VERSION = 0.02;
10              
11 15     15   89 use UNIVERSAL::require;
  15         35  
  15         114  
12 15     15   638 use Data::Dumper;
  15         34  
  15         787  
13 15     15   42462 use File::Find::Rule;
  15         208774  
  15         175  
14 15     15   1214 use File::Spec;
  15         36  
  15         142  
15             our $debug = 0;
16              
17             our $spec_dir; # for the whole class
18             our $syntax_dir; # for the whole class
19             our $spec_map = {
20             message => {code => 'DMD', cache => {}, keys => [qw/code mandatory repeats /]},
21             segment_group => {code => 'DMD', cache => {}, keys => [qw/code mandatory repeats /]},
22             segment => {code => 'DSD', cache => {}, keys => [qw/pos code mandatory repeats/]},
23             composite => {code => 'DCD', cache => {}, keys => [qw/pos code class def /]},
24             # codelist => {code => 'DCL', cache => {}, keys => []},
25             element => {code => 'DED', cache => {}, },
26             };
27              
28             my %fields = (
29             edi_flavor => 'edifact', # someday could be x12 or something...
30             spec_files => undef,
31             version_default => 'd08a',
32             version => undef,
33             syntax_files => undef,
34             syntax_default => '40100',
35             syntax => undef,
36             interactive => 0,
37             );
38              
39             # Constructors
40              
41             sub new {
42 122     122 1 263 my $class = shift;
43 122         220 my %args;
44 122 100       513 if (scalar(@_) == 1) {
45 67         226 $args{version} = shift;
46             } else {
47 55 50       179 scalar(@_) % 2 and croak "Odd number of arguments to new() incorrect. Use (name1 => value1) style.";
48 55         186 %args = @_;
49             }
50 122         618 my $stuff = {_permitted => {(map {$_ => 1} keys %fields)}, %fields};
  976         3396  
51 122         590 foreach (keys %args) {
52 122 50       538 $_ eq 'version' and next; # special case, probably can remove
53 0 0       0 $_ eq 'syntax' and next; # special case, probably can remove
54 0 0       0 exists ($stuff->{_permitted}->{$_}) or croak "Unrecognized argument to new: $_ => $args{$_}";
55             }
56 122         382 my $self = bless($stuff, $class);
57 122   0     681 my $version = lc($args{version} || $self->version || $fields{version} || $self->version_default || $fields{version_default});
58 122   33     1531 my $syntax = lc($args{syntax } || $self->syntax || $fields{syntax } || $self->syntax_default || $fields{ syntax_default});
59 122 100 33     730 $version eq 'default' and $version = $self->version_default || $fields{version_default};
60 122 50 0     402 $syntax eq 'default' and $syntax = $self->syntax_default || $fields{ syntax_default};
61 122 50       381 $debug and warn "### Setting syntax/version $syntax/$version";
62 122 50       578 $self->set_syntax_version($syntax) or croak "Unrecognized spec syntax '$syntax'";
63 122 50       622 $self->set_spec_version( $version) or croak "Unrecognized spec version '$version'";
64 122 50 33     774 $debug and $debug > 1 and print Dumper($self);
65 122         1395 return $self;
66             }
67              
68             # We have to deal with two parallel kinds of CSV definitions: the spec version and the EDIFACT syntax
69             # So we have pairs of methods.
70              
71             sub get_spec_dir {
72 194     194 0 328 my $self = shift;
73 194 50       815 $self->{spec_dir} and return $self->{spec_dir};
74 194 100       1327 $spec_dir and return $spec_dir;
75 12         36 my $target = 'Business/EDI/data/edifact/untdid'; # path relative to @INC. Don't worry about filesystem oddities (see split below)
76 12         65 my @dirs = grep {-d} @INC; # skip non-existant dirs, as in Debian default @INC (-d implies -e)
  132         3101  
77 12         75 foreach (split /\//, $target) {
78 60         259366 @dirs = File::Find::Rule->maxdepth(1)->name($_)->directory()->in(@dirs);
79             }
80             # we use serial Find's so we don't have to care about OS/filesystem variations. And we only do it once, typcially.
81 12 50       18773 $debug and print STDERR "# get_spec_dir() found ", scalar(@dirs), " $target dirs:\n# ", join("\n# ", @dirs), "\n";
82 12 50       65 unless (@dirs) {
83 0         0 warn "Could not locate specifications directory ($target) in \@INC";
84 0         0 return;
85             }
86 12         99 return $spec_dir = $dirs[0];
87             }
88             sub get_syntax_dir {
89 198     198 0 364 my $self = shift;
90 198 50       763 $self->{syntax_dir} and return $self->{syntax_dir};
91 198 100       1190 $syntax_dir and return $syntax_dir;
92 12         43 my $target = 'Business/EDI/data/edifact/iso9735'; # path relative to @INC. Don't worry about filesystem oddities (see split below)
93 12         387 my @dirs = @INC;
94 12         86 foreach (split /\//, $target) {
95 60         330194 @dirs = File::Find::Rule->maxdepth(1)->name($_)->directory()->in(@dirs);
96             }
97             # we use serial Find's so we don't have to care about OS/filesystem variations. And we only do it once, typcially.
98 12 50       19472 $debug and print STDERR "# get_syntax_dir() found ", scalar(@dirs), " $target dirs:\n# ", join("\n# ", @dirs), "\n";
99 12 50       63 unless (@dirs) {
100 0         0 warn "Could not locate specifications directory ($target) in \@INC";
101 0         0 return;
102             }
103 12         111 return $syntax_dir = $dirs[0];
104             }
105              
106             sub set_spec_version {
107 122     122 0 261 my $self = shift;
108 122 50       488 my $code = shift or return $self->carp_error("set_spec_version: required version spec code argument not provided");
109 122         575 my @files = $self->find_spec_files($code);
110 122 50       1244298 unless (@files) {
111 0         0 $self->error("set_spec_version: Unrecognized spec code '$code' (no csv files)");
112 0         0 return;
113             }
114 122         557 $self->{spec_files} = \@files;
115 122         841 return $self->{version} = $code;
116             }
117             sub set_syntax_version {
118 122     122 0 197 my $self = shift;
119 122 50       411 my $code = shift or return $self->carp_error("set_syntax_version: required syntax code argument not provided");
120 122         560 my @files = $self->find_syntax_files($code);
121 122 50       15268470 unless (@files) {
122 0         0 $self->error("set_syntax_version: Unrecognized syntax code '$code' (no csv files)");
123 0         0 return;
124             }
125 122         408 $self->{syntax_files} = \@files;
126 122         676 return $self->{syntax} = $code;
127             }
128              
129             sub find_spec_files {
130 194     194 0 454 my $self = shift;
131 194 100 33     963 my $code = @_ ? shift : ($self->version || $self->version_default);
132 194 50       583 $code or return $self->carp_error("No EDI spec revision argument to find_spec_files(). Nothing to look for!");
133 194 50       772 my $dir = $self->get_spec_dir or return $self->carp_error("EDI Specifications directory missing");
134 194 50       768 $debug and warn "get_spec_dir returned '$dir'. Looking for $dir/*.$code.csv";
135 194         1852 return File::Find::Rule->maxdepth(1)->name("*.$code.csv")->file()->in($dir);
136             }
137             sub find_syntax_files {
138 198     198 0 373 my $self = shift;
139 198 100 33     997 my $code = @_ ? shift : ($self->syntax || $self->syntax_default);
140 198 50       639 $code or return $self->carp_error("No EDI spec revision argument to find_syntax_files(). Nothing to look for!");
141 198 50       826 my $dir = $self->get_syntax_dir or return $self->carp_error("EDI Syntax directory missing");
142 198 50       789 $debug and warn "get_syntax_dir returned '$dir'. Looking for $dir/*.$code.csv";
143 198         12470 return File::Find::Rule->maxdepth(1)->name("*.$code.csv")->file()->in($dir);
144             }
145              
146             sub get_spec_handle {
147 72     72 0 133 my $self = shift;
148 72   50     244 my $type = shift || '';
149 72 50       524 my $version = @_ ? shift : $self->version;
150 72 50       218 $version or return $self->carp_error("spec version is not set (nor passed as a parameter)");
151 72         94 my $trio;
152 72 50 33     463 unless ($type and $trio = $spec_map->{$type}) {
153 0         0 return $self->carp_error("Type '$type' is not mapped to a spec file. Options are: " . join(' ', keys %$spec_map));
154             }
155 72         2547 my @files = $self->find_spec_files;
156 72         472315 my $name = $self->csv_filename($trio->{code}, $version);
157 72 50       293 $debug and print STDERR "get_spec_handle() checking " . scalar(@files) . " files for: $name\n";
158 72         224 my @hits = grep {(File::Spec->splitpath($_))[2] eq $name} @files;
  429         4816  
159 72 50       306 scalar(@hits) or return $self->carp_error("Spec file for $type ($name) not found");
160 72         174 my $file = $hits[0];
161 72 50       306 $debug and warn "get_spec_handle opening $file";
162 72 50       5541 open (my $fh, "<$file") or carp "get_spec_handle failed to open $file";
163 72         622 return $fh;
164             }
165             sub get_syntax_handle {
166 76     76 0 157 my $self = shift;
167 76   50     321 my $type = shift || '';
168 76 50       1483 my $syntax = @_ ? shift : $self->syntax;
169 76 50       368 $syntax or return $self->carp_error("spec syntax is not set (nor passed as a parameter)");
170 76         157 my $trio;
171 76 50 33     990 unless ($type and $trio = $spec_map->{$type}) {
172 0         0 return $self->carp_error("Type '$type' is not mapped to a syntax file. Options are: " . join(' ', keys %$spec_map));
173             }
174 76         399 my @files = $self->find_syntax_files;
175 76         200899 my $name = $self->csv_filename('S', $trio->{code}, $syntax);
176 76 50       458 $debug and print STDERR "get_syntax_handle() checking " . scalar(@files) . " files for: $name\n";
177 76         204 my @hits = grep {(File::Spec->splitpath($_))[2] eq $name} @files;
  304         18241  
178 76 50       319 scalar(@hits) or return $self->carp_error("Syntax file for $type ($name) not found");
179 76         179 my $file = $hits[0];
180 76 50       276 $debug and warn "get_syntax_handle opening $file";
181 76 50       5055 open (my $fh, "<$file") or carp "get_syntax_handle failed to open $file";
182 76         670 return $fh;
183             }
184              
185             sub csv_filename {
186 148     148 0 501 my $self = shift;
187 148 50 50     1993 return (scalar(@_) > 2 ? shift : $self->interactive ? 'I' : 'E')
    100 50        
188             . (shift || '') . '.' . (shift || '') . ".csv";
189             }
190              
191             # alias for get_spec
192             sub spec_page {
193 6232     6232 0 965925 my $self = shift;
194 6232         16963 return $self->get_spec(@_);
195             }
196              
197             # gets a page of the already declared spec, like say the one defining message(s)
198             sub get_spec {
199 12792     12792 0 16736 my $self = shift;
200 12792 50       30197 my $type = shift or return $self->carp_error("get_spec: required argument for spec 'type' missing. Options are: " . join(', ', keys %$spec_map));
201 12792 50       34053 $spec_map->{$type} or return $self->carp_error("Type '$type' is not mapped to a spec file. Options are: " . join(' ', keys %$spec_map));
202 12792 100       25949 my $subpart = @_ ? shift : '';
203 12792 50       67200 my $version = $self->version or return $self->carp_error("spec version is not set");
204 12792 50       62722 my $syntax = $self->syntax or return $self->carp_error("spec syntax is not set");
205 12792 100       56911 if ($spec_map->{$type}->{cache}->{$version}) {
    100          
206 12716 50       45686 $debug and print STDERR "cache hit for spec_map->{$type}->{cache}->{$version}\n";
207             } elsif ($type eq 'segment_group') {
208 8         79 my $message_spec = $self->get_spec('message', $subpart); # segment groups are defined in the message file
209 8         5641 foreach (keys %$message_spec) {
210 22552 100       83349 /^(\S+)\/(SG\d+)$/ or next; # like ORDRSP/SG27
211 21256         82845 $spec_map->{$type}->{cache}->{$version}->{$1}->{$2} = $message_spec->{$_};
212             }
213 8         3753 my $message_syntax = $self->get_syntax('message'); # segment groups are defined in the message file
214 8         79 foreach (keys %$message_syntax) {
215 136 100       526 /^(\S+)\/(SG\d+)$/ or next; # like ORDRSP/SG27
216 112         721 $spec_map->{$type}->{cache}->{$version}->{$1}->{$2} = $message_syntax->{$_}; # combine syntax/spec defs
217             }
218             } else {
219 68 50       324 my $fh = $self->get_spec_handle($type) or return;
220 68         396 $spec_map->{$type}->{cache}->{$version} = $self->parse_plexer($type, $fh);
221 68         46051 my $extras = $self->get_syntax($type);
222 68         726 foreach (keys %$extras) {
223 1610         5719 $spec_map->{$type}->{cache}->{$version}->{$_} = $extras->{$_}; # combine syntax/spec defs
224             }
225             }
226 12792         175852 return $spec_map->{$type}->{cache}->{$version};
227             }
228              
229             sub get_syntax { # no (separate from spec) cache
230 76     76 0 2924 my $self = shift;
231 76 50       411 my $type = shift or return $self->carp_error("get_syntax: required argument for syntax 'type' missing. Options are: " . join(', ', keys %$spec_map));
232 76 50       799 $spec_map->{$type} or return $self->carp_error("Type '$type' is not mapped to a spec file. Options are: " . join(' ', keys %$spec_map));
233 76 50       302 $type = 'message' if ($type eq 'segment_group'); # sort out the messages vs. segments yourself
234 76 50       675 my $fh = $self->get_syntax_handle($type) or return;
235 76         453 return $self->parse_plexer($type, $fh);
236             }
237              
238             # returns pseudohash
239             sub parse_plexer {
240 144     144 0 331 my $self = shift;
241 144 50       478 my $type = shift or croak("parse_plexer: required argument for spec 'type' missing. Options are: " . join(', ', keys %$spec_map));
242 144 50       498 my $fh = shift or croak("parse_plexer: required argument for 'filehandle' missing");
243 144         2021446 my @slurp = <$fh>;
244 144         31194 chomp @slurp;
245 144 50       612 $debug and print STDERR "parsing CSV for $type: $. lines\n";
246 144 100 100     3787 if ($type eq 'element') {
    50 66        
      33        
247             # 1000;an..35;B;Document name
248             return {
249 778         5404 map {
250 2         6 s/\s*$//; # kill trailing spaces
251 778         2231 my @four = split ';', $_;
252 778         4379 $four[0] => {
253             code => $four[0],
254             def => $four[1],
255             class => $four[2],
256             label => $four[3]
257             }
258             } @slurp
259             };
260             } elsif ($type eq 'composite' or $type eq 'message' or $type eq 'segment' or $type eq 'segment_group') {
261             return {
262 122551         14014311 map {
263 142         1035 my ($code, $label, @rest) = split ';', $_;
264 122551         666765 my @codeparts = split ':', $code;
265 122551         233996 my $xpath = $code = $codeparts[0];
266 122551 100 66     919795 if ($codeparts[-1] and $codeparts[-1] ne $code and $codeparts[-1] ne 'UN') {
      100        
267 111501         236495 $xpath .= "/" . $codeparts[-1];
268             }
269 122551 50 33     284649 $debug and $debug > 1 and print STDERR "parsing CSV for $type/$xpath ($label)\n";
270 122551         440639 $xpath => {
271             xpath => $xpath, # ORDERS/SG02 -- xpath is same as code except for segment_groups
272             code => $code, # SG02
273             # version => $codeparts[1] . $codeparts[2],
274             label => $label,
275             parts => $self->parse_array(\@rest, $spec_map->{$type}->{keys})
276             }
277             } @slurp
278             };
279             } else {
280 0         0 croak "Cannot parse CSV for unknown type '$type'";
281             }
282             }
283              
284             # my $foobar = parse_array(\@elements, @keys)
285             sub parse_array {
286 122551     122551 0 187773 my $self = shift;
287 122551 50       306728 @_ >= 2 or croak "\$self->parse_array needs two array_ref arguments";
288 122551         139017 my @parts = @{(shift)}; # extra parens req'd
  122551         3500664  
289 122551         184342 my @keys = @{(shift)}; # extra parens req'd
  122551         319460  
290              
291 122551 50       280223 @keys or croak "No keys passed to parse_array. Cannot interpret spec line";
292 122551 50       288304 (scalar(@parts) % scalar(@keys)) and croak sprintf "Cannot parse %s elements evenly into parts of %s for body: %s", scalar(@parts), scalar(@keys), join(';', @parts);
293              
294 122551         156919 my @return;
295 122551         143614 my $i = 0;
296 122551         277604 while (@parts) {
297 524108         5841626 my %chunk = (index => $i++);
298 524108         848785 foreach (@keys) {
299 1588992         2202788 my $value = shift @parts;
300 1588992 100       3230649 if ($_ eq 'mandatory') {
301 515180 100       1214059 next unless $value eq 'M'; # conditional is assumed
302 161440         255949 $value = 1; # M => 1
303             }
304 1235252         3101413 $chunk{$_} = $value;
305             }
306 524108         3637112 push @return, \%chunk;
307             }
308 122551         7988103 return \@return;
309             }
310              
311              
312             # Cache Control
313              
314             sub dump_cache {
315 0     0 0 0 return Dumper($spec_map);
316             }
317             sub clear_cache {
318 0     0 0 0 foreach (keys %$spec_map) {
319 0         0 $spec_map->{$_}->{cache} = {};
320             }
321             }
322              
323              
324             # Specialized sort functions
325              
326             sub spec_version_sort {
327 4470     4470 0 13803 my $whatever = shift;
328 4470         7574 my ($a, $b) = @_;
329 4470         3938 my ($a_num, $b_num);
330 4470 100       12913 if ($a =~ /^.((\d)\d).$/) {
331 4469         6560 $a_num = $1;
332 4469 100       10131 $a_num += 100 if $2 != 9; # 2-digit year like 06 (or 12) has to sort as greater than 97
333 4469 100       11896 if ($b =~ /^.((\d)\d).$/) {
334 3988         5218 $b_num = $1;
335 3988 100       8506 $b_num += 100 if $2 != 9; # 2-digit year like 06 (or 12) has to sort as greater than 97
336 3988   66     17718 return $a_num <=> $b_num || $a cmp $b;
337             }
338             }
339 482         1221 return $a cmp $b;
340             }
341              
342             sub sg_sort {
343 18402     18402 0 834461 my $whatever = shift;
344 18402         35360 my ($a, $b) = @_;
345 18402         19251 my ($a_part, $a_num);
346 18402 100       71629 if ($a =~ /^(.+)\/SG(\d+)$/) {
347 18319         28888 $a_part = $1;
348 18319         24937 $a_num = $2;
349 18319 100       73528 if ($b =~ /^(.+)\/SG(\d+)$/) {
    50          
350 18317   66     123762 return $a_part cmp $1 || $a_num <=> $2;
351             } elsif ($b =~ /SG(\d+)$/) {
352 2         9 return $a_num <=> $1;
353             }
354             }
355 83 100 66     440 if ($a =~ /SG(\d+)$/ and
      100        
356             $a_num = $1 and
357             $b =~ /SG(\d+)$/ ) {
358 33         115 return $a_num <=> $1;
359             }
360 50         130 return $a cmp $b;
361             }
362              
363              
364             # Meta-mapping
365             # This will need to be updated (or at least reviewed) with each new spec CSV file added
366             #
367             # Key pseudo-ranges should NOT overlap
368             # Many of these have ancillary counterparts as subunits of other SGs,
369             # but this mapping is for the TOP level SGs that apply to the whole message.
370             #
371             # Note: data representation might be slimmed by specifying just:
372             # first version where introduced, first value and a list of versions where SG is incremented
373             # or
374             # different kind of list of new SGs per version, increments needed calculated on the fly
375              
376             my $metamap = {
377             ORDRSP => {
378             # not in 1901..1902 !!
379             line_detail => {
380             '1911..d94b' => "SG25",
381             'd95a..d05b' => "SG26",
382             'd06a..' => "SG27",
383             },
384             line_price => {
385             '1911' => "SG25/SG26",
386             '1921..d94b' => "SG25/SG27",
387             'd95a..d95b' => "SG26/SG29",
388             'd96a..d05b' => "SG26/SG30",
389             'd06a..' => "SG27/SG31",
390             },
391             line_reference => {
392             '1911' => "SG25/SG27",
393             '1921..d94b' => "SG25/SG28",
394             'd95a..d95b' => "SG26/SG30",
395             'd96a..d05b' => "SG26/SG31",
396             'd06a..' => "SG27/SG32",
397             },
398             party => {
399             '1911..d94b' => "SG2",
400             'd95a..' => "SG3",
401             },
402             currency => {
403             '1911..d94b' => "SG7",
404             'd95a..' => "SG8",
405             },
406             payment_terms => {
407             '1911..d94b' => "SG8",
408             'd95a..' => "SG9",
409             },
410             transport => {
411             '1911..d94b' => "SG9",
412             'd95a..' => "SG10",
413             },
414             delivery_terms => {
415             '1911..d94b' => "SG11",
416             'd95a..' => "SG12",
417             },
418             delivery_schedule => {
419             '1911..d94b' => "SG15",
420             'd95a..' => "SG16",
421             },
422             packaging => {
423             '1911..d94b' => "SG12",
424             'd95a..' => "SG13",
425             },
426             mark_label => {
427             '1911..d94b' => "SG13",
428             'd95a..' => "SG14",
429             },
430             handling => {
431             '1911..d94b' => "SG14",
432             'd95a..' => "SG15",
433             },
434             APR => {
435             '1911..d94b' => "SG17",
436             'd95a..' => "SG18",
437             },
438             allowance => {
439             '1911..d94b' => "SG18",
440             'd95a..' => "SG19",
441             },
442             requirement => {
443             '1911..d94b' => "SG24",
444             'd95a..' => "SG25",
445             },
446             },
447             };
448              
449             # $self->metamap('ORDRSP', 'allowance');
450             # $self->metamap('ORDRSP', 'allowance', 'd11b'); # override $self's version w/ optional arg.
451              
452             sub metamap {
453 2022 50   2022 0 47428 my $self = shift or croak "Illegal direct call to object method metamap()";
454 2022 50       3983 my $message = shift or return $self->carp_error("Missing message argument to method metamap(), e.g. 'ORDRSP'");
455 2022 50       3496 my $target = shift or return $self->carp_error("Missing target argument to method metamap(), e.g. 'line_detail'");
456 2022 50       4089 unless ($metamap->{$message}) {
457 0 0       0 $debug and $self->carp_error("Message '$message' is not mapped via metamap()");
458 0         0 return;
459             }
460 2022 100       10638 my $v = @_ ? shift : $self->version;
461 2022 50       4217 $v or return $self->carp_error("Spec version not set (or passed as optional parameter)");
462 2022 100       7309 my $ranges = $metamap->{$message}->{$target} or return; # else got nuthin
463 618 50       423013 my @keys = keys %$ranges or return; # no ranges means no hits
464 618         1147 foreach (@keys) { # note: unsorted, hence non-overlap requirement, else results undetermined (first hit in keys order)
465 2218         2324 my ($low, $hi);
466             # if (/^([^\.]*)\.\.([^\.]*)$/) {
467 2218 100       8817 if (/^(.*)\.\.(.*)$/) {
468 2101   50     6218 $low = $1 || '0900'; # tricky, default "lowest" value as sorted by spec_version_sort
469 2101   100     5570 $hi = $2 || 'zzzz'; # default "highest" value as sorted by spec_version_sort
470 2101 100 100     16404 return $ranges->{$_} if ($v eq $low or $v eq $hi); # match on a boundary is a hit
471 1553         3256 my @trio = sort {$self->spec_version_sort($a,$b)} ($low, $hi, $v);
  4202         7144  
472 1553 0       3244 $debug and print STDERR "metamap sorted (low,val,hi) ($low,$v,$hi): ", join(" ", @trio), ($v eq $trio[1] ? ' MATCH' : '' ), "\n";
    50          
473 1553 100       4764 return $ranges->{$_} if $v eq $trio[1]; # else, if it sorts to the position between bounds, it's a hit
474             } else { # solitary value
475 117 100       269 $v eq $_ and return $ranges->{$_};
476             }
477             }
478 0           return $self->carp_error("$message/$target cannot place version $v in " . scalar(@keys) . " ranges: " . join(' ', @keys));
479             }
480              
481             # $spec->metamap_keys('ORDRSP');
482             sub metamap_keys {
483 0 0   0 0   my $self = shift or croak "Illegal direct call to object method metamap_keys()";
484 0 0         my $message = shift or return $self->carp_error("Missing message argument to method metamap_keys()");
485 0 0         unless ($metamap->{$message}) {
486 0 0         $debug and $self->carp_error("Message '$message' is not mapped via metamap_keys()");
487 0           return;
488             }
489 0           return keys %{$metamap->{$message}};
  0            
490             }
491              
492             1;
493             __END__