File Coverage

blib/lib/Perinci/Sub/Gen/AccessTable.pm
Criterion Covered Total %
statement 558 600 93.0
branch 350 442 79.1
condition 208 284 73.2
subroutine 28 28 100.0
pod 1 1 100.0
total 1145 1355 84.5


line stmt bran cond sub pod time code
1              
2             use 5.010001;
3 2     2   10955 use strict;
  2         14  
4 2     2   11 use warnings;
  2         4  
  2         39  
5 2     2   9 use experimental 'smartmatch';
  2         5  
  2         50  
6 2     2   9 use Log::ger;
  2         14  
  2         11  
7 2     2   1818  
  2         53  
  2         12  
8             use Exporter 'import';
9 2     2   501 use Function::Fallback::CoreOrPP qw(clone);
  2         5  
  2         70  
10 2     2   983 use List::Util qw(shuffle);
  2         1389  
  2         129  
11 2     2   14 use Locale::Set qw(:locale_h setlocale);
  2         4  
  2         126  
12 2     2   864 use Locale::TextDomain::UTF8 'Perinci-Sub-Gen-AccessTable';
  2         4424  
  2         365  
13 2     2   980 use Perinci::Object::Metadata;
  2         42684  
  2         17  
14 2     2   60771 use Perinci::Sub::Gen;
  2         1968  
  2         68  
15 2     2   849 use Perinci::Sub::Util qw(err);
  2         338  
  2         74  
16 2     2   1052 #use String::Trim::More qw(trim_blank_lines);
  2         4989  
  2         10605  
17              
18             our $AUTHORITY = 'cpan:PERLANCAR'; # AUTHORITY
19             our $DATE = '2022-11-02'; # DATE
20             our $DIST = 'Perinci-Sub-Gen-AccessTable'; # DIST
21             our $VERSION = '0.588'; # VERSION
22              
23             our @EXPORT_OK = qw(gen_read_table_func);
24              
25             our %SPEC;
26              
27             require Data::Sah::Normalize;
28             Data::Sah::Normalize::normalize_schema($_[0]);
29 251     251   1907 }
30 251         3949  
31             my $data = shift;
32             ref($data) eq 'ARRAY' && (!@$data || ref($data->[0]) eq 'ARRAY');
33             }
34 215     215   393  
35 215 100 100     1669 my $data = shift;
36             ref($data) eq 'ARRAY' && (!@$data || ref($data->[0]) eq 'HASH');
37             }
38              
39 137     137   249 my ($arg, $func_meta) = @_;
40 137 100 33     844 my $args = $func_meta->{args};
41             return 0 unless $args && $args->{$arg};
42             my $tags = $args->{$arg}{tags};
43             return 0 unless $tags;
44 12     12   34 for my $tag (@$tags) {
45 12         31 next unless ref($tag) eq 'HASH';
46 12 50 33     60 return 1 if $tag->{name} =~ /^category:filtering/;
47 12         27 }
48 12 50       31 0;
49 12         31 }
50 12 50       40  
51 12 50       86 my %args = @_;
52              
53 0         0 my $arg_name = $args{name};
54             my $fname = $args{name}; $fname =~ s/\..+//;
55             my $func_meta = $args{func_meta};
56             my $langs = $args{langs};
57 1888     1888   16879  
58             die "BUG: Duplicate arg $arg_name" if $func_meta->{args}{$arg_name};
59 1888         3661  
60 1888         3216 my $tag = {name=>"category:$args{cat_name}"};
  1888         7447  
61 1888         3635 my $schema = ref($args{type}) eq 'ARRAY' ? $args{type} :
62 1888         2927 [$args{type} => {}];
63             $schema->[1] //= {};
64 1888 50       4702 $schema->[1]{default} = $args{default} if defined($args{default});
65             my $arg_spec = {
66 1888         5704 schema => $schema,
67             tags => [$tag],
68 1888 100       5985 };
69 1888   50     4500  
70 1888 100       4041 if ($args{aliases}) {
71 1888         4769 $arg_spec->{cmdline_aliases} = $args{aliases};
72             }
73              
74             if (defined $args{pos}) {
75             $arg_spec->{pos} = $args{pos};
76 1888 100       4067 }
77 66         141  
78             if (defined $args{slurpy}) {
79             $arg_spec->{slurpy} = $args{slurpy};
80 1888 100       3832 }
81 32         70  
82             if ($args{extra_props}) {
83             for (keys %{ $args{extra_props} }) {
84 1888 100       3624 $arg_spec->{$_} = $args{extra_props}{$_};
85 32         63 }
86             }
87              
88 1888 100       3940 # translation args
89 66         109 my %xargs = (field => $fname);
  66         216  
