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   430935 use 5.010001;
  7         93  
4 7     7   38 use strict;
  7         14  
  7         157  
5 7     7   34 use warnings;
  7         12  
  7         225  
6 7     7   56 no warnings 'once';
  7         15  
  7         263  
7 7     7   12694 use Log::ger;
  7         424  
  7         42  
8              
9 7     7   5031 use Data::Sah::CoerceCommon;
  7         22  
  7         282  
10              
11 7     7   51 use Exporter qw(import);
  7         15  
  7         4723  
12              
13             our $AUTHORITY = 'cpan:PERLANCAR'; # AUTHORITY
14             our $DATE = '2021-11-28'; # DATE
15             our $DIST = 'Data-Sah-Coerce'; # DIST
16             our $VERSION = '0.052'; # 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 1732168 my %args = @_;
40              
41 19   100     140 my $rt = $args{return_type} // 'val';
42             # old values still supported but deprecated
43 19 50       76 $rt = 'bool_coerced+val' if $rt eq 'status+val';
44 19 50       63 $rt = 'bool_coerced+str_errmsg+val' if $rt eq 'status+err+val';
45              
46 19         125 my $rules = Data::Sah::CoerceCommon::get_coerce_rules(
47             %args,
48             compiler=>'perl',
49             data_term=>'$data',
50             );
51              
52 19         42 my $code;
53 19 100       58 if (@$rules) {
54 18         42 my $code_require = '';
55 18         32 my %mem;
56 18         47 for my $rule (@$rules) {
57 59 100       141 next unless $rule->{modules};
58 47         67 for my $mod (keys %{$rule->{modules}}) {
  47         135  
59 55 100       149 next if $mem{$mod}++;
60 29         93 $code_require .= "require $mod;\n";
61             }
62             }
63              
64 18         43 my $expr;
65 18         44 for my $i (reverse 0..$#{$rules}) {
  18         53  
66 59         104 my $rule = $rules->[$i];
67 59         107 my $prev_term;
68 59 100       77 if ($i == $#{$rules}) {
  59         170  
69 18 100       58 if ($rt eq 'val') {
    100          
70 14         30 $prev_term = '$data';
71             } elsif ($rt eq 'bool_coerced+val') {
72 3         5 $prev_term = '[undef, $data]';
73             } else { # bool_coerced+str_errmsg+val
74 1         47 $prev_term = '[undef, undef, $data]';
75             }
76             } else {
77 41         70 $prev_term = $expr;
78             }
79              
80 59 100       149 if ($rt eq 'val') {
    100          
81 49 100       117 if ($rule->{meta}{might_fail}) {
82 13         85 $expr = "do { if ($rule->{expr_match}) { my \$res = $rule->{expr_coerce}; \$res->[0] ? undef : \$res->[1] } else { $prev_term } }";
83             } else {
84 36         176 $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         7 $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         21 $expr = "($rule->{expr_match}) ? [1, $rule->{expr_coerce}] : $prev_term";
91             }
92             } else { # bool_coerced+str_errmsg+val
93 4 100       11 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         13 $expr = "($rule->{expr_match}) ? [1, undef, $rule->{expr_coerce}] : $prev_term";
97             }
98             }
99             }
100              
101 18 100       233 $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       3 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       68 if ($Log_Coercer_Code) {
126 0         0 log_trace("Coercer code (gen args: %s): %s", \%args, $code);
127             }
128              
129 19 50       58 return $code if $args{source};
130              
131 19         8212 my $coercer = eval $code; ## no critic: BuiltinFunctions::ProhibitStringyEval
132 19 50       86 die if $@;
133 19         236 $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.052 of Data::Sah::Coerce (from Perl distribution Data-Sah-Coerce), released on 2021-11-28.
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(%args) -> 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             Arguments ('*' denotes required arguments):
347              
348             =over 4
349              
350             =item * B<coerce_rules> => I<array[str]>
351              
352             A specification of coercion rules to use (or avoid).
353              
354             This setting is used to specify which coercion rules to use (or avoid) in a
355             flexible way. Each element is a string, in the form of either C<NAME> to mean
356             specifically include a rule, or C<!NAME> to exclude a rule.
357              
358             Some coercion modules are used by default, unless explicitly avoided using the
359             '!NAME' rule.
360              
361             To not use any rules:
362              
363             To use the default rules plus R1 and R2:
364              
365             ['R1', 'R2']
366              
367             To use the default rules but not R1 and R2:
368              
369             ['!R1', '!R2']
370              
371             =item * B<coerce_to> => I<str>
372              
373             Some Sah types, like C<date>, can be represented in a choice of types in the
374             target language. For example, in Perl you can store it as a floating number
375             a.k.a. C<float(epoch)>, or as a L<DateTime> object, or L<Time::Moment>
376             object. Storing in DateTime can be convenient for date manipulation but requires
377             an overhead of loading the module and storing in a bulky format. The choice is
378             yours to make, via this setting.
379              
380             =item * B<return_type> => I<str> (default: "val")
381              
382             C<val> means the coercer will return the input (possibly) coerced or undef if
383             coercion fails.
384              
385             C<bool_coerced+val> means the coercer will return a 2-element array. The first
386             element is a bool value set to 1 if coercion has been performed or 0 if
387             otherwise. The second element is the (possibly) coerced input.
388              
389             C<bool_coerced+str_errmsg+val> means the coercer will return a 3-element array.
390             The first element is a bool value set to 1 if coercion has been performed or 0
391             if otherwise. The second element is the error message string which will be set
392             if there is a failure in coercion (or undef if coercion is successful). The
393             third element is the (possibly) coerced input.
394              
395             =item * B<source> => I<bool>
396              
397             If set to true, will return coercer source code string instead of compiled code.
398              
399             =item * B<type>* => I<sah::type_name>
400              
401              
402             =back
403              
404             Return value: (any)
405              
406             =head1 ENVIRONMENT
407              
408             =head2 LOG_SAH_COERCER_CODE => bool
409              
410             Set default for C<$Log_Coercer_Code>.
411              
412             =head1 HOMEPAGE
413              
414             Please visit the project's homepage at L<https://metacpan.org/release/Data-Sah-Coerce>.
415              
416             =head1 SOURCE
417              
418             Source repository is at L<https://github.com/perlancar/perl-Data-Sah-Coerce>.
419              
420             =head1 SEE ALSO
421              
422             L<Data::Sah::CoerceCommon> for detailed syntax of coerce rules (explicitly
423             including/excluding rules etc).
424              
425             L<Data::Sah>
426              
427             L<Data::Sah::CoerceJS>
428              
429             L<App::SahUtils>, including L<coerce-with-sah> to conveniently test coercion
430             from the command-line.
431              
432             =head1 AUTHOR
433              
434             perlancar <perlancar@cpan.org>
435              
436             =head1 CONTRIBUTING
437              
438              
439             To contribute, you can send patches by email/via RT, or send pull requests on
440             GitHub.
441              
442             Most of the time, you don't need to build the distribution yourself. You can
443             simply modify the code, then test via:
444              
445             % prove -l
446              
447             If you want to build the distribution (e.g. to try to install it locally on your
448             system), you can install L<Dist::Zilla>,
449             L<Dist::Zilla::PluginBundle::Author::PERLANCAR>, and sometimes one or two other
450             Dist::Zilla plugin and/or Pod::Weaver::Plugin. Any additional steps required
451             beyond that are considered a bug and can be reported to me.
452              
453             =head1 COPYRIGHT AND LICENSE
454              
455             This software is copyright (c) 2021, 2020, 2019, 2018, 2017, 2016 by perlancar <perlancar@cpan.org>.
456              
457             This is free software; you can redistribute it and/or modify it under
458             the same terms as the Perl 5 programming language system itself.
459              
460             =head1 BUGS
461              
462             Please report any bugs or feature requests on the bugtracker website L<https://rt.cpan.org/Public/Dist/Display.html?Name=Data-Sah-Coerce>
463              
464             When submitting a bug or request, please include a test-file or a
465             patch to an existing test-file that illustrates the bug or desired
466             feature.
467              
468             =cut