File Coverage

blib/lib/Perinci/Sub/Gen/AccessTable.pm
Criterion Covered Total %
statement 559 603 92.7
branch 352 444 79.2
condition 212 291 72.8
subroutine 28 29 96.5
pod 1 1 100.0
total 1152 1368 84.2


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