File Coverage

blib/lib/Perinci/Sub/Normalize.pm
Criterion Covered Total %
statement 88 103 85.4
branch 52 68 76.4
condition 40 47 85.1
subroutine 7 7 100.0
pod 1 1 100.0
total 188 226 83.1


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