File Coverage

blib/lib/Perinci/Sub/Gen/AccessTable.pm
Criterion Covered Total %
statement 553 595 92.9
branch 347 440 78.8
condition 200 272 73.5
subroutine 27 27 100.0
pod 1 1 100.0
total 1128 1335 84.4


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