90 99         230  
91             my $orig_locale = setlocale(LC_ALL);
92              
93             for my $prop (qw/summary description/) {
94             next unless defined $args{$prop};
95 1888         4468 #$args{$prop} = trim_blank_lines($args{$prop});
96             for my $lang (@$langs) {
97 1888         4753 setlocale(LC_ALL, $lang) or warn "Can't setlocale $lang";
98             my $isdeflang = $lang eq 'en_US';
99 1888         14021 my $k = $prop . ($isdeflang ? '' : ".alt.lang.$lang");
100 3776 100       110030 $arg_spec->{$k} = __x($args{$prop}, %xargs);
101             }
102 1989         3682 }
103 1989 50       4091  
104 1989         104816 for my $lang (@$langs) {
105 1989 50       5861 for my $prop (qw/summary/) {
106 1989         9106 setlocale(LC_ALL, $lang) or warn "Can't setlocale $lang";
107             my $isdeflang = $lang eq 'en_US';
108             my $k = $prop . ($isdeflang ? '' : ".alt.lang.$lang");
109             $tag->{$k} = __x($args{cat_text}, %xargs);
110 1888         8524 }
111 1888         3164 }
112 1888 50       4898  
113 1888         88283 setlocale(LC_ALL, $orig_locale);
114 1888 50       5649 $func_meta->{args}{$arg_name} = $arg_spec;
115 1888         8353 }
116              
117             require Data::Sah::Resolve;
118              
119 1888         99266 my ($table_spec, $opts) = @_;
120 1888         42333 my $langs = $opts->{langs};
121              
122             my $fields = $table_spec->{fields};
123              
124 34     34   1211 # add general arguments
125              
126 34         2877 my $func_meta = {
127 34         88 v => 1.1,
128             summary => $opts->{summary} // $table_spec->{summary} // "REPLACE ME",
129 34         80 description => $opts->{description} // "REPLACE ME",
130             args => {},
131             result => {
132             table => {
133             spec => $table_spec,
134             },
135             },
136 34   33     569 'x.dynamic_generator_modules' => [__PACKAGE__],
      50        
      50        
137             };
138              
139             my $func_args = $func_meta->{args};
140              
141             _add_arg(
142             func_meta => $func_meta,
143             langs => $langs,
144             name => 'with_field_names',
145             type => 'bool',
146 34         81 default => $opts->{default_with_field_names},
147             aliases => $opts->{with_field_names_aliases},
148             cat_name => 'field-selection',
149             cat_text => N__('field selection'),
150             summary => N__('Return field names in each record (as hash/'.
151             'associative array)'),
152             description => N__(<<'_',
153              
154             When enabled, function will return each record as hash/associative array
155 34         248 (field name => value pairs). Otherwise, function will return each record
156             as list/array (field value, field value, ...).
157              
158             _
159             ));
160             _add_arg(
161             func_meta => $func_meta,
162             langs => $langs,
163             name => 'detail',
164             type => 'bool',
165             default => $opts->{default_detail} // 0,
166             aliases => $opts->{detail_aliases},
167             cat_name => 'field-selection',
168             cat_text => N__('field selection'),
169             summary => N__('Return array of full records instead of '.
170             'just ID fields'),
171             description => N__(<<'_',
172              
173             By default, only the key (ID) field is returned per result entry.
174 34   100     301  
175             _
176             ));
177             _add_arg(
178             func_meta => $func_meta,
179             langs => $langs,
180             name => 'fields',
181             type => ['array*' => {of=>['str*', in=>[keys %{$table_spec->{fields}}]]}],
182             default => $opts->{default_fields},
183             aliases => $opts->{fields_aliases},
184             cat_name => 'field-selection',
185             cat_text => N__('field selection'),
186             summary => N__('Select fields to return'),
187             extra_props => {
188 33         307 'x.name.is_plural' => 1,
189             },
190             ) if $opts->{enable_field_selection};
191             _add_arg(
192             func_meta => $func_meta,
193             langs => $langs,
194             name => 'exclude_fields',
195             type => ['array*' => {of=>['str*', in=>[keys %{$table_spec->{fields}}]]}],
196             default => $opts->{default_exclude_fields},
197 34 100       189 aliases => $opts->{exclude_fields_aliases},
198             cat_name => 'field-selection',
199             cat_text => N__('field selection'),
200             summary => N__('Select fields to return'),
201             extra_props => {
202 33         312 'x.name.is_plural' => 1,
203             'x.name.singular' => 'exclude_field',
204             },
205             ) if $opts->{enable_field_selection};
206             _add_arg(
207             func_meta => $func_meta,
208             langs => $langs,
209             name => 'sort',
210             type => ['array*', {of=>['str*', {in=>[
211             map {($_, "-$_")} grep {$fields->{$_}{sortable}} sort keys %$fields,
212 34 100       258 ]}]}],
213             default => $opts->{default_sort},
214             aliases => $opts->{sort_aliases},
215             cat_name => 'ordering',
216             cat_text => N__('ordering'),
217             summary => N__('Order records according to certain field(s)'),
218 0         0 description => N__(<<'_',
  241         620  
219              
220             A list of field names separated by comma. Each field can be prefixed with '-' to
221             specify descending order instead of the default ascending.
222              
223             _
224             )) if $opts->{enable_ordering};
225             _add_arg(
226             func_meta => $func_meta,
227             langs => $langs,
228             name => 'random',
229             type => 'bool',
230             default => $opts->{default_random} // 0,
231 34 100       450 aliases => $opts->{random_aliases},
232             cat_name => 'ordering',
233             cat_text => N__('ordering'),
234             summary => N__('Return records in random order'),
235             ) if $opts->{enable_ordering} && $opts->{enable_random_ordering};
236             _add_arg(
237             func_meta => $func_meta,
238             langs => $langs,
239             name => 'result_limit',
240             type => 'int',
241             default => $opts->{default_result_limit},
242 34 100 100     431 aliases => $opts->{result_limit_aliases},
      100        
243             cat_name => 'paging',
244             cat_text => N__('paging'),
245             summary => N__('Only return a certain number of records'),
246             ) if $opts->{enable_paging};
247             _add_arg(
248             func_meta => $func_meta,
249             langs => $langs,
250             name => 'result_start',
251             aliases => $opts->{result_start_aliases},
252             type => 'int',
253 34 100       248 default => 1,
254             cat_name => 'paging',
255             cat_text => N__('paging'),
256             summary => N__("Only return starting from the n'th record"),
257             ) if $opts->{enable_paging};
258             _add_arg(
259             func_meta => $func_meta,
260             langs => $langs,
261             name => 'queries',
262             aliases => $opts->{queries_aliases},
263             type => ['array*', of=>'str*'],
264 34 100       250 cat_name => 'filtering',
265             cat_text => N__('filtering'),
266             summary => N__("Search"),
267             pos => 0,
268             slurpy => 1,
269             ) if $opts->{enable_filtering} && $opts->{enable_search};
270              
271             # add filter arguments for each table field
272              
273             for my $fname (keys %{$table_spec->{fields}}) {
274             my $fspec = $table_spec->{fields}{$fname};
275             my $fschema = $fspec->{schema};
276 34 100 100     386 my $frschema = Data::Sah::Resolve::resolve_schema($fschema);
277             my $ftype = $frschema->{type};
278              
279             next unless $opts->{enable_filtering};
280 34         101 next if defined($fspec->{filterable}) && !$fspec->{filterable};
  34         159  
281 249         567  
282 249         477 unless ($fspec->{include_by_default} // 1) {
283 249         662 _add_arg(
284 249         107192 func_meta => $func_meta,
285             langs => $langs,
286 249 100       701 name => "with.$fname",
287 241 100 66     888 type => "bool",
288             default => 0,
289 212 100 100     941 cat_name => "field-selection",
290 1         7 cat_text => N__('field selection'),
291             summary => N__("Show field '{field}'"),
292             );
293             }
294             _add_arg(
295             func_meta => $func_meta,
296             langs => $langs,
297             name => "$fname.is",
298             type => "$ftype*",
299             default => $opts->{"default_$fname.is"},
300             cat_name => "filtering-for-$fname",
301             cat_text => N__("filtering for {field}"),
302             summary => N__("Only return records where the '{field}' field ".
303             "equals specified value"),
304             );
305             unless ($func_args->{$fname}) {
306 212         1212 $func_args->{$fname} =
307             clone($func_args->{"$fname.is"});
308             }
309             _add_arg(
310             func_meta => $func_meta,
311             langs => $langs,
312 212 50       886 name => "$fname.isnt",
313             type => "$ftype*",
314 212         847 default => $opts->{"default_$fname.isnt"},
315             cat_name => "filtering-for-$fname",
316             cat_text => N__("filtering for {field}"),
317             summary => N__("Only return records where the '{field}' field ".
318             "does not equal specified value"),
319             );
320              
321 212         5095 # .in & .not_in should be applicable to arrays to, but it is currently
322             # implemented with perl's ~~ which can't handle this transparently. as
323             # for bool, it's not that important.
324             unless ($ftype ~~ [qw/array bool/]) {
325             _add_arg(
326             func_meta => $func_meta,
327             langs => $langs,
328             name => "$fname.in",
329             type => ['array*' => {of => "$ftype*"}],
330             cat_name => "filtering-for-$fname",
331 212 100       1335 cat_text => N__("filtering for {field}"),
332 153         856 summary => N__("Only return records where the '{field}' field ".
333             "is in the specified values"),
334             );
335             _add_arg(
336             func_meta => $func_meta,
337             langs => $langs,
338             name => "$fname.not_in",
339             type => ['array*' => {of => "$ftype*"}],
340             cat_name => "filtering-for-$fname",
341             cat_text => N__("filtering for {field}"),
342 153         1009 summary => N__("Only return records where the '{field}' field ".
343             "is not in the specified values"),
344             );
345             }
346             if ($ftype eq 'array') {
347             _add_arg(
348             func_meta => $func_meta,
349             langs => $langs,
350             name => "$fname.has",
351             type => [array => {of=>'str*'}],
352             default => $opts->{"default_$fname.has"},
353 212 100       812 cat_name => "filtering-for-$fname",
354             cat_text => N__("filtering for {field}"),
355             summary => N__("Only return records where the '{field}' field ".
356             "is an array/list which contains specified value"),
357             );
358             _add_arg(
359 30         278 func_meta => $func_meta,
360             langs => $langs,
361             name => "$fname.lacks",
362             type => [array => {of=>'str*'}],
363             default => $opts->{"default_$fname.lacks"},
364             cat_name => "filtering-for-$fname",
365             cat_text => N__("filtering for {field}"),
366             summary => N__("Only return records where the '{field}' field ".
367             "is an array/list which does not contain specified value"),
368             );
369             }
370 30         332 if ($ftype =~ /^(?:int|float|str|date)$/) { # XXX all Comparable types
371             _add_arg(
372             func_meta => $func_meta,
373             langs => $langs,
374             name => "$fname.min",
375             type => $ftype,
376             default => $opts->{"default_$fname.min"},
377 212 100       1064 cat_name => "filtering-for-$fname",
378             cat_text => N__("filtering for {field}"),
379             summary => N__("Only return records where the '{field}' field ".
380             "is greater than or equal to specified value"),
381             );
382             _add_arg(
383 153         929 func_meta => $func_meta,
384             langs => $langs,
385             name => "$fname.max",
386             type => $ftype,
387             default => $opts->{"default_$fname.max"},
388             cat_name => "filtering-for-$fname",
389             cat_text => N__("filtering for {field}"),
390             summary => N__("Only return records where the '{field}' field ".
391             "is less than or equal to specified value"),
392             );
393             _add_arg(
394 153         982 func_meta => $func_meta,
395             langs => $langs,
396             name => "$fname.xmin",
397             type => $ftype,
398             default => $opts->{"default_$fname.xmin"},
399             cat_name => "filtering-for-$fname",
400             cat_text => N__("filtering for {field}"),
401             summary => N__("Only return records where the '{field}' field ".
402             "is greater than specified value"),
403             );
404             _add_arg(
405 153         961 func_meta => $func_meta,
406             langs => $langs,
407             name => "$fname.xmax",
408             type => $ftype,
409             default => $opts->{"default_$fname.xmax"},
410             cat_name => "filtering-for-$fname",
411             cat_text => N__("filtering for {field}"),
412             summary => N__("Only return records where the '{field}' field ".
413             "is less than specified value"),
414             );
415             }
416 153         959 if ($ftype eq 'str') {
417             _add_arg(
418             func_meta => $func_meta,
419             langs => $langs,
420             name => "$fname.contains",
421             type => $ftype,
422             default => $opts->{"default_$fname.contains"},
423 212 100       1237 cat_name => "filtering-for-$fname",
424             cat_text => N__("filtering for {field}"),
425             summary => N__("Only return records where the '{field}' field ".
426             "contains specified text"),
427             );
428             _add_arg(
429 64         440 func_meta => $func_meta,
430             langs => $langs,
431             name => "$fname.not_contains",
432             type => $ftype,
433             default => $opts->{"default_$fname.not_contains"},
434             cat_name => "filtering-for-$fname",
435             cat_text => N__("filtering for {field}"),
436             summary => N__("Only return records where the '{field}' field ".
437             "does not contain specified text"),
438             );
439             if ($fspec->{filterable_regex}) {
440 64         483 _add_arg(
441             func_meta => $func_meta,
442             langs => $langs,
443             name => "$fname.matches",
444             type => $ftype,
445             default => $opts->{"default_$fname.matches"},
446 64 100       454 cat_name => "filtering-for-$fname",
447             cat_text => N__("filtering for {field}"),
448             summary => N__("Only return records where the '{field}' field ".
449             "matches specified regular expression pattern"),
450             );
451             _add_arg(
452 30         242 func_meta => $func_meta,
453             langs => $langs,
454             name => "$fname.not_matches",
455             type => $ftype,
456             default => $opts->{"default_$fname.not_matches"},
457             cat_name => "filtering-for-$fname",
458             cat_text => N__("filtering for {field}"),
459             summary => N__("Only return records where the '{field}' field " .
460             "does not match specified regular expression"),
461             );
462             }
463 30         256 }
464             } # for each fspec
465              
466             # custom filters
467             if ($opts->{enable_filtering}) {
468             my $cff = $opts->{custom_filters} // {};
469             while (my ($cfn, $cf) = each %$cff) {
470             $func_args->{$cfn} and return [
471             400, "Custom filter '$cfn' clashes with another argument"];
472             $func_args->{$cfn} = $cf->{meta};
473             }
474 34 100       188 }
475 33   50     140  
476 33         193 # extra arguments
477 2 50       8 my $ea = $opts->{extra_args} // {};
478             $func_args->{$_} = $ea->{$_} for keys %$ea;
479 2         10  
480             # extra metadata properties
481             my $extra_props = $opts->{extra_props} // {};
482             $func_meta->{$_} = $extra_props->{$_} for keys %$extra_props;
483              
484 34   100     195 [200, "OK", $func_meta];
485 34         154 }
486              
487             require Data::Sah::Resolve;
488 34   100     175  
489 34         111 my ($table_spec, $opts, $func_meta, $args) = @_;
490             my $query = {args=>$args};
491 34         139  
492             my $fspecs = $table_spec->{fields};
493             # reminder: index property is for older spec, will be removed someday
494             my @fields = sort {($fspecs->{$a}{pos}//$fspecs->{$a}{index}) <=>
495 178     178   1094 ($fspecs->{$b}{pos}//$fspecs->{$b}{index})}
496             keys %$fspecs;
497 178         438  
498 178         486 my @requested_fields;
499             SELECT_FIELDS:
500 178         360 {
501             if ($args->{detail} || $args->{exclude_fields}) {
502 178         862 @requested_fields = grep {
503 2896   100     8800 ($fspecs->{$_}{include_by_default} // 1) ||
      100        
504             $args->{"with.$_"}
505             } @fields;
506 178         420 if ($args->{exclude_fields}) {
507             my @filtered_fields;
508             for my $field (@requested_fields) {
509 178 100 100     290 next if grep { $field eq $_ } @{ $args->{exclude_fields} };
  178 100       888  
510             push @filtered_fields, $field;
511 9         22 }
512 62 100 100     221 @requested_fields = @filtered_fields;
513             }
514 9 100       48 $args->{with_field_names} //= $args->{detail} ? 1:0;
515 2         3 } elsif ($args->{fields}) {
516 2         7 @requested_fields = @{ $args->{fields} };
517 16 100       19 $args->{with_field_names} //= 0;
  80         151  
  16         33  
518 6         13 } else {
519             @requested_fields = ($table_spec->{pk});
520 2         5 $args->{with_field_names} //= 0;
521             }
522 9 50 66     42 } # SELECT_FIELDS
523              
524 6         15 for (@requested_fields) {
  6         21  
525 6   100     25 return err(400, "Unknown field $_") unless $_ ~~ @fields;
526             }
527 163         446 $query->{requested_fields} = \@requested_fields;
528 163   50     609  
529             my @filter_fields;
530             my @filters; # ([field, field-type, operator, operand...])
531             my %frschemas;
532 178         386  
533 225 100       778 for my $f (@fields) {
534             $frschemas{$f} = Data::Sah::Resolve::resolve_schema($fspecs->{$f}{schema});
535 177         383 }
536              
537 177         464 my $cic = $opts->{case_insensitive_comparison};
538              
539 177         0 for my $f (grep {$frschemas{$_}{type} eq 'bool'} @fields) {
540             my $fspec = $fspecs->{$f};
541 177         350 my $ftype = $frschemas{$f}{type};
542 1377         157934 my $exists;
543             if (defined $args->{"$f.is"}) {
544             $exists++;
545 177         22821 push @filters, [$f, $ftype, "truth", $args->{"$f.is"}, $cic];
546             } elsif (defined $args->{"$f.isnt"}) {
547 177         349 $exists++;
  1377         3165  
548 170         334 push @filters, [$f, $ftype, "truth", !$args->{"$f.isnt"}, $ftype, $cic];
549 170         299 } elsif (defined($args->{$f}) && __is_filter_arg($f, $func_meta)) {
550 170         266 $exists++;
551 170 100 66     1058 push @filters, [$f, $ftype, "truth", $args->{$f}, $ftype, $cic];
    50          
    100          
552 2         5 }
553 2         7 push @filter_fields, $f if $exists && !($f ~~ @filter_fields);
554             }
555 0         0  
556 0         0 for my $f (grep {$frschemas{$_}{type} eq 'array'} @fields) {
557             my $fspec = $fspecs->{$f};
558 6         17 my $ftype = $frschemas{$f}{type};
559 6         24 my $exists;
560             if (defined $args->{"$f.has"}) {
561 170 100 66     553 $exists++;
562             push @filters, [$f, $ftype, "~~", $args->{"$f.has"}, $ftype, $cic];
563             }
564 177         332 if (defined $args->{"$f.lacks"}) {
  1377         2646  
565 172         337 $exists++;
566 172         294 push @filters, [$f, $ftype, "!~~", $args->{"$f.lacks"}, $ftype, $cic];
567 172         273 }
568 172 100       467 push @filter_fields, $f if $exists && !($f ~~ @filter_fields);
569 2         9 }
570 2         16  
571             for my $f (grep {!($frschemas{$_}{type} ~~ ['array','bool'])} @fields) {
572 172 100       403 my $fspec = $fspecs->{$f};
573 2         4 my $ftype = $frschemas{$f}{type};
574 2         10 my $exists;
575             if (defined $args->{"$f.in"}) {
576 172 100 66     525 $exists++;
577             push @filters, [$f, $ftype, "in", $args->{"$f.in"}, $cic];
578             }
579 177         303 if (defined $args->{"$f.not_in"}) {
  1377         4208  
580 1035         1615 $exists++;
581 1035         1603 push @filters, [$f, $ftype, "not_in", $args->{"$f.not_in"}, $cic];
582 1035         1424 }
583 1035 100       2175 }
584 3         7  
585 3         13 for my $f (grep {$frschemas{$_}{type} =~ /^(int|float|str|date)$/}
586             @fields) { # XXX all Comparable
587 1035 100       2393 my $fspec = $fspecs->{$f};
588 3         20 my $ftype = $frschemas{$f}{type};
589 3         19 my $exists;
590             if (defined $args->{"$f.is"}) {
591             $exists++;
592             push @filters,
593 177         329 [$f, $ftype, "==", $args->{"$f.is"}, $cic];
  1377         3864  
594             } elsif (defined($args->{$f}) && __is_filter_arg($f, $func_meta)) {
595 1035         1627 $exists++;
596 1035         1645 push @filters, [$f, $ftype, "==", $args->{$f}, $cic];
597 1035         1431 }
598 1035 100 66     3288 if (defined $args->{"$f.isnt"}) {
    100          
599 2         7 $exists++;
600             push @filters,
601 2         10 [$f, $ftype, "!=", $args->{"$f.isnt"}, $cic];
602             } elsif (defined($args->{$f}) && __is_filter_arg($f, $func_meta)) {
603 3         8 $exists++;
604 3         16 push @filters, [$f, $ftype, "==", $args->{$f}, $cic];
605             }
606 1035 100 66     3206 if (defined $args->{"$f.min"}) {
    100          
607 2         5 $exists++;
608             push @filters, [$f, $ftype, '>=', $args->{"$f.min"}, $cic];
609 2         8 }
610             if (defined $args->{"$f.max"}) {
611 3         7 $exists++;
612 3         12 push @filters, [$f, $ftype, '<=', $args->{"$f.max"}, $cic];
613             }
614 1035 100       2127 if (defined $args->{"$f.xmin"}) {
615 7         22 $exists++;
616 7         34 push @filters, [$f, $ftype, '>', $args->{"$f.xmin"}, $cic];
617             }
618 1035 100       2068 if (defined $args->{"$f.xmax"}) {
619 4         14 $exists++;
620 4         21 push @filters, [$f, $ftype, '<', $args->{"$f.xmax"}, $cic];
621             }
622 1035 100       2134 push @filter_fields, $f if $exists && !($f ~~ @filter_fields);
623 2         5 }
624 2         8  
625             for my $f (grep {$frschemas{$_}{type} =~ /^str$/} @fields) {
626 1035 100       2074 my $fspec = $fspecs->{$f};
627 2         5 my $ftype = $frschemas{$f}{type};
628 2         8 my $exists;
629             if (defined $args->{"$f.contains"}) {
630 1035 100 66     2416 $exists++;
631             push @filters, [$f, $ftype, 'pos', $args->{"$f.contains"}, $cic];
632             }
633 177         348 if (defined $args->{"$f.not_contains"}) {
  1377         3219  
634 521         862 $exists++;
635 521         807 push @filters, [$f, $ftype, '!pos', $args->{"$f.not_contains"}, $cic];
636 521         734 }
637 521 100       1166 if (defined $args->{"$f.matches"}) {
638 1         3 $exists++;
639 1         4 push @filters, [$f, $ftype, '=~', $args->{"$f.matches"}, $cic];
640             }
641 521 100       1112 if (defined $args->{"$f.not_matches"}) {
642 1         2 $exists++;
643 1         5 push @filters, [$f, $ftype, '!~', $args->{"$f.not_matches"}, $cic];
644             }
645 521 100       1149 push @filter_fields, $f if $exists && !($f ~~ @filter_fields);
646 1         8 }
647 1         6 $query->{filters} = \@filters;
648             $query->{filter_fields} = \@filter_fields;
649 521 100       1141  
650 1         3 my $cff = $opts->{custom_filters} // {};
651 1         6 while (my ($cfn, $cf) = each %$cff) {
652             next unless defined $args->{$cfn};
653 521 100 66     1255 push @filters, [$cf->{fields}, undef, 'call', [$cf->{code}, $args->{$cfn}], $cic];
654             for (@{$cf->{fields} // []}) {
655 177         376 push @filter_fields, $_ if !($_ ~~ @filter_fields);
656 177         363 }
657             }
658 177   50     491  
659 177         613 my $search_re;
660 6 100       21 my $search_opts = {ci => $opts->{case_insensitive_search}};
661 4         18 my @searchable_fields = grep {
662 4   50     9 !defined($fspecs->{$_}{searchable}) || $fspecs->{$_}{searchable}
  4         14  
663 4 50       27 } @fields;
664             my $q = $args->{query}; # old, still supported
665             my $qq = $args->{queries};
666             my @qq = defined $qq && @$qq ? @$qq : defined $q ? ($q) : ();
667 177         291 if (@qq) {
668 177         486 require String::Query::To::Regexp;
669             $search_re = String::Query::To::Regexp::query2re(
670 177         395 {
671 1377 100       3567 word => $opts->{word_search},
672 177         350 bool => $args->{query_boolean} // $opts->{default_query_boolean} // 'and',
673 177         288 ci => $opts->{case_insensitive_search},
674 177 100 66     623 },
    100          
675 177 100       423 @qq);
676 11         775 }
677             $query->{queries} = @qq ? \@qq : undef;
678             $query->{search_opts} = $args->{search_opts};
679             unless ($opts->{custom_search}) {
680             $query->{search_fields} = \@searchable_fields;
681             $query->{search_str_fields} = [grep {
682             $frschemas{$_}{type} =~ /^(str)$/
683 11   33     894 } @searchable_fields];
      50        
684             $query->{search_array_fields} = [grep {
685 177 100       1247 $frschemas{$_}{type} =~ /^(array)$/
686 177         344 } @searchable_fields];
687 177 100       412 $query->{search_re} = $search_re;
688 176         354 }
689              
690 176         320 my @sort_fields;
  1367         3602  
691             my @sorts;
692             if (defined $args->{sort}) {
693 176         360 for my $f (@{ $args->{sort} }) {
  1367         2911  
694             my $desc = $f =~ s/^-//;
695 176         393 return err(400, "Unknown field in sort: $f")
696             unless $f ~~ @fields;
697             my $fspec = $fspecs->{$f};
698 177         342 my $ftype = $frschemas{$f}{type};
699             return err(400, "Field $f is not sortable")
700 177 100       498 unless !defined($fspec->{sortable}) || $fspec->{sortable};
701 8         17 my $op = $ftype =~ /^(int|float)$/ ? '<=>' : 'cmp';
  8         23  
702 9         28 #print "ftype=$ftype, op=$op\n";
703 9 100       42 push @sorts, [$f, $op, $desc ? -1:1];
704             push @sort_fields, $f;
705 8         19 }
706 8         18 }
707             $query->{random} = $args->{random};
708 8 50 66     34 $query->{sorts} = \@sorts;
709 7 50       29 $query->{sort_fields} = \@sort_fields;
710              
711 7 100       30 my @mentioned_fields =
712 7         21 keys %{{ map {$_=>1} @requested_fields,
713             @filter_fields, @sort_fields }};
714             $query->{mentioned_fields} = \@mentioned_fields;
715 175         347  
716 175         354 $query->{result_limit} = $args->{result_limit};
717 175         361 $query->{result_start} = $args->{result_start} // 1;
718              
719             log_trace("parsed query: %s", $query);
720 175         294 [200, "OK", $query];
  175         342  
  271         1225  
721             }
722 175         542  
723             my ($table_spec, $opts, $table_data, $func_meta) = @_;
724 175         338  
725 175   50     687 my $fspecs = $table_spec->{fields};
726             my $func_args = $func_meta->{args};
727 175         731 my $func = sub {
728 175         2914 my %args = @_;
729             my $hooks = $opts->{hooks};
730             my %hookargs = %args;
731             $hookargs{_func_args} = \%args;
732 34     34   117  
733             # XXX schema
734 34         84 while (my ($ak, $av) = each %$func_args) {
735 34         72 if (ref($av->{schema}) && ref($av->{schema}[1]) &&
736             defined($av->{schema}[1]{default})) {
737 179     179   399107 $args{$ak} //= $av->{schema}[1]{default};
738 179         422 }
739 179         419 # array-ize "string,with,comma"
740 179         440 if ($ak =~ /\A(exclude_fields|fields)\z/ && defined($args{$ak})) {
741             $args{$ak} = [split /\s*[,;]\s*/, $args{$ak}]
742             unless ref($args{$ak}) eq 'ARRAY';
743 179         768 }
744 11936 100 66     53202 }
      100        
745              
746 549   100     1688 for ('before_parse_query') {
747             last unless $hooks->{$_};
748             $hookargs{_stage} = $_;
749 11936 100 100     41022 my $hres = $hooks->{$_}->(%hookargs);
750             return $hres if ref($hres);
751 9 100       84 }
752             my $query;
753             {
754             my $res = __parse_query($table_spec, $opts, $func_meta, \%args);
755 179         429 for ('after_parse_query') {
756 179 100       521 $hookargs{_parse_res} = $res;
757 2         6 last unless $hooks->{$_};
758 2         13 $hookargs{_stage} = $_;
759 2 100       53 my $hres = $hooks->{$_}->(%hookargs);
760             return $hres if ref($hres);
761 178         291 }
762             return $res unless $res->[0] == 200;
763 178         273 $query = $res->[2];
  178         476  
764 178         664 }
765 178         355  
766 178 100       475 # retrieve data
767 1         4 my $data;
768 1         5 my $metadata = {};
769 1 50       12 for ('before_fetch_data') {
770             $hookargs{_query} = $query;
771 178 100       461 last unless $hooks->{$_};
772 175         330 $hookargs{_stage} = $_;
773             my $hres = $hooks->{$_}->(%hookargs);
774             return $hres if ref($hres);
775             }
776 175         273 if (__is_aoa($table_data) || __is_aoh($table_data)) {
777 175         320 $data = $table_data;
778 175         335 } elsif (ref($table_data) eq 'CODE') {
779 175         342 my $res;
780 175 100       397 return err(500, "BUG: Table data function died: $@")
781 1         3 unless eval { $res = $table_data->($query) };
782 1         4 return err(500, "BUG: Result returned from table data function ".
783 1 50       11 "is not a hash") unless ref($res) eq 'HASH';
784             $data = $res->{data};
785 175 100 100     427 return err(500, "BUG: 'data' key from table data function ".
    50          
786 173         335 "is not an AoA/AoH")
787             unless __is_aoa($data) || __is_aoh($data);
788 2         4 for (qw/filtered sorted paged fields_selected/) {
789             $metadata->{$_} = $res->{$_};
790 2 50       5 }
  2         7  
791 2 50       15 } else {
792             # this should be impossible, already checked earlier
793 2         4 die "BUG: 'data' from table data function is not an array";
794 2 50 33     5 }
795             for ('after_fetch_data') {
796             $hookargs{_data} = $data;
797 2         7 last unless $hooks->{$_};
798 8         20 $hookargs{_stage} = $_;
799             my $hres = $hooks->{$_}->(%hookargs);
800             return $hres if ref($hres);
801             }
802 0         0  
803             # this will be the final result.
804 175         380 my @r;
805 175         365  
806 175 100       390 no warnings; # silence undef warnings when comparing record values
807 1         3  
808 1         6 log_trace("(read_table_func) Filtering ...");
809 1 50       12 my $qq = $query->{queries};
810             my $search_re = $query->{search_re};
811              
812             if (grep { $_->[1] eq 'date' } @{ $query->{filters} }) {
813 175         268 require Data::Sah::Util::Type::Date;
814             Data::Sah::Util::Type::Date->import('coerce_date');
815 2     2   25 }
  2         4  
  2         3034  
816              
817 175         483 local $Data::Sah::Util::Type::Date::DATE_MODULE = 'Time::Moment'
818 175         537 if %Data::Sah::Util::Type::Date::;
819 175         291  
820             REC:
821 175 100       264 for my $r0 (@$data) {
  51         175  
  175         506  
822 7         749 my $r_h;
823 7         1714 if (ref($r0) eq 'ARRAY') {
824             # currently, internally we always use hashref for records and
825             # convert to array/scalar later when returning final data.
826 175 50       517 $r_h = {};
827             for my $f (keys %$fspecs) {
828             # reminder: index property is for older spec, will be
829             # removed someday
830 175         362 $r_h->{$f} = $r0->[$fspecs->{$f}{pos}//$fspecs->{$f}{index}];
831 689         1055 }
832 689 100       1540 } elsif (ref($r0) eq 'HASH') {
    50          
833             $r_h = { %$r0 };
834             } else {
835 218         379 return err(500, "BUG: Invalid record, not a hash/array");
836 218         594 }
837              
838             goto SKIP_FILTER if $metadata->{filtered};
839 1734   66     4974  
840             for my $filter (@{$query->{filters}}) {
841             my ($f, $ftype, $op, $opn, $cic) = @$filter;
842 471         2247 my $stringy = $ftype eq 'str' || $ftype eq 'cistr';
843             $cic = 1 if $ftype eq 'cistr';
844 0         0 if ($op eq 'truth') {
845             next REC if $r_h->{$f} xor $opn;
846             } elsif ($op eq '~~') {
847 689 100       1753 if ($stringy && $cic) {
848             my @vals = map {lc} @{$r_h->{$f}};
849 685         995 for (@$opn) {
  685         1340  
850 186         499 next REC unless lc($_) ~~ @vals;
851 186   66     596 }
852 186 50       397 } else {
853 186 100 100     2492 for (@$opn) {
    100 100        
    100 100        
    100 100        
    100 66        
    100 33        
    100 66        
    100 66        
    100 100        
    50 66        
    50 100        
    100 100        
    50 100        
    50 66        
    100 100        
    100 66        
    100 100        
    50 66        
    100 100        
    100 100        
    50 100        
    50 66        
    100 66        
    100 66        
    100 66        
    100          
    50          
    100          
    50          
    100          
    100          
    50          
    100          
    50          
    100          
    50          
854 28 100 100     152 next REC unless $_ ~~ @{$r_h->{$f}};
855             }
856 8 50 33     21 }
857 0         0 } elsif ($op eq '!~~') {
  0         0  
  0         0  
858 0         0 if ($stringy && $cic) {
859 0 0       0 my @vals = map {lc} @{$r_h->{$f}};
860             for (@$opn) {
861             next REC if lc($_) ~~ @vals;
862 8         15 }
863 10 100       17 } else {
  10         42  
864             for (@$opn) {
865             next REC if $_ ~~ @{$r_h->{$f}};
866             }
867 8 50 33     26 }
868 0         0 } elsif ($op eq 'in') {
  0         0  
  0         0  
869 0         0 if ($stringy && $cic) {
870 0 0       0 my @vals = map {lc} @$opn;
871             next REC unless lc($r_h->{$f}) ~~ @vals;
872             } else {
873 8         17 next REC unless $r_h->{$f} ~~ @$opn;
874 10 100       18 }
  10         42  
875             } elsif ($op eq 'not_in') {
876             if ($stringy && $cic) {
877             my @vals = map {lc} @$opn;
878 12 50 66     40 next REC if $r_h->{$f} ~~ @vals;
879 0         0 } else {
  0         0  
880 0 0       0 next REC if $r_h->{$f} ~~ @$opn;
881             }
882 12 100       89 } elsif ($op eq '==' && $stringy && $cic) {
883             next REC unless lc $r_h->{$f} eq lc $opn;
884             } elsif ($op eq '==' && $stringy) {
885 12 50 66     39 next REC unless $r_h->{$f} eq $opn;
886 0         0 } elsif ($op eq '==' && $ftype eq 'date') {
  0         0  
887 0 0       0 my $dopn = coerce_date($opn);
888             my $d = coerce_date($r_h->{$f});
889 12 100       57 next REC unless $dopn && $d;
890             next REC unless $d->compare($dopn) == 0;
891             } elsif ($op eq '==') {
892 3 100       17 next REC unless $r_h->{$f} == $opn;
893              
894 5 100       20 } elsif ($op eq '!=' && $stringy && $cic) {
895             next REC unless lc $r_h->{$f} ne lc $opn;
896 4         12 } elsif ($op eq '!=' && $stringy) {
897 4         116 next REC unless $r_h->{$f} ne $opn;
898 4 50 33     144 } elsif ($op eq '!=' && $ftype eq 'date') {
899 4 100       28 my $dopn = coerce_date($opn);
900             my $d = coerce_date($r_h->{$f});
901 10 100       42 next REC unless $dopn && $d;
902             next REC unless $d->compare($dopn) != 0;
903             } elsif ($op eq '!=') {
904 0 0       0 next REC unless $r_h->{$f} != $opn;
905              
906 0 0       0 } elsif ($op eq '>=' && $stringy && $cic) {
907             next REC unless lc $r_h->{$f} ge lc $opn;
908 8         25 } elsif ($op eq '>=' && $stringy) {
909 8         254 next REC unless $r_h->{$f} ge $opn;
910 8 100 66     244 } elsif ($op eq '>=' && $ftype eq 'date') {
911 4 100       28 my $dopn = coerce_date($opn);
912             my $d = coerce_date($r_h->{$f});
913 0 0       0 next REC unless $dopn && $d;
914             next REC unless $d->compare($dopn) >= 0;
915             } elsif ($op eq '>=') {
916 0 0       0 next REC unless $r_h->{$f} >= $opn;
917              
918 4 100       16 } elsif ($op eq '>' && $stringy && $cic) {
919             next REC unless lc $r_h->{$f} gt lc $opn;
920 4         13 } elsif ($op eq '>' && $stringy) {
921 4         117 next REC unless $r_h->{$f} gt $opn;
922 4 50 33     140 } elsif ($op eq '>' && $ftype eq 'date') {
923 4 100       29 my $dopn = coerce_date($opn);
924             my $d = coerce_date($r_h->{$f});
925 18 100       82 next REC unless $dopn && $d;
926             next REC unless $d->compare($dopn) > 0;
927             } elsif ($op eq '>' ) {
928 0 0       0 next REC unless $r_h->{$f} > $opn;
929              
930 4 100       20 } elsif ($op eq '<=' && $stringy && $cic) {
931             next REC unless lc $r_h->{$f} le lc $opn;
932 4         15 } elsif ($op eq '<=' && $stringy) {
933 4         117 next REC unless $r_h->{$f} le $opn;
934 4 50 33     141 } elsif ($op eq '<=' && $ftype eq 'date') {
935 4 100       30 my $dopn = coerce_date($opn);
936             my $d = coerce_date($r_h->{$f});
937 0 0       0 next REC unless $dopn && $d;
938             next REC unless $d->compare($dopn) <= 0;
939             } elsif ($op eq '<=') {
940 0 0       0 next REC unless $r_h->{$f} <= $opn;
941              
942 4 100       17 } elsif ($op eq '<' && $stringy) {
943             next REC unless lc $r_h->{$f} lt lc $opn;
944 4         15 } elsif ($op eq '<' && $stringy) {
945 4         2024 next REC unless $r_h->{$f} lt $opn;
946 4 50 33     179 } elsif ($op eq '<' && $ftype eq 'date') {
947 4 100       38 my $dopn = coerce_date($opn);
948             my $d = coerce_date($r_h->{$f});
949 8 100       36 next REC unless $dopn && $d;
950             next REC unless $d->compare($dopn) < 0;
951             } elsif ($op eq '<' ) {
952 4 100       25 next REC unless $r_h->{$f} < $opn;
953              
954 0 0       0 # XXX case-insensitive regex matching
955             } elsif ($op eq '=~') {
956 4         13 next REC unless $r_h->{$f} =~ $opn;
957 4         122  
958 4 50 33     148 # XXX case-insensitive regex negative matching
959 4 100       36 } elsif ($op eq '!~') {
960             next REC unless $r_h->{$f} !~ $opn;
961 0 0       0  
962             } elsif ($op eq 'pos' && $cic) {
963             next REC unless index(lc $r_h->{$f}, lc $opn) >= 0;
964             } elsif ($op eq 'pos') {
965 4 100       63 next REC unless index($r_h->{$f}, $opn) >= 0;
966              
967             } elsif ($op eq '!pos' && $cic) {
968             next REC if index(lc $r_h->{$f}, lc $opn) >= 0;
969 4 100       41 } elsif ($op eq '!pos') {
970             next REC if index($r_h->{$f}, $opn) >= 0;
971              
972 0 0       0 } elsif ($op eq 'call') {
973             next REC unless $opn->[0]->($r_h, $opn->[1]);
974 4 100       19  
975             } else {
976             die "BUG: Unknown op $op";
977 0 0       0 }
978             }
979 4 100       19  
980             if (defined $qq) {
981             if ($opts->{custom_search}) {
982 14 100       41 next REC unless $opts->{custom_search}->(
983             $r_h, $qq, $query->{search_opts});
984             } else {
985 0         0 my $match;
986             for my $f (@{$query->{search_str_fields}}) {
987             if ($r_h->{$f} =~ $search_re) {
988             $match++; last;
989 592 100       1244 }
990 40 100       98 }
991             ARY_FIELD:
992 4 100       12 for my $f (@{$query->{search_array_fields}}) {
993             for my $el (@{$r_h->{$f}}) {
994 36         53 if ($el =~ $search_re) {
995 36         60 $match++; last ARY_FIELD;
  36         67  
996 72 100       329 }
997 6         18 }
  6         12  
998             }
999             next REC unless $match;
1000             }
1001 36         69 }
  36         81  
1002 30         45  
  30         62  
1003 30 100       148 SKIP_FILTER:
1004 3         6  
  3         7  
1005             push @r, $r_h;
1006             }
1007              
1008 36 100       128 log_trace("(read_table_func) Ordering ...");
1009             if ($metadata->{sorted}) {
1010             # do nothing
1011             } elsif ($query->{random}) {
1012             @r = shuffle @r;
1013             } elsif (@{$query->{sorts}}) {
1014 567         1137 @r = sort {
1015             for my $s (@{$query->{sorts}}) {
1016             my ($f, $op, $desc) = @$s;
1017 175         603 my $x;
1018 175 50       828 if ($op eq 'cmp') {
    100          
    100          
1019             $x = $a->{$f} cmp $b->{$f};
1020             } else {
1021 100         714 $x = $a->{$f} <=> $b->{$f};
1022 75         216 }
1023             #print "$a->{$f} $op $b->{$f} = $x (desc=$desc)\n";
1024 6         28 return $x*$desc if $x != 0;
  29         43  
  29         60  
1025 31         62  
1026 31         50 }
1027 31 50       66 0;
1028 31         56 } @r;
1029             }
1030 0         0  
1031             use warnings;
1032             use experimental 'smartmatch';
1033 31 100       97  
1034             # perform paging
1035             log_trace("(read_table_func) Paging ...");
1036             unless ($metadata->{paged}) {
1037             if ($query->{result_start} > 1) {
1038             splice @r, 0, $query->{result_start}-1;
1039             }
1040 2     2   18 if (defined $query->{result_limit}) {
  2         5  
  2         69  
1041 2     2   13 splice @r, $query->{result_limit};
  2         5  
  2         18  
1042             }
1043             }
1044 175         504  
1045 175 50       590 # select fields
1046 175 100       411 log_trace("(read_table_func) Selecting fields ...");
1047 1         5 my $pk = $table_spec->{pk};
1048             goto SKIP_SELECT_FIELDS if $metadata->{fields_selected};
1049 175 100       420 REC2:
1050 4         15 for my $r (@r) {
1051             if (!$args{detail} && !$args{fields} && !$args{exclude_fields}) {
1052             $r = $r->{$pk};
1053             next REC2;
1054             }
1055 175         457 if ($args{with_field_names}) {
1056 175         532 my @f = keys %$fspecs;
1057 175 50       395 for (@f) {
1058             delete $r->{$_}
1059 175         346 unless $_ ~~ @{$query->{requested_fields}};
1060 560 100 100     2436 }
      100        
1061 510         1972 } else {
1062 510         1309 $r = [map {$r->{$_}} @{$query->{requested_fields}}];
1063             }
1064 50 100       91 }
1065 36         95 SKIP_SELECT_FIELDS:
1066 36         1040  
1067             my $resmeta = {};
1068 288 100       435 my $res = [200, "OK", \@r, $resmeta];
  288         759  
1069              
1070             $resmeta->{'table.fields'} = $query->{requested_fields};
1071 14         21  
  33         88  
  14         27  
1072             for ('before_return') {
1073             $hookargs{_func_res} = $res;
1074             last unless $hooks->{$_};
1075             $hookargs{_stage} = $_;
1076 175         334 my $hres = $hooks->{$_}->(%hookargs);
1077 175         436 return $hres if ref($hres);
1078             }
1079 175         406  
1080             $res;
1081 175         342 }; # func;
1082 175         329  
1083 175 100       410 [200, "OK", $func];
1084 1         3 }
1085 1         5  
1086 1 50       13 $SPEC{gen_read_table_func} = {
1087             v => 1.1,
1088             summary => 'Generate function (and its metadata) to read table data',
1089 175         1427 description => <<'_',
1090 34         971  
1091             The generated function acts like a simple single table SQL SELECT query,
1092 34         155 featuring filtering, ordering, and paging, but using arguments as the 'query
1093             language'. The generated function is suitable for exposing a table data from an
1094             API function.
1095              
1096             The resulting function returns an array of results/records and accepts these
1097             arguments.
1098              
1099             * *with_field_names* => BOOL (default 1)
1100              
1101             If set to 1, function will return records of field values along with field
1102             names (hashref), e.g. {id=>'ID', country=>'Indonesia', capital=>'Jakarta'}. If
1103             set to 0, then function will return record containing field values without
1104             field names (arrayref) instead, e.g.: ['ID', 'Indonesia', 'Jakarta'].
1105              
1106             * *detail* => BOOL (default 0)
1107              
1108             This is a field selection option. If set to 0, function will return PK field
1109             only. If this argument is set to 1, then all fields will be returned (see also
1110             *fields* to instruct function to return some fields only).
1111              
1112             * *fields* => ARRAY
1113              
1114             This is a field selection option. If you only want certain fields, specify
1115             them here (see also *detail*).
1116              
1117             * *result_limit* => INT (default undef)
1118              
1119             * *result_start* => INT (default 1)
1120              
1121             The *result_limit* and *result_start* arguments are paging options, they work
1122             like LIMIT clause in SQL, except that index starts at 1 and not 0. For
1123             example, to return the first 20 records in the result, set *result_limit* to
1124             20 . To return the next 20 records, set *result_limit* to 20 and
1125             *result_start* to 21.
1126              
1127             * *random* => BOOL (default 0)
1128              
1129             The random argument is an ordering option. If set to true, order of records
1130             returned will be shuffled first. This happened before paging.
1131              
1132             * *sort* => array of str
1133              
1134             The sort argument is an ordering option, containing names of field. A `-`
1135             prefix before the field name signifies descending instead of ascending order.
1136             Multiple fields are allowed for secondary sort fields.
1137              
1138             * *q* => ARRAY[STR]
1139              
1140             A filtering option. By default, all fields except those specified with
1141             searchable=0 will be searched using simple case-insensitive string search.
1142             There are a few options to customize this, using these gen arguments:
1143             *word_search*, *case_insensitive_search*, *custom_search*,
1144             *default_query_boolean*.
1145              
1146             * *query_boolean* => STR
1147              
1148             Either `and` or `or`. Default can be set with gen argument
1149             *default_query_boolean*. With `and`, all the words in *q* argument must match.
1150             With `or`, only one of the words in *q* argument must match.
1151              
1152             * Filter arguments
1153              
1154             They will be generated for each field, except when field has 'filterable'
1155             clause set to false.
1156              
1157             Undef values will not match any filter, just like NULL in SQL.
1158              
1159             * *FIELD.is* and *FIELD.isnt* arguments for each field. Only records with
1160             field equalling (or not equalling) value exactly ('==' or 'eq') will be
1161             included. If doesn't clash with other function arguments, *FIELD* will also
1162             be added as an alias for *FIELD.is*.
1163              
1164             * *FIELD.has* and *FIELD.lacks* array arguments for each set field. Only
1165             records with field having or lacking certain value will be included.
1166              
1167             * *FIELD.min* and *FIELD.max* for each int/float/str field. Only records with
1168             field greater/equal than, or less/equal than a certain value will be
1169             included.
1170              
1171             * *FIELD.contains* and *FIELD.not_contains* for each str field. Only records
1172             with field containing (or not containing) certain value (substring) will be
1173             included.
1174              
1175             * *FIELD.matches* and *FIELD.not_matches* for each str field. Only records
1176             with field matching (or not matching) certain value (regex) (or will be
1177             included. Function will return 400 if regex is invalid. These arguments will
1178             not be generated if 'filterable_regex' clause in field specification is set
1179             to 0.
1180              
1181             _
1182             args => {
1183             %Perinci::Sub::Gen::common_args,
1184             table_data => {
1185             req => 1,
1186             schema => ['any*' => of => ['array*', 'code*']],
1187             summary => 'Data',
1188             description => <<'_',
1189              
1190             Table data is either an AoH or AoA. Or you can also pass a Perl subroutine (see
1191             below).
1192              
1193             Passing a subroutine lets you fetch data dynamically and from arbitrary source
1194             (e.g. DBI table or other external sources). The subroutine will be called with
1195             these arguments ('$query') and is expected to return a hashref like this {data
1196             => DATA, paged=>BOOL, filtered=>BOOL, sorted=>BOOL, fields_selected=>BOOL}. DATA
1197             is AoA or AoH. If paged is set to 1, data is assumed to be already paged and
1198             won't be paged again; likewise for filtered, sorted, and fields selected. These
1199             are useful for example with DBI result, where requested data is already
1200             filtered/sorted (including randomized)/field selected/paged via appropriate SQL
1201             query. This way, the generated function will not attempt to duplicate the
1202             efforts.
1203              
1204             '$query' is a hashref which contains information about the query, e.g. 'args'
1205             (the original arguments passed to the generated function, e.g. {random=>1,
1206             result_limit=>1, field1_match=>'f.+'}), 'mentioned_fields' which lists fields
1207             that are mentioned in either filtering arguments or fields or ordering,
1208             'requested_fields' (fields mentioned in list of fields to be returned),
1209             'sort_fields' (fields mentioned in sort arguments), 'filter_fields' (fields
1210             mentioned in filter arguments).
1211              
1212             _
1213             },
1214             table_spec => {
1215             req => 1,
1216             schema => 'hash*',
1217             summary => 'Table specification',
1218             description => <<'_',
1219              
1220             See `TableDef` for more details.
1221              
1222             A hashref with these required keys: 'fields', 'pk'. 'fields' is a hashref of
1223             field specification with field name as keys, while 'pk' specifies which field is
1224             to be designated as the primary key. Currently only single-field PK is allowed.
1225              
1226             Field specification. A hashref with these required keys: 'schema' (a Sah
1227             schema), 'index' (an integer starting from 0 that specifies position of field in
1228             the record, required with AoA data) and these optional clauses: 'sortable' (a
1229             boolean stating whether field can be sorted, default is true), 'filterable' (a
1230             boolean stating whether field can be mentioned in filter options, default is
1231             true).
1232              
1233             _
1234             },
1235             langs => {
1236             schema => [array => {of=>'str*', default=>['en_US']}],
1237             summary => 'Choose language for function metadata',
1238             description => <<'_',
1239              
1240             This function can generate metadata containing text from one or more languages.
1241             For example if you set 'langs' to ['en_US', 'id_ID'] then the generated function
1242             metadata might look something like this:
1243              
1244             {
1245             v => 1.1,
1246             args => {
1247             random => {
1248             summary => 'Random order of results', # English
1249             "summary.alt.lang.id_ID" => "Acak urutan hasil", # Indonesian
1250             ...
1251             },
1252             ...
1253             },
1254             ...
1255             }
1256              
1257             _
1258             },
1259             default_detail => {
1260             schema => 'bool',
1261             summary => "Supply default 'detail' value for function arg spec",
1262             },
1263             default_fields => {
1264             schema => 'str',
1265             summary => "Supply default 'fields' value for function arg spec",
1266             },
1267             default_exclude_fields => {
1268             schema => 'str',
1269             summary => "Supply default 'exclude_fields' value for function arg spec",
1270             },
1271             default_with_field_names => {
1272             schema => 'bool',
1273             summary => "Supply default 'with_field_names' ".
1274             "value in generated function's metadata",
1275             },
1276             default_sort => {
1277             schema => ['array*', of=>'str*'],
1278             summary => "Supply default 'sort' ".
1279             "value in generated function's metadata",
1280             },
1281             default_random => {
1282             schema => 'bool',
1283             summary => "Supply default 'random' ".
1284             "value in generated function's metadata",
1285             },
1286             default_result_limit => {
1287             schema => 'int',
1288             summary => "Supply default 'result_limit' ".
1289             "value in generated function's metadata",
1290             },
1291             enable_filtering => {
1292             schema => ['bool' => {
1293             default => 1,
1294             }],
1295             summary => "Decide whether generated function will support ".
1296             "filtering (the FIELD, FIELD.is, FIELD.min, etc arguments)",
1297             },
1298             enable_search => {
1299             schema => ['bool' => {
1300             default => 1,
1301             }],
1302             summary => "Decide whether generated function will support ".
1303             "searching (argument q)",
1304             description => <<'_',
1305              
1306             Filtering must also be enabled (`enable_filtering`).
1307              
1308             _
1309             },
1310             word_search => {
1311             schema => ['bool' => {
1312             default => 0,
1313             }],
1314             summary => "Decide whether generated function will perform ".
1315             "word searching instead of string searching",
1316             description => <<'_',
1317              
1318             For example, if search term is 'pine' and field value is 'green pineapple',
1319             search will match if word_search=false, but won't match under word_search.
1320              
1321             This will not have effect under 'custom_search'.
1322              
1323             _
1324             },
1325             default_arg_values => {
1326             schema => 'hash',
1327             summary => "Specify defaults for generated function's arguments",
1328             description => <<'_',
1329              
1330             Can be used to supply default filters, e.g.
1331              
1332             # limit years for credit card expiration date
1333             { "year.min" => $curyear, "year.max" => $curyear+10, }
1334              
1335             _
1336             },
1337             default_query_boolean => {
1338             schema => ['str', in=>['and', 'or']],
1339             default => 'and',
1340             summary => "Specify default for --query-boolean option",
1341             },
1342             case_insensitive_search => {
1343             schema => ['bool' => {
1344             default => 1,
1345             }],
1346             summary => 'Decide whether generated function will perform '.
1347             'case-insensitive search',
1348             },
1349             case_insensitive_comparison => {
1350             schema => ['bool' => {
1351             default => 1,
1352             }],
1353             summary => 'Decide whether generated function will perform '.
1354             'case-insensitive comparison (e.g. for FIELD.is)',
1355             },
1356             custom_search => {
1357             schema => 'code',
1358             summary => 'Supply custom searching for generated function',
1359             description => <<'_',
1360              
1361             Code will be supplied ($r, $q, $opts) where $r is the record (hashref), $q is
1362             the search term (from the function argument 'q'), and $opts is {ci=>0|1}. Code
1363             should return true if record matches search term.
1364              
1365             _
1366             },
1367             enable_ordering => {
1368             schema => ['bool' => {
1369             default => 1,
1370             }],
1371             summary => "Decide whether generated function will support ".
1372             "ordering (the `sort` & `random` arguments)",
1373             },
1374             enable_random_ordering => {
1375             schema => ['bool' => {
1376             default => 1,
1377             }],
1378             summary => "Decide whether generated function will support ".
1379             "random ordering (the `random` argument)",
1380             description => <<'_',
1381              
1382             Ordering must also be enabled (`enable_ordering`).
1383              
1384             _
1385             },
1386             enable_paging => {
1387             schema => ['bool' => {
1388             default => 1,
1389             }],
1390             summary => "Decide whether generated function will support ".
1391             "paging (the `result_limit` & `result_start` arguments)",
1392             },
1393             enable_field_selection => {
1394             schema => ['bool' => {
1395             default => 1,
1396             }],
1397             summary => "Decide whether generated function will support ".
1398             "field selection (the `fields` argument)",
1399             },
1400             extra_args => {
1401             schema => ['hash*'],
1402             summary => 'Extra arguments for the generated function',
1403             },
1404             extra_props => {
1405             schema => ['hash*'],
1406             summary => 'Extra metadata properties for the generated function metadata',
1407             },
1408             custom_filters => {
1409             schema => [hash => {of=>['hash*' => {keys=>{
1410             'code'=>'code*', 'meta'=>'hash*'}}]}],
1411             summary => 'Supply custom filters',
1412             description => <<'_',
1413              
1414             A hash of filter name and definitions. Filter name will be used as generated
1415             function's argument and must not clash with other arguments. Filter definition
1416             is a hash containing these keys: *meta* (hash, argument metadata), *code*,
1417             *fields* (array, list of table fields related to this field).
1418              
1419             Code will be called for each record to be filtered and will be supplied ($r, $v,
1420             $opts) where $v is the filter value (from the function argument) and $r the
1421             hashref record value. $opts is currently empty. Code should return true if
1422             record satisfies the filter.
1423              
1424             _
1425             },
1426             hooks => {
1427             schema => [hash => {of=>'code*'}],
1428             summary => 'Supply hooks',
1429             description => <<'_',
1430              
1431             You can instruct the generated function to execute codes in various stages by
1432             using hooks. Currently available hooks are: `before_parse_query`,
1433             `after_parse_query`, `before_fetch_data`, `after_fetch_data`, `before_return`.
1434             Hooks will be passed the function arguments as well as one or more additional
1435             ones. All hooks will get `_stage` (name of stage) and `_func_res` (function
1436             arguments, but as hash reference so you can modify it). `after_parse_query` and
1437             later hooks will also get `_parse_res` (parse result). `before_fetch_data` and
1438             later will also get `_query`. `after_fetch_data` and later will also get
1439             `_data`. `before_return` will also get `_func_res` (the enveloped response to be
1440             returned to user).
1441              
1442             Hook should return nothing or a false value on success. It can abort execution
1443             of the generated function if it returns an envelope response (an array). On that
1444             case, the function will return with this return value.
1445              
1446             _
1447             },
1448              
1449             result_limit_aliases => {
1450             schema => 'hash*',
1451             },
1452             result_start_aliases => {
1453             schema => 'hash*',
1454             },
1455             with_field_names_aliases => {
1456             schema => 'hash*',
1457             },
1458             detail_aliases => {
1459             schema => 'hash*',
1460             },
1461             fields_aliases => {
1462             schema => 'hash*',
1463             },
1464             exclude_fields_aliases => {
1465             schema => 'hash*',
1466             },
1467             sort_aliases => {
1468             schema => 'hash*',
1469             },
1470             random_aliases => {
1471             schema => 'hash*',
1472             },
1473             queries_aliases => {
1474             schema => 'hash*',
1475             },
1476              
1477             }, # args
1478             result => {
1479             summary => 'A hash containing generated function, metadata',
1480             schema => 'hash*',
1481             description => <<'_',
1482             _
1483             },
1484             };
1485             my %args = @_;
1486              
1487             # XXX schema
1488             my ($uqname, $package);
1489             my $fqname = $args{name};
1490             return [400, "Please specify name"] unless $fqname;
1491             my @caller = caller();
1492             if ($fqname =~ /(.+)::(.+)/) {
1493             $package = $1;
1494             $uqname = $2;
1495 38     38 1 334312 } else {
1496             $package = $args{package} // $caller[0];
1497             $uqname = $fqname;
1498 38         101 $fqname = "$package\::$uqname";
1499 38         99 }
1500 38 50       130 my $table_data = $args{table_data}
1501 38         131 or return [400, "Please specify table_data"];
1502 38 50       127 __is_aoa($table_data) or __is_aoh($table_data) or
1503 0         0 ref($table_data) eq 'CODE'
1504 0         0 or return [400, "Invalid table_data: must be AoA/AoH/function"];
1505             my $table_spec = $args{table_spec}
1506 38   33     225 or return [400, "Please specify table_spec"];
1507 38         80 ref($table_spec) eq 'HASH'
1508 38         109 or return [400, "Invalid table_spec: must be a hash"];
1509             $table_spec->{fields} or
1510             return [400, "Invalid table_spec: fields not specified"];
1511 38 50       129 ref($table_spec->{fields}) eq 'HASH' or
1512 38 50 100     110 return [400, "Invalid table_spec: fields must be hash"];
      66        
1513             $table_spec->{pk} or
1514             return [400, "Invalid table_spec: pk not specified"];
1515             exists($table_spec->{fields}{ $table_spec->{pk} }) or
1516 38 50       155 return [400, "Invalid table_spec: pk not in fields"];
1517 38 50       125  
1518             # duplicate and make each field's schema normalized
1519             $table_spec = clone($table_spec);
1520 38 100       133 for my $fspec (values %{$table_spec->{fields}}) {
1521 37 50       110 $fspec->{schema} //= 'any';
1522             $fspec->{schema} = __parse_schema($fspec->{schema});
1523             }
1524 37 100       119 # make each custom filter's schema normalized
1525 35 100       135 my $cff = $args{custom_filters} // {};
1526             while (my ($cfn, $cf) = each %$cff) {
1527             $cf->{meta} //= {};
1528             $cf->{meta}{schema} //= 'any';
1529 34         159 $cf->{meta}{schema} = __parse_schema($cf->{meta}{schema});
1530 34         3761 }
  34         179  
1531 249   50     3976  
1532 249         503 my $dav = $args{default_arg_values} // {};
1533             my $opts = {
1534             summary => $args{summary},
1535 34   100     813 description => $args{description},
1536 34         177 langs => $args{langs} // ['en_US'],
1537 2   50     23  
1538 2   50     7 default_detail => $args{default_detail},
1539 2         8 detail_aliases => $args{detail_cmdline_aliases} // {l=>{}},
1540              
1541             default_with_field_names => $args{default_with_field_names},
1542 34   100     228 with_field_names_aliases => $args{with_field_names_aliases},
1543              
1544             default_fields => $args{default_fields},
1545             fields_aliases => $args{fields_aliases},
1546              
1547             default_sort => $args{default_sort},
1548             sort_aliases => $args{sort_aliases},
1549              
1550             default_random => $args{default_random},
1551             random_aliases => $args{random_aliases},
1552              
1553             default_result_limit => $args{default_result_limit},
1554             result_limit_aliases => $args{result_limit_aliases},
1555              
1556             result_start_aliases => $args{result_start_aliases},
1557              
1558             query_aliases => $args{query_aliases} // {q=>{}}, # old, still supported
1559             queries_aliases => $args{queries_aliases} // {q=>{}},
1560              
1561             default_query_boolean => $args{default_query_boolean} // 'and',
1562              
1563             enable_filtering => $args{enable_filtering} // 1,
1564             enable_search => $args{enable_search} // 1,
1565             custom_search => $args{custom_search},
1566             word_search => $args{word_search},
1567             case_insensitive_search => $args{case_insensitive_search} // 1,
1568             case_insensitive_comparison=> $args{case_insensitive_comparison},
1569             enable_ordering => $args{enable_ordering} // 1,
1570             enable_random_ordering => ($args{enable_random_ordering} //
1571             $args{enable_ordering} // 1),
1572             enable_paging => $args{enable_paging} // 1,
1573             enable_field_selection => $args{enable_field_selection} // 1,
1574             (map { ("default_$_" => $dav->{$_}) } keys %$dav),
1575             custom_filters => $cff,
1576             extra_args => $args{extra_args},
1577             extra_props => $args{extra_props},
1578             hooks => $args{hooks} // {},
1579             };
1580              
1581             my $res;
1582             $res = _gen_meta($table_spec, $opts);
1583             return err(500, "Can't generate meta", $res) unless $res->[0] == 200;
1584 1         23 my $func_meta = $res->[2];
1585              
1586             $res = _gen_func($table_spec, $opts, $table_data, $func_meta);
1587             return err(500, "Can't generate func", $res) unless $res->[0] == 200;
1588             my $func = $res->[2];
1589 34   50     1684  
      50        
      50        
      50        
      50        
      100        
      100        
      100        
      100        
      66        
      100        
      100        
      100        
      100        
1590             if ($args{install} // 1) {
1591 34         84 no strict 'refs'; ## no critic: TestingAndDebugging::ProhibitNoStrict
1592 34         134 no warnings;
1593 34 50       125 log_trace("Installing function as %s ...", $fqname);
1594 34         73 *{ $fqname } = $func;
1595             ${$package . "::SPEC"}{$uqname} = $func_meta;
1596 34         129 }
1597 34 50       111  
1598 34         75 [200, "OK", {meta=>$func_meta, code=>$func}];
1599             }
1600 34 50 50     161  
1601 2     2   3301 1;
  2         4  
  2         75  
1602 2     2   12 # ABSTRACT: Generate function (and its metadata) to read table data
  2         4  
  2         488  
1603 0         0  
1604 0         0  
  0         0  
1605 0         0 =pod
  0         0  
1606              
1607             =encoding UTF-8
1608 34         348  
1609             =head1 NAME
1610              
1611             Perinci::Sub::Gen::AccessTable - Generate function (and its metadata) to read table data
1612              
1613             =head1 VERSION
1614              
1615             This document describes version 0.588 of Perinci::Sub::Gen::AccessTable (from Perl distribution Perinci-Sub-Gen-AccessTable), released on 2022-11-02.
1616              
1617             =head1 SYNOPSIS
1618              
1619             In list_countries.pl:
1620              
1621             #!perl
1622             use strict;
1623             use warnings;
1624             use Perinci::CmdLine;
1625             use Perinci::Sub::Gen::AccessTable qw(gen_read_table_func);
1626              
1627             our %SPEC;
1628              
1629             my $countries = [
1630             ['cn', 'China', 'Cina', [qw/panda/]],
1631             ['id', 'Indonesia', 'Indonesia', [qw/bali tropical/]],
1632             ['sg', 'Singapore', 'Singapura', [qw/tropical/]],
1633             ['us', 'United States of America', 'Amerika Serikat', [qw//]],
1634             ];
1635              
1636             my $res = gen_read_table_func(
1637             name => 'list_countries',
1638             summary => 'func summary', # opt
1639             description => 'func description', # opt
1640             table_data => $countries,
1641             table_spec => {
1642             summary => 'List of countries',
1643             fields => {
1644             id => {
1645             schema => 'str*',
1646             summary => 'ISO 2-letter code for the country',
1647             pos => 0,
1648             sortable => 1,
1649             },
1650             eng_name => {
1651             schema => 'str*',
1652             summary => 'English name',
1653             pos => 1,
1654             sortable => 1,
1655             },
1656             ind_name => {
1657             schema => 'str*',
1658             summary => 'Indonesian name',
1659             pos => 2,
1660             sortable => 1,
1661             },
1662             tags => {
1663             schema => 'array*',
1664             summary => 'Keywords/tags',
1665             pos => 3,
1666             sortable => 0,
1667             },
1668             },
1669             pk => 'id',
1670             },
1671             );
1672             die "Can't generate function: $res->[0] - $res->[1]" unless $res->[0] == 200;
1673              
1674             Perinci::CmdLine->new(url=>'/main/list_countries')->run;
1675              
1676             Now you can do:
1677              
1678             # list all countries, by default only PK field is shown
1679             $ list_countries.pl --format=text-simple
1680             cn
1681             id
1682             sg
1683             us
1684              
1685             # show as json, randomize order
1686             $ list_countries.pl --format=json --random
1687             ["id","us","sg","cn"]
1688              
1689             # only list countries which are tagged as 'tropical', sort by ind_name field in
1690             # descending order, show all fields (--detail)
1691             $ list_countries.pl --detail --sort -ind_name --tags-has '[tropical]'
1692             .---------------------------------------------.
1693             | eng_name | id | ind_name | tags |
1694             +-----------+----+-----------+----------------+
1695             | Singapore | sg | Singapura | tropical |
1696             | Indonesia | id | Indonesia | bali, tropical |
1697             '-----------+----+-----------+----------------'
1698              
1699             # show only certain fields, limit number of records, return in YAML format
1700             $ list_countries.pl --fields '[id, eng_name]' --result-limit 2 --format=yaml
1701             - 200
1702             - OK
1703             -
1704             - id: cn
1705             eng_name: China
1706             - id: id
1707             eng_name: Indonesia
1708              
1709             =head1 DESCRIPTION
1710              
1711             This module is useful when you want to expose a table data (an array of
1712             hashrefs, an array of arrays, or external data like a SQL table) as an API
1713             function. This module will generate a function (along with its L<Rinci>
1714             metadata) that accepts arguments for specifying fields, filtering, sorting, and
1715             paging. The resulting function can then be run via command-line using
1716             L<Perinci::CmdLine> (as demonstrated in Synopsis), or served via HTTP using
1717             L<Perinci::Access::HTTP::Server>, or consumed normally by Perl programs.
1718              
1719             =head1 FUNCTIONS
1720              
1721              
1722             =head2 gen_read_table_func
1723              
1724             Usage:
1725              
1726             gen_read_table_func(%args) -> [$status_code, $reason, $payload, \%result_meta]
1727              
1728             Generate function (and its metadata) to read table data.
1729              
1730             The generated function acts like a simple single table SQL SELECT query,
1731             featuring filtering, ordering, and paging, but using arguments as the 'query
1732             language'. The generated function is suitable for exposing a table data from an
1733             API function.
1734              
1735             The resulting function returns an array of results/records and accepts these
1736             arguments.
1737              
1738             =over
1739              
1740             =item * I<with_field_names> => BOOL (default 1)
1741              
1742             If set to 1, function will return records of field values along with field
1743             names (hashref), e.g. {id=>'ID', country=>'Indonesia', capital=>'Jakarta'}. If
1744             set to 0, then function will return record containing field values without
1745             field names (arrayref) instead, e.g.: ['ID', 'Indonesia', 'Jakarta'].
1746              
1747             =item * I<detail> => BOOL (default 0)
1748              
1749             This is a field selection option. If set to 0, function will return PK field
1750             only. If this argument is set to 1, then all fields will be returned (see also
1751             I<fields> to instruct function to return some fields only).
1752              
1753             =item * I<fields> => ARRAY
1754              
1755             This is a field selection option. If you only want certain fields, specify
1756             them here (see also I<detail>).
1757              
1758             =item * I<result_limit> => INT (default undef)
1759              
1760             =item * I<result_start> => INT (default 1)
1761              
1762             The I<result_limit> and I<result_start> arguments are paging options, they work
1763             like LIMIT clause in SQL, except that index starts at 1 and not 0. For
1764             example, to return the first 20 records in the result, set I<result_limit> to
1765             20 . To return the next 20 records, set I<result_limit> to 20 and
1766             I<result_start> to 21.
1767              
1768             =item * I<random> => BOOL (default 0)
1769              
1770             The random argument is an ordering option. If set to true, order of records
1771             returned will be shuffled first. This happened before paging.
1772              
1773             =item * I<sort> => array of str
1774              
1775             The sort argument is an ordering option, containing names of field. A C<->
1776             prefix before the field name signifies descending instead of ascending order.
1777             Multiple fields are allowed for secondary sort fields.
1778              
1779             =item * I<q> => ARRAY[STR]
1780              
1781             A filtering option. By default, all fields except those specified with
1782             searchable=0 will be searched using simple case-insensitive string search.
1783             There are a few options to customize this, using these gen arguments:
1784             I<word_search>, I<case_insensitive_search>, I<custom_search>,
1785             I<default_query_boolean>.
1786              
1787             =item * I<query_boolean> => STR
1788              
1789             Either C<and> or C<or>. Default can be set with gen argument
1790             I<default_query_boolean>. With C<and>, all the words in I<q> argument must match.
1791             With C<or>, only one of the words in I<q> argument must match.
1792              
1793             =item * Filter arguments
1794              
1795             They will be generated for each field, except when field has 'filterable'
1796             clause set to false.
1797              
1798             Undef values will not match any filter, just like NULL in SQL.
1799              
1800             =over
1801              
1802             =item * I<FIELD.is> and I<FIELD.isnt> arguments for each field. Only records with
1803             field equalling (or not equalling) value exactly ('==' or 'eq') will be
1804             included. If doesn't clash with other function arguments, I<FIELD> will also
1805             be added as an alias for I<FIELD.is>.
1806              
1807             =item * I<FIELD.has> and I<FIELD.lacks> array arguments for each set field. Only
1808             records with field having or lacking certain value will be included.
1809              
1810             =item * I<FIELD.min> and I<FIELD.max> for each int/float/str field. Only records with
1811             field greater/equal than, or less/equal than a certain value will be
1812             included.
1813              
1814             =item * I<FIELD.contains> and I<FIELD.not_contains> for each str field. Only records
1815             with field containing (or not containing) certain value (substring) will be
1816             included.
1817              
1818             =item * I<FIELD.matches> and I<FIELD.not_matches> for each str field. Only records
1819             with field matching (or not matching) certain value (regex) (or will be
1820             included. Function will return 400 if regex is invalid. These arguments will
1821             not be generated if 'filterable_regex' clause in field specification is set
1822             to 0.
1823              
1824             =back
1825              
1826             =back
1827              
1828             This function is not exported by default, but exportable.
1829              
1830             Arguments ('*' denotes required arguments):
1831              
1832             =over 4
1833              
1834             =item * B<case_insensitive_comparison> => I<bool> (default: 1)
1835              
1836             Decide whether generated function will perform case-insensitive comparison (e.g. for FIELD.is).
1837              
1838             =item * B<case_insensitive_search> => I<bool> (default: 1)
1839              
1840             Decide whether generated function will perform case-insensitive search.
1841              
1842             =item * B<custom_filters> => I<hash>
1843              
1844             Supply custom filters.
1845              
1846             A hash of filter name and definitions. Filter name will be used as generated
1847             function's argument and must not clash with other arguments. Filter definition
1848             is a hash containing these keys: I<meta> (hash, argument metadata), I<code>,
1849             I<fields> (array, list of table fields related to this field).
1850              
1851             Code will be called for each record to be filtered and will be supplied ($r, $v,
1852             $opts) where $v is the filter value (from the function argument) and $r the
1853             hashref record value. $opts is currently empty. Code should return true if
1854             record satisfies the filter.
1855              
1856             =item * B<custom_search> => I<code>
1857              
1858             Supply custom searching for generated function.
1859              
1860             Code will be supplied ($r, $q, $opts) where $r is the record (hashref), $q is
1861             the search term (from the function argument 'q'), and $opts is {ci=>0|1}. Code
1862             should return true if record matches search term.
1863              
1864             =item * B<default_arg_values> => I<hash>
1865              
1866             Specify defaults for generated function's arguments.
1867              
1868             Can be used to supply default filters, e.g.
1869              
1870             # limit years for credit card expiration date
1871             { "year.min" => $curyear, "year.max" => $curyear+10, }
1872              
1873             =item * B<default_detail> => I<bool>
1874              
1875             Supply default 'detail' value for function arg spec.
1876              
1877             =item * B<default_exclude_fields> => I<str>
1878              
1879             Supply default 'exclude_fields' value for function arg spec.
1880              
1881             =item * B<default_fields> => I<str>
1882              
1883             Supply default 'fields' value for function arg spec.
1884              
1885             =item * B<default_query_boolean> => I<str> (default: "and")
1886              
1887             Specify default for --query-boolean option.
1888              
1889             =item * B<default_random> => I<bool>
1890              
1891             Supply default 'random' value in generated function's metadata.
1892              
1893             =item * B<default_result_limit> => I<int>
1894              
1895             Supply default 'result_limit' value in generated function's metadata.
1896              
1897             =item * B<default_sort> => I<array[str]>
1898              
1899             Supply default 'sort' value in generated function's metadata.
1900              
1901             =item * B<default_with_field_names> => I<bool>
1902              
1903             Supply default 'with_field_names' value in generated function's metadata.
1904              
1905             =item * B<description> => I<str>
1906              
1907             Generated function's description.
1908              
1909             =item * B<detail_aliases> => I<hash>
1910              
1911             (No description)
1912              
1913             =item * B<enable_field_selection> => I<bool> (default: 1)
1914              
1915             Decide whether generated function will support field selection (the `fields` argument).
1916              
1917             =item * B<enable_filtering> => I<bool> (default: 1)
1918              
1919             Decide whether generated function will support filtering (the FIELD, FIELD.is, FIELD.min, etc arguments).
1920              
1921             =item * B<enable_ordering> => I<bool> (default: 1)
1922              
1923             Decide whether generated function will support ordering (the `sort` & `random` arguments).
1924              
1925             =item * B<enable_paging> => I<bool> (default: 1)
1926              
1927             Decide whether generated function will support paging (the `result_limit` & `result_start` arguments).
1928              
1929             =item * B<enable_random_ordering> => I<bool> (default: 1)
1930              
1931             Decide whether generated function will support random ordering (the `random` argument).
1932              
1933             Ordering must also be enabled (C<enable_ordering>).
1934              
1935             =item * B<enable_search> => I<bool> (default: 1)
1936              
1937             Decide whether generated function will support searching (argument q).
1938              
1939             Filtering must also be enabled (C<enable_filtering>).
1940              
1941             =item * B<exclude_fields_aliases> => I<hash>
1942              
1943             (No description)
1944              
1945             =item * B<extra_args> => I<hash>
1946              
1947             Extra arguments for the generated function.
1948              
1949             =item * B<extra_props> => I<hash>
1950              
1951             Extra metadata properties for the generated function metadata.
1952              
1953             =item * B<fields_aliases> => I<hash>
1954              
1955             (No description)
1956              
1957             =item * B<hooks> => I<hash>
1958              
1959             Supply hooks.
1960              
1961             You can instruct the generated function to execute codes in various stages by
1962             using hooks. Currently available hooks are: C<before_parse_query>,
1963             C<after_parse_query>, C<before_fetch_data>, C<after_fetch_data>, C<before_return>.
1964             Hooks will be passed the function arguments as well as one or more additional
1965             ones. All hooks will get C<_stage> (name of stage) and C<_func_res> (function
1966             arguments, but as hash reference so you can modify it). C<after_parse_query> and
1967             later hooks will also get C<_parse_res> (parse result). C<before_fetch_data> and
1968             later will also get C<_query>. C<after_fetch_data> and later will also get
1969             C<_data>. C<before_return> will also get C<_func_res> (the enveloped response to be
1970             returned to user).
1971              
1972             Hook should return nothing or a false value on success. It can abort execution
1973             of the generated function if it returns an envelope response (an array). On that
1974             case, the function will return with this return value.
1975              
1976             =item * B<install> => I<bool> (default: 1)
1977              
1978             Whether to install generated function (and metadata).
1979              
1980             By default, generated function will be installed to the specified (or caller's)
1981             package, as well as its generated metadata into %SPEC. Set this argument to
1982             false to skip installing.
1983              
1984             =item * B<langs> => I<array[str]> (default: ["en_US"])
1985              
1986             Choose language for function metadata.
1987              
1988             This function can generate metadata containing text from one or more languages.
1989             For example if you set 'langs' to ['en_US', 'id_ID'] then the generated function
1990             metadata might look something like this:
1991              
1992             {
1993             v => 1.1,
1994             args => {
1995             random => {
1996             summary => 'Random order of results', # English
1997             "summary.alt.lang.id_ID" => "Acak urutan hasil", # Indonesian
1998             ...
1999             },
2000             ...
2001             },
2002             ...
2003             }
2004              
2005             =item * B<name> => I<str>
2006              
2007             Generated function's name, e.g. `myfunc`.
2008              
2009             =item * B<package> => I<str>
2010              
2011             Generated function's package, e.g. `My::Package`.
2012              
2013             This is needed mostly for installing the function. You usually don't need to
2014             supply this if you set C<install> to false.
2015              
2016             If not specified, caller's package will be used by default.
2017              
2018             =item * B<queries_aliases> => I<hash>
2019              
2020             (No description)
2021              
2022             =item * B<random_aliases> => I<hash>
2023              
2024             (No description)
2025              
2026             =item * B<result_limit_aliases> => I<hash>
2027              
2028             (No description)
2029              
2030             =item * B<result_start_aliases> => I<hash>
2031              
2032             (No description)
2033              
2034             =item * B<sort_aliases> => I<hash>
2035              
2036             (No description)
2037              
2038             =item * B<summary> => I<str>
2039              
2040             Generated function's summary.
2041              
2042             =item * B<table_data>* => I<array|code>
2043              
2044             Data.
2045              
2046             Table data is either an AoH or AoA. Or you can also pass a Perl subroutine (see
2047             below).
2048              
2049             Passing a subroutine lets you fetch data dynamically and from arbitrary source
2050             (e.g. DBI table or other external sources). The subroutine will be called with
2051             these arguments ('$query') and is expected to return a hashref like this {data
2052             => DATA, paged=>BOOL, filtered=>BOOL, sorted=>BOOL, fields_selected=>BOOL}. DATA
2053             is AoA or AoH. If paged is set to 1, data is assumed to be already paged and
2054             won't be paged again; likewise for filtered, sorted, and fields selected. These
2055             are useful for example with DBI result, where requested data is already
2056             filtered/sorted (including randomized)/field selected/paged via appropriate SQL
2057             query. This way, the generated function will not attempt to duplicate the
2058             efforts.
2059              
2060             '$query' is a hashref which contains information about the query, e.g. 'args'
2061             (the original arguments passed to the generated function, e.g. {random=>1,
2062             result_limit=>1, field1_match=>'f.+'}), 'mentioned_fields' which lists fields
2063             that are mentioned in either filtering arguments or fields or ordering,
2064             'requested_fields' (fields mentioned in list of fields to be returned),
2065             'sort_fields' (fields mentioned in sort arguments), 'filter_fields' (fields
2066             mentioned in filter arguments).
2067              
2068             =item * B<table_spec>* => I<hash>
2069              
2070             Table specification.
2071              
2072             See C<TableDef> for more details.
2073              
2074             A hashref with these required keys: 'fields', 'pk'. 'fields' is a hashref of
2075             field specification with field name as keys, while 'pk' specifies which field is
2076             to be designated as the primary key. Currently only single-field PK is allowed.
2077              
2078             Field specification. A hashref with these required keys: 'schema' (a Sah
2079             schema), 'index' (an integer starting from 0 that specifies position of field in
2080             the record, required with AoA data) and these optional clauses: 'sortable' (a
2081             boolean stating whether field can be sorted, default is true), 'filterable' (a
2082             boolean stating whether field can be mentioned in filter options, default is
2083             true).
2084              
2085             =item * B<with_field_names_aliases> => I<hash>
2086              
2087             (No description)
2088              
2089             =item * B<word_search> => I<bool> (default: 0)
2090              
2091             Decide whether generated function will perform word searching instead of string searching.
2092              
2093             For example, if search term is 'pine' and field value is 'green pineapple',
2094             search will match if word_search=false, but won't match under word_search.
2095              
2096             This will not have effect under 'custom_search'.
2097              
2098              
2099             =back
2100              
2101             Returns an enveloped result (an array).
2102              
2103             First element ($status_code) is an integer containing HTTP-like status code
2104             (200 means OK, 4xx caller error, 5xx function error). Second element
2105             ($reason) is a string containing error message, or something like "OK" if status is
2106             200. Third element ($payload) is the actual result, but usually not present when enveloped result is an error response ($status_code is not 2xx). Fourth
2107             element (%result_meta) is called result metadata and is optional, a hash
2108             that contains extra information, much like how HTTP response headers provide additional metadata.
2109              
2110             Return value: A hash containing generated function, metadata (hash)
2111              
2112             =head1 FAQ
2113              
2114             =head2 I want my function to accept additional arguments.
2115              
2116             You can use the C<extra_args> argument:
2117              
2118             gen_read_table_func(
2119             name => 'myfunc',
2120             extra_args => {
2121             foo => {schema=>'int*'},
2122             bar => {summary => 'Yet another arg for myfunc', schema=>'str*'},
2123             },
2124             );
2125              
2126             As for the implementation, you can specify hooks to do things with the extra
2127             arguments.
2128              
2129             =head1 HOMEPAGE
2130              
2131             Please visit the project's homepage at L<https://metacpan.org/release/Perinci-Sub-Gen-AccessTable>.
2132              
2133             =head1 SOURCE
2134              
2135             Source repository is at L<https://github.com/perlancar/perl-Perinci-Sub-Gen-AccessTable>.
2136              
2137             =head1 SEE ALSO
2138              
2139             L<Perinci::Sub::Gen::AccessTable::Simple> for a simpler variant.
2140              
2141             L<Rinci>
2142              
2143             L<Perinci::CmdLine>
2144              
2145             =head1 AUTHOR
2146              
2147             perlancar <perlancar@cpan.org>
2148              
2149             =head1 CONTRIBUTOR
2150              
2151             =for stopwords Steven Haryanto
2152              
2153             Steven Haryanto <stevenharyanto@gmail.com>
2154              
2155             =head1 CONTRIBUTING
2156              
2157              
2158             To contribute, you can send patches by email/via RT, or send pull requests on
2159             GitHub.
2160              
2161             Most of the time, you don't need to build the distribution yourself. You can
2162             simply modify the code, then test via:
2163              
2164             % prove -l
2165              
2166             If you want to build the distribution (e.g. to try to install it locally on your
2167             system), you can install L<Dist::Zilla>,
2168             L<Dist::Zilla::PluginBundle::Author::PERLANCAR>,
2169             L<Pod::Weaver::PluginBundle::Author::PERLANCAR>, and sometimes one or two other
2170             Dist::Zilla- and/or Pod::Weaver plugins. Any additional steps required beyond
2171             that are considered a bug and can be reported to me.
2172              
2173             =head1 COPYRIGHT AND LICENSE
2174              
2175             This software is copyright (c) 2022, 2020, 2019, 2018, 2017, 2016, 2015, 2014, 2013, 2012, 2011 by perlancar <perlancar@cpan.org>.
2176              
2177             This is free software; you can redistribute it and/or modify it under
2178             the same terms as the Perl 5 programming language system itself.
2179              
2180             =head1 BUGS
2181              
2182             Please report any bugs or feature requests on the bugtracker website L<https://rt.cpan.org/Public/Dist/Display.html?Name=Perinci-Sub-Gen-AccessTable>
2183              
2184             When submitting a bug or request, please include a test-file or a
2185             patch to an existing test-file that illustrates the bug or desired
2186             feature.
2187              
2188             =head1 CAVEATS
2189              
2190             It is often not a good idea to expose your database schema directly as API.
2191              
2192             =cut