File Coverage

blib/lib/Data/Sah/Coerce.pm
Criterion Covered Total %
statement 65 68 95.5
branch 32 40 80.0
condition 2 2 100.0
subroutine 8 8 100.0
pod 1 1 100.0
total 108 119 90.7


line stmt bran cond sub pod time code
1             package Data::Sah::Coerce;
2              
3 7     7   436467 use 5.010001;
  7         91  
4 7     7   40 use strict;
  7         22  
  7         150  
5 7     7   34 use warnings;
  7         14  
  7         208  
6 7     7   52 no warnings 'once';
  7         14  
  7         264  
7 7     7   13616 use Log::ger;
  7         503  
  7         37  
8              
9 7     7   4938 use Data::Sah::CoerceCommon;
  7         17  
  7         289  
10              
11 7     7   57 use Exporter qw(import);
  7         15  
  7         5173  
12              
13             our $AUTHORITY = 'cpan:PERLANCAR'; # AUTHORITY
14             our $DATE = '2021-10-18'; # DATE
15             our $DIST = 'Data-Sah-Coerce'; # DIST
16             our $VERSION = '0.050'; # VERSION
17              
18             our @EXPORT_OK = qw(gen_coercer);
19              
20             our %SPEC;
21              
22             our $Log_Coercer_Code = $ENV{LOG_SAH_COERCER_CODE} // 0;
23              
24             $SPEC{gen_coercer} = {
25             v => 1.1,
26             summary => 'Generate coercer code',
27             description => <<'_',
28              
29             This is mostly for testing. Normally the coercion rules will be used from
30             <pm:Data::Sah>.
31              
32             _
33             args => {
34             %Data::Sah::CoerceCommon::gen_coercer_args,
35             },
36             result_naked => 1,
37             };
38             sub gen_coercer {
39 19     19 1 1705862 my %args = @_;
40              
41 19   100     131 my $rt = $args{return_type} // 'val';
42             # old values still supported but deprecated
43 19 50       65 $rt = 'bool_coerced+val' if $rt eq 'status+val';
44 19 50       57 $rt = 'bool_coerced+str_errmsg+val' if $rt eq 'status+err+val';
45              
46 19         109 my $rules = Data::Sah::CoerceCommon::get_coerce_rules(
47             %args,
48             compiler=>'perl',
49             data_term=>'$data',
50             );
51              
52 19         47 my $code;
53 19 100       63 if (@$rules) {
54 18         36 my $code_require = '';
55 18         34 my %mem;
56 18         59 for my $rule (@$rules) {
57 59 100       138 next unless $rule->{modules};
58 47         63 for my $mod (keys %{$rule->{modules}}) {
  47         138  
59 55 100       149 next if $mem{$mod}++;
60 29         91 $code_require .= "require $mod;\n";
61             }
62             }
63              
64 18         39 my $expr;
65 18         37 for my $i (reverse 0..$#{$rules}) {
  18         60  
66 59         108 my $rule = $rules->[$i];
67 59         76 my $prev_term;
68 59 100       74 if ($i == $#{$rules}) {
  59         141  
69 18 100       65 if ($rt eq 'val') {
    100          
70 14         31 $prev_term = '$data';
71             } elsif ($rt eq 'bool_coerced+val') {
72 3         6 $prev_term = '[undef, $data]';
73             } else { # bool_coerced+str_errmsg+val
74 1         87 $prev_term = '[undef, undef, $data]';
75             }
76             } else {
77 41         67 $prev_term = $expr;
78             }
79              
80 59 100       147 if ($rt eq 'val') {
    100          
81 49 100       104 if ($rule->{meta}{might_fail}) {
82 13         76 $expr = "do { if ($rule->{expr_match}) { my \$res = $rule->{expr_coerce}; \$res->[0] ? undef : \$res->[1] } else { $prev_term } }";
83             } else {
84 36         214 $expr = "($rule->{expr_match}) ? ($rule->{expr_coerce}) : $prev_term";
85             }
86             } elsif ($rt eq 'bool_coerced+val') {
87 6 100       15 if ($rule->{meta}{might_fail}) {
88 1         8 $expr = "do { if ($rule->{expr_match}) { my \$res = $rule->{expr_coerce}; \$res->[0] ? [1,\$res->[1]] : [1,\$res->[1]] } else { $prev_term } }";
89             } else {
90 5         25 $expr = "($rule->{expr_match}) ? [1, $rule->{expr_coerce}] : $prev_term";
91             }
92             } else { # bool_coerced+str_errmsg+val
93 4 100       12 if ($rule->{meta}{might_fail}) {
94 1         8 $expr = "do { if ($rule->{expr_match}) { my \$res = $rule->{expr_coerce}; \$res->[0] ? [1, \$res->[0], \$res->[1]] : [1, undef, \$res->[1]] } else { $prev_term } }";
95             } else {
96 3         20 $expr = "($rule->{expr_match}) ? [1, undef, $rule->{expr_coerce}] : $prev_term";
97             }
98             }
99             }
100              
101 18 100       194 $code = join(
    100          
102             "",
103             $code_require,
104             "sub {\n",
105             " my \$data = shift;\n",
106             " unless (defined \$data) {\n",
107             " ", ($rt eq 'val' ? "return undef;" :
108             $rt eq 'bool_coerced+val' ? "return [undef, undef];" :
109             "return [undef, undef, undef];" # bool_coerced+str_errmsg+val
110             ), "\n",
111             " }\n",
112             " $expr;\n",
113             "}",
114             );
115             } else {
116 1 50       4 if ($rt eq 'val') {
    0          
117 1         3 $code = 'sub { $_[0] }';
118             } elsif ($rt eq 'bool_coerced+val') {
119 0         0 $code = 'sub { [undef, $_[0]] }';
120             } else { # bool_coerced+str_errmsg+val
121 0         0 $code = 'sub { [undef, undef, $_[0]] }';
122             }
123             }
124              
125 19 50       69 if ($Log_Coercer_Code) {
126 0         0 log_trace("Coercer code (gen args: %s): %s", \%args, $code);
127             }
128              
129 19 50       54 return $code if $args{source};
130              
131 19         7415 my $coercer = eval $code; ## no critic: BuiltinFunctions::ProhibitStringyEval
132 19 50       98 die if $@;
133 19         197 $coercer;
134             }
135              
136             1;
137             # ABSTRACT: Coercion rules for Data::Sah
138              
139             __END__
140              
141             =pod
142              
143             =encoding UTF-8
144              
145             =head1 NAME
146              
147             Data::Sah::Coerce - Coercion rules for Data::Sah
148              
149             =head1 VERSION
150              
151             This document describes version 0.050 of Data::Sah::Coerce (from Perl distribution Data-Sah-Coerce), released on 2021-10-18.
152              
153             =head1 SYNOPSIS
154              
155             use Data::Sah::Coerce qw(gen_coercer);
156              
157             # a utility routine: gen_coercer
158             my $c = gen_coercer(
159             type => 'date',
160             coerce_to => 'DateTime',
161             coerce_rules => ['From_str::natural'], # explicitly enable a rule, etc. See Data::Sah::CoerceCommon's get_coerce_rules() for detailed syntax
162             # return_type => 'str+val', # default is 'val'
163             );
164              
165             my $val = $c->(123); # unchanged, 123
166             my $val = $c->(1463307881); # becomes a DateTime object
167             my $val = $c->("2016-05-15"); # becomes a DateTime object
168             my $val = $c->("2016foo"); # unchanged, "2016foo"
169              
170             =head1 DESCRIPTION
171              
172             This distribution contains a standard set of coercion rules for L<Data::Sah>. It
173             is separated from the C<Data-Sah> distribution and can be used independently.
174              
175             A coercion rule is put in
176             C<Data::Sah::Coerce::$COMPILER::To_$TARGET_TYPE::From_$SOURCE_TYPE::DESCRIPTION>
177             module, for example: L<Data::Sah::Coerce::perl::To_date::From_float::epoch> for
178             converting date from integer (Unix epoch) or
179             L<Data::Sah::Coerce::perl::To_date::From_str::iso8601> for converting date from
180             ISO8601 strings like "2016-05-15".
181              
182             Basically, a coercion rule will provide an expression (C<expr_match>) that
183             evaluates to true when data can be coerced, and an expression (C<expr_coerce>)
184             to actually coerce/convert data to the target type. This rule can be combined
185             with other rules to form the final coercion code.
186              
187             The module must contain C<meta> subroutine which must return a hashref that has
188             the following keys (C<*> marks that the key is required):
189              
190             =over
191              
192             =item * v* => int (default: 1)
193              
194             Metadata specification version. From L<DefHash>. Currently at 4.
195              
196             History: bumped from 3 to 4 to remove C<enable_by_default> property. Now the
197             list of standard (enabled-by-default) coercion rules is maintained in
198             Data::Sah::Coerce itself. This allows us to skip scanning all
199             Data::Sah::Coerce::* coercion modules installed on the system. Data::Sah::Coerce
200             still accepts version 3; it just ignores the C<enable_by_default> property.
201              
202             History: bumped from 2 to 3 to allow coercion expression to return error message
203             explaining why coercion fails. The C<might_die> metadata property is replaced
204             with C<might_fail>. When C<might_fail> is set to true, C<expr_coerce> must
205             return array containing error message and coerced data, instead of just coerced
206             data.
207              
208             History: Bumped from 1 to 2 to exclude old module names.
209              
210             =item * summary => str
211              
212             From L<DefHash>.
213              
214             =item * might_fail => bool (default: 0)
215              
216             Whether coercion might fail, e.g. because of invalid input. If set to 1,
217             C<expr_coerce> key that the C<coerce()> routine returns must be an expression
218             that returns an array (envelope) of C<< (error_msg, data) >> instead of just
219             coerced data. Error message should be a string that is set when coercion fails
220             and explains why. Otherwise, if coercion succeeds, the error message string
221             should be set to undefined value.
222              
223             An example of a rule like this is coercing from string in the form of
224             "YYYY-MM-DD" to a DateTime object. The rule might match any string in the form
225             of C<< /\A(\d{4})-(\d{2})-(\d{2})\z/ >> while it might not be a valid date.
226              
227             This is used for coercion rules that act as a data checker.
228              
229             =item * prio => int (0-100, default: 50)
230              
231             This is to regulate the ordering of rules. The higher the number, the lower the
232             priority (meaning the rule will be put further back). Rules that are
233             computationally more expensive and/or match more broadly in general should be
234             put further back (lower priority, higher number).
235              
236             =item * precludes => array of (str|re)
237              
238             List the other rules or rule patterns that are precluded by this rule. Rules
239             that are mutually exclusive or pure alternatives to one another (e.g. date
240             coercien rules
241             L<From_str::natural|Data::Sah::Coerce::To_date::From_str::natural> vs
242             L<From_str::flexible|Data::Sah::Coerce::To_date::From_str::flexible> both parse
243             natural language date string; there is usually little to none of usefulness in
244             using both; besides, both rules match all string and dies when failing to parse
245             the string. So in C<From_str::natural> rule, you'll find this metadata:
246              
247             precludes => [qr/\A(From_str::alami(_.+)?|From_str::natural)\z/]
248              
249             and in C<From_str::flexible> rule you'll find this metadata:
250              
251             precludes => [qr/\A(From_str::alami(_.+)?|From_str::flexible)\z/]
252              
253             Also note that rules which are specifically requested to be used (e.g. using
254             C<x.perl.coerce_rules> attribute in Sah schema) will still be precluded.
255              
256             =back
257              
258             The module must also contain C<coerce> subroutine which must generate the code
259             for coercion. The subroutine must accept a hash of arguments (C<*> indicates
260             required arguments):
261              
262             =over
263              
264             =item * data_term => str
265              
266             =item * coerce_to => str
267              
268             Some Sah types are "abstract" and can be represented using a choice of several
269             actual types in the target programming language. For example, "date" can be
270             represented in Perl as an integer (Unix epoch value), or a DateTime object, or a
271             Time::Moment object.
272              
273             Not all target Sah types will need this argument.
274              
275             =back
276              
277             The C<coerce> subroutine must return a hashref with the following keys (C<*>
278             indicates required keys):
279              
280             =over
281              
282             =item * expr_match => str
283              
284             Expression in the target language to test whether the data can be coerced. For
285             example, in C<Data::Sah::Coerce::perl::To_date::From_float::epoch>, only
286             integers ranging from 10^8 to 2^31 are converted into date. Non-integers or
287             integers outside this range are not coerced.
288              
289             =item * expr_coerce => str
290              
291             Expression in the target language to actually convert data to the target type.
292              
293             =item * modules => hash
294              
295             A list of modules required by the expressions.
296              
297             =back
298              
299             Basically, the C<coerce> subroutine must generates a code that accepts a
300             non-undef data and must convert this data to the desired type/format under the
301             right condition. The code to match the right condition must be put in
302             C<expr_match> and the code to convert data must be put in C<expr_coerce>.
303              
304             Program/library that uses Data::Sah::Coerce can collect rules from the rule
305             modules then compose them into the final code, something like (in pseudocode):
306              
307             if (data is undef) {
308             return undef;
309             } elsif (data matches expr-match-from-rule1) {
310             return expr-coerce-from-rule1;
311             } elsif (data matches expr-match-from-rule2) {
312             return expr-coerce-from-rule1;
313             ...
314             } else {
315             # does not match any expr-match
316             return original data;
317             }
318              
319             =head1 VARIABLES
320              
321             =head2 $Log_Coercer_Code => bool (default: from ENV or 0)
322              
323             If set to true, will log the generated coercer code (currently using L<Log::ger>
324             at trace level). To see the log message, e.g. to the screen, you can use
325             something like:
326              
327             % TRACE=1 perl -MLog::ger::LevelFromEnv -MLog::ger::Output=Screen \
328             -MData::Sah::Coerce=gen_coercer -E'my $c = gen_coercer(...)'
329              
330             =head1 FUNCTIONS
331              
332              
333             =head2 gen_coercer
334              
335             Usage:
336              
337             gen_coercer() -> any
338              
339             Generate coercer code.
340              
341             This is mostly for testing. Normally the coercion rules will be used from
342             L<Data::Sah>.
343              
344             This function is not exported by default, but exportable.
345              
346             No arguments.
347              
348             Return value: (any)
349              
350             =head1 ENVIRONMENT
351              
352             =head2 LOG_SAH_COERCER_CODE => bool
353              
354             Set default for C<$Log_Coercer_Code>.
355              
356             =head1 HOMEPAGE
357              
358             Please visit the project's homepage at L<https://metacpan.org/release/Data-Sah-Coerce>.
359              
360             =head1 SOURCE
361              
362             Source repository is at L<https://github.com/perlancar/perl-Data-Sah-Coerce>.
363              
364             =head1 SEE ALSO
365              
366             L<Data::Sah::CoerceCommon> for detailed syntax of coerce rules (explicitly
367             including/excluding rules etc).
368              
369             L<Data::Sah>
370              
371             L<Data::Sah::CoerceJS>
372              
373             L<App::SahUtils>, including L<coerce-with-sah> to conveniently test coercion
374             from the command-line.
375              
376             =head1 AUTHOR
377              
378             perlancar <perlancar@cpan.org>
379              
380             =head1 CONTRIBUTING
381              
382              
383             To contribute, you can send patches by email/via RT, or send pull requests on
384             GitHub.
385              
386             Most of the time, you don't need to build the distribution yourself. You can
387             simply modify the code, then test via:
388              
389             % prove -l
390              
391             If you want to build the distribution (e.g. to try to install it locally on your
392             system), you can install L<Dist::Zilla>,
393             L<Dist::Zilla::PluginBundle::Author::PERLANCAR>, and sometimes one or two other
394             Dist::Zilla plugin and/or Pod::Weaver::Plugin. Any additional steps required
395             beyond that are considered a bug and can be reported to me.
396              
397             =head1 COPYRIGHT AND LICENSE
398              
399             This software is copyright (c) 2021, 2020, 2019, 2018, 2017, 2016 by perlancar <perlancar@cpan.org>.
400              
401             This is free software; you can redistribute it and/or modify it under
402             the same terms as the Perl 5 programming language system itself.
403              
404             =head1 BUGS
405              
406             Please report any bugs or feature requests on the bugtracker website L<https://rt.cpan.org/Public/Dist/Display.html?Name=Data-Sah-Coerce>
407              
408             When submitting a bug or request, please include a test-file or a
409             patch to an existing test-file that illustrates the bug or desired
410             feature.
411              
412             =cut