File Coverage

blib/lib/Perinci/Sub/CoerceArgs.pm
Criterion Covered Total %
statement 127 139 91.3
branch 82 112 73.2
condition 55 90 61.1
subroutine 11 11 100.0
pod 1 1 100.0
total 276 353 78.1


line stmt bran cond sub pod time code
1             package Perinci::Sub::CoerceArgs;
2              
3             our $DATE = '2015-09-30'; # DATE
4             our $VERSION = '0.12'; # VERSION
5              
6 1     1   655 use 5.010001;
  1         2  
7 1     1   4 use strict;
  1         2  
  1         17  
8 1     1   4 use warnings;
  1         1  
  1         25  
9             #use Log::Any '$log';
10              
11 1     1   4 use Scalar::Util qw(blessed looks_like_number);
  1         1  
  1         104  
12              
13 1     1   5 use Exporter;
  1         2  
  1         2254  
14             our @ISA = qw(Exporter);
15             our @EXPORT_OK = qw(
16             coerce_args
17             );
18              
19             our %SPEC;
20              
21             # a cheap Module::Load
22             #sub _require_class {
23             # my $class = shift;
24             # (my $class_pm = $class) =~ s!::!/!g; $class_pm .= ".pm";
25             # require $class_pm;
26             #}
27              
28             sub _coerce_to_datetime {
29 6     6   10 my ($args, $arg_name) = @_;
30              
31 6         10 my $val = $args->{$arg_name};
32              
33 6 100       87 if ($val =~ /\A\d{8,}\z/) {
    100          
    50          
34 2         8 require DateTime;
35             $args->{$arg_name} = DateTime->from_epoch(
36             epoch => $val,
37 2   100     18 time_zone => $ENV{TZ} // "UTC",
38             );
39 2         408 return [200];
40             } elsif ($val =~ m!\A
41             (\d{4})[/-](\d{1,2})[/-](\d{1,2})
42             (?:[ Tt](\d{1,2}):(\d{1,2}):(\d{1,2}))?
43             \z!x) {
44 3         60 require DateTime;
45             $args->{$arg_name} = DateTime->new(
46             year => $1, month => $2, day => $3,
47             hour => $4 // 0,
48             minute => $4 // 0,
49             second => $4 // 0,
50 3   100     74 time_zone => $ENV{TZ} // "UTC",
      100        
      100        
      50        
51             );
52 3         761 return [200];
53             } elsif (blessed($val)) {
54 1 50       9 if ($val->isa("DateTime")) {
    50          
55             # no-op
56 0         0 return [200];
57             } elsif ($val->isa("Time::Moment")) {
58 1         4 require DateTime;
59 1 50       15 my $tz = sprintf("%s%04d",
60             $val->offset < 0 ? "-":"+",
61             abs($val->offset/60*100));
62 1         8 $args->{$arg_name} = DateTime->from_epoch(
63             epoch => $val->epoch,
64             time_zone => $tz,
65             );
66 1         441 return [200];
67             }
68             }
69              
70 0         0 return [400, "Can't coerce '$arg_name' to DateTime object: " .
71             "'$args->{$arg_name}'"];
72             }
73              
74             sub _coerce_to_time_moment {
75 6     6   8 my ($args, $arg_name) = @_;
76              
77 6         8 my $val = $args->{$arg_name};
78              
79             # XXX just use Time::Moment's from_string()?
80 6 100       45 if ($val =~ /\A\d{8,}\z/) {
    100          
    50          
81 2         8 require Time::Moment;
82 2         9 $args->{$arg_name} = Time::Moment->from_epoch($val);
83 2         4 return [200];
84             } elsif ($val =~ m!\A
85             (\d{4})[/-](\d{1,2})[/-](\d{1,2})
86             (?:[ Tt](\d{1,2}):(\d{1,2}):(\d{1,2}))?
87             \z!x) {
88             # XXX parse time zone offset
89 3         50 require Time::Moment;
90 3   100     52 $args->{$arg_name} = Time::Moment->new(
      100        
      100        
91             year => $1, month => $2, day => $3,
92             hour => $4 // 0,
93             minute => $4 // 0,
94             second => $4 // 0,
95             );
96 3         8 return [200];
97             } elsif (blessed($val)) {
98 1 50       7 if ($val->isa("Time::Moment")) {
    0          
99             # no-op
100 1         3 return [200];
101             } elsif ($val->isa("DateTime")) {
102 0         0 require Time::Moment;
103 0         0 $args->{$arg_name} = Time::Moment->from_object($val);
104 0         0 return [200];
105             }
106             }
107              
108 0         0 return [400, "Can't coerce '$arg_name' to Time::Moment object: " .
109             "'$args->{$arg_name}'"];
110             }
111              
112             sub _coerce_to_epoch {
113 4     4   7 my ($args, $arg_name) = @_;
114              
115 4         5 my $val = $args->{$arg_name};
116              
117 4 100       38 if (looks_like_number($val)) {
    100          
    50          
118             # no-op
119 1         3 return [200];
120             } elsif ($val =~ m!\A
121             (\d{4})[/-](\d{1,2})[/-](\d{1,2})
122             (?:[ Tt](\d{1,2}):(\d{1,2}):(\d{1,2}))?
123             \z!x) {
124 2         45 require DateTime;
125             $args->{$arg_name} = DateTime->new(
126             year => $1, month => $2, day => $3,
127             hour => $4 // 0,
128             minute => $4 // 0,
129             second => $4 // 0,
130 2   100     33 time_zone => $ENV{TZ} // "UTC",
      100        
      100        
      50        
131             )->epoch;
132 2         436 return [200];
133             } elsif (blessed($val)) {
134 1 50       11 if ($val->isa("DateTime")) {
    50          
135 0         0 $args->{$arg_name} = $val->epoch;
136 0         0 return [200];
137             } elsif ($val->isa("Time::Moment")) {
138 1         4 $args->{$arg_name} = $val->epoch;
139 1         3 return [200];
140             }
141             }
142              
143 0         0 return [400, "Can't coerce epoch " .
144             "'$arg_name' from '$args->{$arg_name}'"];
145             }
146              
147             sub _coerce_to_datetime_duration {
148 6     6   10 my ($args, $arg_name) = @_;
149              
150 6         10 my $val = $args->{$arg_name};
151              
152 6         6 my $d;
153              
154 6 100 33     44 if ($val =~ /\A\+?\d+(?:\.\d*)?\z/) {
    100          
    100          
    50          
155 2         9 require DateTime::Duration;
156 2         5 my $days = int($val/86400);
157 2         4 my $secs = $val - $days*86400;
158 2         8 $args->{$arg_name} = DateTime::Duration->new(
159             days => $days,
160             seconds => $secs,
161             );
162 2         111 return [200];
163             } elsif ($val =~ /\AP
164             (?:([0-9]+(?:\.[0-9]+)?)Y)?
165             (?:([0-9]+(?:\.[0-9]+)?)M)?
166             (?:([0-9]+(?:\.[0-9]+)?)W)?
167             (?:([0-9]+(?:\.[0-9]+)?)D)?
168             (?: T
169             (?:([0-9]+(?:\.[0-9]+)?)H)?
170             (?:([0-9]+(?:\.[0-9]+)?)M)?
171             (?:([0-9]+(?:\.[0-9]+)?)S)?
172             )?\z/x) {
173 2         9 require DateTime::Duration;
174 2   50     56 $args->{$arg_name} = DateTime::Duration->new(
      50        
      50        
      50        
      50        
      50        
      50        
175             years => $1 || 0,
176             months => $2 || 0,
177             weeks => $3 || 0,
178             days => $4 || 0,
179             hours => $5 || 0,
180             minutes => $6 || 0,
181             seconds => $7 || 0,
182             );
183 2         231 return [200];
184             } elsif (blessed($val)) {
185 1 50       6 if ($val->isa("DateTime::Duration")) {
186             # no-op
187 1         3 return [200];
188             }
189 1         6 } elsif (eval { require Time::Duration::Parse::AsHash; $d = Time::Duration::Parse::AsHash::parse_duration($val) } && !$@) {
  1         5  
190 1         45 require DateTime::Duration;
191             $args->{$arg_name} = DateTime::Duration->new(
192             years => $d->{years} || 0,
193             months => $d->{months} || 0,
194             weeks => $d->{weeks} || 0,
195             days => $d->{days} || 0,
196             hours => $d->{hours} || 0,
197             minutes => $d->{minutes} || 0,
198 1   50     32 seconds => $d->{seconds} || 0,
      50        
      50        
      50        
      50        
      50        
      50        
199             );
200 1         68 return [200];
201             }
202              
203 0         0 return [400, "Can't coerce '$arg_name' to DateTime::Duration object: " .
204             "'$args->{$arg_name}'"];
205             }
206              
207             sub _coerce_to_secs {
208 4     4   8 my ($args, $arg_name) = @_;
209              
210 4         6 my $val = $args->{$arg_name};
211              
212 4         4 my $d;
213              
214 4 100 33     30 if ($val =~ /\A\+?\d+(?:\.\d*)?\z/) {
    100          
    100          
    50          
215             # no-op
216 1         2 return [200];
217             } elsif ($val =~ /\AP
218             (?:([0-9]+(?:\.[0-9]+)?)Y)?
219             (?:([0-9]+(?:\.[0-9]+)?)M)?
220             (?:([0-9]+(?:\.[0-9]+)?)W)?
221             (?:([0-9]+(?:\.[0-9]+)?)D)?
222             (?: T
223             (?:([0-9]+(?:\.[0-9]+)?)H)?
224             (?:([0-9]+(?:\.[0-9]+)?)M)?
225             (?:([0-9]+(?:\.[0-9]+)?)S)?
226             )?\z/x) {
227 1   50     33 $args->{$arg_name} =
      50        
      50        
      50        
      50        
      50        
      50        
228             (($1//0)*365 + ($2 // 0)*30 + ($3 // 0)*7 + ($4 // 0)) * 86400 +
229             ($5 // 0)*3600 + ($6 // 0)*60 + ($7 // 0);
230 1         4 return [200];
231             } elsif (blessed($val)) {
232 1 50       6 if ($val->isa("DateTime::Duration")) {
233 1         5 my ($y, $mon, $d, $min, $s) = $val->in_units(
234             "years", "months", "days", "minutes", "seconds");
235 1         35 $args->{$arg_name} =
236             ($y*365 + $mon*30 + $d) * 86400 +
237             $min*60 + $s;
238 1         3 return [200];
239             }
240 1         5 } elsif (eval { require Time::Duration::Parse::AsHash; $d = Time::Duration::Parse::AsHash::parse_duration($val) } && !$@) {
  1         4  
241             $args->{$arg_name} =
242             ($d->{years} // 0) * 365*86400 +
243             ($d->{months} // 0) * 30*86400 +
244             ($d->{weeks} // 0) * 7*86400 +
245             ($d->{days} // 0) * 86400 +
246             ($d->{hours} // 0) * 3600 +
247             ($d->{minutes} // 0) * 60 +
248 1   50     61 ($d->{seconds} // 0);
      50        
      50        
      50        
      50        
      50        
      50        
249 1         3 return [200];
250             }
251              
252 0         0 return [400, "Can't coerce '$arg_name' to seconds: " .
253             "'$args->{$arg_name}'"];
254             }
255              
256             $SPEC{coerce_args} = {
257             v => 1.1,
258             summary => 'Coerce arguments',
259             description => <<'_',
260              
261             This routine can be used when function arguments are retrieved from strings,
262             like from command-line arguments in CLI application (see
263             `Perinci::CmdLine::Lite` or `Perinci::CmdLine::Classic`) or from web form
264             variables in web application (see `Borang`). For convenience, object or complex
265             data structure can be converted from string (e.g. `DateTime` object from strings
266             like `2015-03-27` or epoch integer). And filters can be applied to
267             clean/preprocess the string (e.g. remove leading/trailing blanks) beforehand.
268              
269             _
270             args => {
271             meta => {
272             summary => 'Rinci function metadata',
273             schema => 'hash*',
274             req => 1,
275             },
276             meta_is_normalized => {
277             schema => 'bool*',
278             },
279             args => {
280             summary => 'Reference to hash which store the arguments',
281             schema => 'hash*',
282             },
283             },
284             };
285             sub coerce_args {
286 40     40 1 181630 my %fargs = @_;
287              
288 40 50       115 my $meta = $fargs{meta} or return [400, "Please specify meta"];
289 40 100       91 unless ($fargs{meta_is_normalized}) {
290 39         919 require Perinci::Sub::Normalize;
291 39         3500 $meta = Perinci::Sub::Normalize::normalize_function_metadata($meta);
292             }
293 40         7109 my $args = $fargs{args};
294              
295 40         93 for my $arg_name (keys %$args) {
296 40         59 my $val = $args->{$arg_name};
297 40 50       89 next unless defined($val);
298 40         57 my $arg_spec = $meta->{args}{$arg_name};
299 40 50       77 next unless $arg_spec;
300              
301 40 100       83 if (my $filters = $arg_spec->{filters}) {
302 5         8 for my $filter (@$filters) {
303 6 100       25 if (ref($filter) eq 'CODE') {
    100          
    100          
    50          
304 1         4 $val = $filter->($val);
305             } elsif ($filter eq 'trim') {
306 1         4 $val =~ s/\A\s+//s;
307 1         5 $val =~ s/\s+\z//s;
308             } elsif ($filter eq 'ltrim') {
309 2         9 $val =~ s/\s+\z//s;
310             } elsif ($filter eq 'rtrim') {
311 2         8 $val =~ s/\A\s+//s;
312             } else {
313 0         0 return [400, "Unknown filter '$filter' ".
314             "for argument '$arg_name'"];
315             }
316             }
317 5 50       21 $args->{$arg_name} = $val if @$filters;
318             }
319              
320 40 50       86 if (my $schema = $arg_spec->{schema}) {
321 40   100     128 my $coerce_to = $arg_spec->{'x.perl.coerce_to'} // '';
322 40 100       132 if ($schema->[0] eq 'obj') {
    100          
    100          
323 7   50     35 my $class = $schema->[1]{isa} // '';
324             # convert DateTime object from epoch/some formatted string
325 6 100       21 if ($class eq 'DateTime') {
    100          
    50          
326 2         6 my $coerce_res = _coerce_to_datetime($args, $arg_name);
327 2 50       10 return $coerce_res unless $coerce_res->[0] == 200;
328             } elsif ($class eq 'DateTime::Duration') {
329 2         6 my $coerce_res = _coerce_to_datetime_duration($args, $arg_name);
330 2 50       10 return $coerce_res unless $coerce_res->[0] == 200;
331             } elsif ($class eq 'Time::Moment') {
332 2         7 my $coerce_res = _coerce_to_time_moment($args, $arg_name);
333 2 50       10 return $coerce_res unless $coerce_res->[0] == 200;
334             }
335             } elsif ($schema->[0] eq 'date') {
336 16 100       49 if ($coerce_to eq 'DateTime') {
    100          
    100          
337 4         8 my $coerce_res = _coerce_to_datetime($args, $arg_name);
338 4 50       18 return $coerce_res unless $coerce_res->[0] == 200;
339             } elsif ($coerce_to eq 'Time::Moment') {
340 4         8 my $coerce_res = _coerce_to_time_moment($args, $arg_name);
341 4 50       18 return $coerce_res unless $coerce_res->[0] == 200;
342             } elsif ($coerce_to eq 'int(epoch)') {
343 4         10 my $coerce_res = _coerce_to_epoch($args, $arg_name);
344 4 50       19 return $coerce_res unless $coerce_res->[0] == 200;
345             }
346             } elsif ($schema->[0] eq 'duration') {
347 12 100       33 if ($coerce_to eq 'DateTime::Duration') {
    100          
348 4         10 my $coerce_res = _coerce_to_datetime_duration($args, $arg_name);
349 4 50       18 return $coerce_res unless $coerce_res->[0] == 200;
350             } elsif ($coerce_to eq 'int(secs)') {
351 4         10 my $coerce_res = _coerce_to_secs($args, $arg_name);
352 4 50       18 return $coerce_res unless $coerce_res->[0] == 200;
353             }
354             }
355             } # has schema
356             }
357              
358 39         226 [200, "OK", $args];
359             }
360              
361             1;
362             # ABSTRACT: Coerce arguments
363              
364             __END__