File Coverage

blib/lib/Perinci/Sub/Normalize.pm
Criterion Covered Total %
statement 86 102 84.3
branch 50 68 73.5
condition 38 47 80.8
subroutine 7 7 100.0
pod 1 1 100.0
total 182 225 80.8


line stmt bran cond sub pod time code
1             package Perinci::Sub::Normalize;
2              
3 2     2   1226 use 5.010001;
  2         22  
4 2     2   11 use strict;
  2         4  
  2         54  
5 2     2   9 use warnings;
  2         8  
  2         62  
6              
7 2     2   11 use Exporter 'import';
  2         4  
  2         3037  
8              
9             our $AUTHORITY = 'cpan:PERLANCAR'; # AUTHORITY
10             our $DATE = '2022-12-09'; # DATE
11             our $DIST = 'Perinci-Sub-Normalize'; # DIST
12             our $VERSION = '0.204'; # VERSION
13              
14             our @EXPORT_OK = qw(
15             normalize_function_metadata
16             );
17              
18             sub _check {
19 20     20   34 my $meta = shift; # must be normalized
20              
21             CHECK_ARGS: {
22 20         30 my $argspecs = $meta->{args};
  20         35  
23             CHECK_ARGS_POS: {
24 20         28 my @pos;
  20         31  
25             my $slurpy_pos;
26 20         52 for my $argname (keys %$argspecs) {
27 18         28 my $argspec = $argspecs->{$argname};
28 18 100       68 if (defined $argspec->{pos}) {
29 10 100       25 return "Argument $argname: Negative pos" if $argspec->{pos} < 0;
30 9 100       23 return "Duplicate position $argspec->{pos}" if defined $pos[ $argspec->{pos} ];
31 8         14 $pos[ $argspec->{pos} ] = $argname;
32             }
33 16 100 100     65 if ($argspec->{slurpy} || $argspec->{greedy}) { # greedy is deprecated, but we should keep observing to make us properly strict
34             return "Argument $argname: slurpy=1 without setting pos"
35 3 100       13 unless defined $argspec->{pos};
36 1 50       14 return "Multiple args with slurpy=1" if defined $slurpy_pos;
37             }
38             }
39 16 50 33     44 if (defined $slurpy_pos && $slurpy_pos < @pos) {
40 0         0 return "Clash of argument positions: slurpy=1 defined for pos >= $slurpy_pos but there is another argument with pos > $slurpy_pos";
41             }
42             # we have holes
43             return "There needs to be more arguments that define pos"
44 16 100       45 if grep { !defined } @pos;
  10         30  
45 13 50 33     55 if ($meta->{args_as} && $meta->{args_as} =~ /\Aarray(ref)?\z/) {
46 0 0       0 return "Function accepts array/arrayref but there are arguments with no pos defined"
47             if scalar(keys %$argspecs) > @pos;
48             }
49             }
50             }
51              
52 13         25 undef;
53             }
54              
55             sub _normalize {
56 60     60   175 my ($meta, $ver, $opts, $proplist, $nmeta, $prefix, $modprefix) = @_;
57              
58 60         102 my $opt_aup = $opts->{allow_unknown_properties};
59 60         88 my $opt_nss = $opts->{normalize_sah_schemas};
60 60         95 my $opt_rip = $opts->{remove_internal_properties};
61              
62 60 100       123 if (defined $ver) {
63 31 100 66     313 defined($meta->{v}) && $meta->{v} eq $ver
64             or die "$prefix: Metadata version must be $ver";
65             }
66              
67             KEY:
68 58         201 for my $k (keys %$meta) {
69 107 100       737 die "Invalid prop/attr syntax '$k', must be word/dotted-word only"
70             unless $k =~ /\A(\w+)(?:\.(\w+(?:\.\w+)*))?(?:\((\w+)\))?\z/;
71              
72 106         174 my ($prop, $attr);
73 106 100       239 if (defined $3) {
74 1         3 $prop = $1;
75 1 50       5 $attr = defined($2) ? "$2.alt.lang.$3" : "alt.lang.$3";
76             } else {
77 105         214 $prop = $1;
78 105         169 $attr = $2;
79             }
80              
81 106 100       293 my $nk = "$prop" . (defined($attr) ? ".$attr" : "");
82              
83             # strip property/attr started with _
84 106 100 100     347 if ($prop =~ /\A_/ || defined($attr) && $attr =~ /\A_|\._/) {
      100        
85 5 100       12 unless ($opt_rip) {
86 4         8 $nmeta->{$nk} = $meta->{$k};
87             }
88 5         11 next KEY;
89             }
90              
91 101         167 my $prop_proplist = $proplist->{$prop};
92              
93             # try to load module that declare new props first
94 101 100 100     285 if (!$opt_aup && !$prop_proplist) {
95 4   66     43 $modprefix //= $prefix;
96 4         14 my $mod = "Perinci/Sub/Property$modprefix/$prop.pm";
97 4         10 eval { require $mod };
  4         706  
98             # hide technical error message from require()
99 4 50       33 if ($@) {
100 4 50       69 die "Unknown property '$prefix/$prop' (and couldn't ".
101             "load property module '$mod'): $@" if $@;
102             }
103 0         0 $prop_proplist = $proplist->{$prop};
104             }
105 97 50 66     263 die "Unknown property '$prefix/$prop'"
106             unless $opt_aup || $prop_proplist;
107              
108 97 100 100     462 if ($prop_proplist && $prop_proplist->{_prop}) {
    50 66        
    100 66        
109             die "Property '$prefix/$prop' must be a hash"
110 9 50       29 unless ref($meta->{$k}) eq 'HASH';
111 9         18 $nmeta->{$nk} = {};
112             _normalize(
113             $meta->{$k},
114             $prop_proplist->{_ver},
115             $opts,
116             $prop_proplist->{_prop},
117 9         50 $nmeta->{$nk},
118             "$prefix/$prop",
119             );
120             } elsif ($prop_proplist && $prop_proplist->{_elem_prop}) {
121             die "Property '$prefix/$prop' must be an array"
122 0 0       0 unless ref($meta->{$k}) eq 'ARRAY';
123 0         0 $nmeta->{$nk} = [];
124 0         0 my $i = 0;
125 0         0 for (@{ $meta->{$k} }) {
  0         0  
126 0         0 my $href = {};
127 0 0       0 if (ref($_) eq 'HASH') {
128             _normalize(
129             $_,
130             $prop_proplist->{_ver},
131             $opts,
132             $prop_proplist->{_elem_prop},
133 0         0 $href,
134             "$prefix/$prop/$i",
135             );
136 0         0 push @{ $nmeta->{$nk} }, $href;
  0         0  
137             } else {
138 0         0 push @{ $nmeta->{$nk} }, $_;
  0         0  
139             }
140 0         0 $i++;
141             }
142             } elsif ($prop_proplist && $prop_proplist->{_value_prop}) {
143             die "Property '$prefix/$prop' must be a hash"
144 19 50       51 unless ref($meta->{$k}) eq 'HASH';
145 19         42 $nmeta->{$nk} = {};
146 19         28 for (keys %{ $meta->{$k} }) {
  19         63  
147 24         53 $nmeta->{$nk}{$_} = {};
148             die "Property '$prefix/$prop/$_' must be a hash"
149 24 50       56 unless ref($meta->{$k}{$_}) eq 'HASH';
150             _normalize(
151             $meta->{$k}{$_},
152             $prop_proplist->{_ver},
153             $opts,
154             $prop_proplist->{_value_prop},
155 24 100       204 $nmeta->{$nk}{$_},
156             "$prefix/$prop/$_",
157             ($prop eq 'args' ? "$prefix/arg" : undef),
158             );
159             }
160             } else {
161 69 100 100     181 if ($k eq 'schema' && $opt_nss) { # XXX currently hardcoded
162 20         95 require Data::Sah::Normalize;
163             $nmeta->{$nk} = Data::Sah::Normalize::normalize_schema(
164 20         54 $meta->{$k});
165             } else {
166 49         133 $nmeta->{$nk} = $meta->{$k};
167             }
168             }
169             } # for each key
170 48         387 $nmeta;
171             }
172              
173             sub normalize_function_metadata($;$) { ## no critic: Subroutines::ProhibitSubroutinePrototypes
174 27     27 1 90738 my ($meta, $opts) = @_;
175              
176 27   100     133 $opts //= {};
177              
178 27   100     121 $opts->{allow_unknown_properties} //= 0;
179 27   100     99 $opts->{normalize_sah_schemas} //= 1;
180 27   100     98 $opts->{remove_internal_properties} //= 0;
181              
182 27         769 require Sah::Schema::rinci::function_meta;
183 27         3575 my $sch = $Sah::Schema::rinci::function_meta::schema;
184             my $sch_proplist = $sch->[1]{_prop}
185 27 50       76 or die "BUG: Rinci schema structure changed (1a)";
186              
187 27         71 my $nmeta = _normalize($meta, 1.1, $opts, $sch_proplist, {}, '');
188              
189 20         49 my $err = _check($meta);
190 20 100       115 die $err if $err;
191              
192 13         187 $nmeta;
193             }
194              
195             1;
196             # ABSTRACT: Normalize Rinci function metadata
197              
198             __END__
199              
200             =pod
201              
202             =encoding UTF-8
203              
204             =head1 NAME
205              
206             Perinci::Sub::Normalize - Normalize Rinci function metadata
207              
208             =head1 VERSION
209              
210             This document describes version 0.204 of Perinci::Sub::Normalize (from Perl distribution Perinci-Sub-Normalize), released on 2022-12-09.
211              
212             =head1 SYNOPSIS
213              
214             use Perinci::Sub::Normalize qw(normalize_function_metadata);
215              
216             my $nmeta = normalize_function_metadata($meta);
217              
218             =head1 FUNCTIONS
219              
220             =head2 normalize_function_metadata($meta[ , \%opts ]) => HASH
221              
222             Normalize and check L<Rinci> function metadata C<$meta>. Return normalized
223             metadata, which is a shallow copy of C<$meta>. Die on error.
224              
225             Available options:
226              
227             =over
228              
229             =item * allow_unknown_properties => BOOL (default: 0)
230              
231             If set to true, will die if there are unknown properties.
232              
233             =item * normalize_sah_schemas => BOOL (default: 1)
234              
235             By default, L<Sah> schemas e.g. in C<result/schema> or C<args/*/schema> property
236             is normalized using L<Data::Sah>'s C<normalize_schema>. Set this to 0 if you
237             don't want this.
238              
239             =item * remove_internal_properties => BOOL (default: 0)
240              
241             If set to 1, all properties and attributes starting with underscore (C<_>) with
242             will be stripped. According to L<DefHash> specification, they are ignored and
243             usually contain notes/comments/extra information.
244              
245             =back
246              
247             =head1 HOMEPAGE
248              
249             Please visit the project's homepage at L<https://metacpan.org/release/Perinci-Sub-Normalize>.
250              
251             =head1 SOURCE
252              
253             Source repository is at L<https://github.com/perlancar/perl-Perinci-Sub-Normalize>.
254              
255             =head1 SEE ALSO
256              
257             L<Rinci::function>
258              
259             =head1 AUTHOR
260              
261             perlancar <perlancar@cpan.org>
262              
263             =head1 CONTRIBUTOR
264              
265             =for stopwords Steven Haryanto
266              
267             Steven Haryanto <stevenharyanto@gmail.com>
268              
269             =head1 CONTRIBUTING
270              
271              
272             To contribute, you can send patches by email/via RT, or send pull requests on
273             GitHub.
274              
275             Most of the time, you don't need to build the distribution yourself. You can
276             simply modify the code, then test via:
277              
278             % prove -l
279              
280             If you want to build the distribution (e.g. to try to install it locally on your
281             system), you can install L<Dist::Zilla>,
282             L<Dist::Zilla::PluginBundle::Author::PERLANCAR>,
283             L<Pod::Weaver::PluginBundle::Author::PERLANCAR>, and sometimes one or two other
284             Dist::Zilla- and/or Pod::Weaver plugins. Any additional steps required beyond
285             that are considered a bug and can be reported to me.
286              
287             =head1 COPYRIGHT AND LICENSE
288              
289             This software is copyright (c) 2022, 2018, 2016, 2015, 2014 by perlancar <perlancar@cpan.org>.
290              
291             This is free software; you can redistribute it and/or modify it under
292             the same terms as the Perl 5 programming language system itself.
293              
294             =head1 BUGS
295              
296             Please report any bugs or feature requests on the bugtracker website L<https://rt.cpan.org/Public/Dist/Display.html?Name=Perinci-Sub-Normalize>
297              
298             When submitting a bug or request, please include a test-file or a
299             patch to an existing test-file that illustrates the bug or desired
300             feature.
301              
302             =cut