File Coverage

blib/lib/Complete/Sah.pm
Criterion Covered Total %
statement 188 231 81.3
branch 84 120 70.0
condition 60 100 60.0
subroutine 9 9 100.0
pod 1 1 100.0
total 342 461 74.1


line stmt bran cond sub pod time code
1             package Complete::Sah;
2              
3 1     1   110988 use 5.010001;
  1         16  
4 1     1   5 use strict;
  1         8  
  1         44  
5 1     1   6 use warnings;
  1         1  
  1         43  
6 1     1   2770 use Log::ger;
  1         63  
  1         5  
7              
8 1     1   797 use Complete::Common qw(:all);
  1         429  
  1         167  
9 1     1   680 use Complete::Util qw(combine_answers complete_array_elem hashify_answer);
  1         6470  
  1         74  
10 1     1   6 use Exporter qw(import);
  1         6  
  1         332  
11              
12             our $AUTHORITY = 'cpan:PERLANCAR'; # AUTHORITY
13             our $DATE = '2023-01-28'; # DATE
14             our $DIST = 'Complete-Sah'; # DIST
15             our $VERSION = '0.011'; # VERSION
16              
17             our %SPEC;
18             our @EXPORT_OK = qw(complete_from_schema);
19              
20             $SPEC{':package'} = {
21             v => 1.1,
22             summary => 'Sah-related completion routines',
23             };
24              
25             $SPEC{complete_from_schema} = {
26             v => 1.1,
27             summary => 'Complete a value from schema',
28             description => <<'_',
29              
30             Employ some heuristics to complete a value from Sah schema. For example, if
31             schema is `[str => in => [qw/new open resolved rejected/]]`, then we can
32             complete from the `in` clause. Or for something like `[int => between => [1,
33             20]]` we can complete using values from 1 to 20.
34              
35             Tip: If you want to give summary for each entry in `in` clause, you can use the
36             `x.in.summaries` attribute, example:
37              
38             # schema
39             ['str', {
40             in => ['b', 'g'],
41             'x.in.summaries' => ['Male/boy', 'Female/girl'],
42             }]
43              
44             _
45             args => {
46             schema => {
47             schema => ['any*', of=>['str*', 'array*']], # XXX sah::schema
48             description => <<'_',
49              
50             Will be normalized, unless when `schema_is_normalized` is set to true, in which
51             case schema must already be normalized.
52              
53             _
54             req => 1,
55             },
56             schema_is_normalized => {
57             schema => 'bool',
58             default => 0,
59             },
60             word => {
61             schema => [str => default => ''],
62             req => 1,
63             },
64             },
65             result_naked => 1,
66             };
67             sub complete_from_schema {
68 43     43 1 134750 my %args = @_;
69 43         100 my $sch = $args{schema};
70 43   50     104 my $word = $args{word} // "";
71              
72 43 100       95 unless ($args{schema_is_normalized}) {
73 42         1546 require Data::Sah::Normalize;
74 42         2000 $sch =Data::Sah::Normalize::normalize_schema($sch);
75             }
76              
77 43         2897 my $fres;
78 43         130 log_trace("[compsah] entering complete_from_schema, word=<%s>, schema=%s", $word, $sch);
79              
80 43         115 my ($type, $clset) = @{$sch};
  43         105  
81              
82             # schema might be based on other schemas, if that is the case, let's try to
83             # look at Sah::SchemaR::* module to quickly find the base type
84 42 100       194 unless ($type =~ /\A(all|any|array|bool|buf|cistr|code|date|duration|float|hash|int|num|obj|re|str|undef)\z/) {
85 1     1   15 no strict 'refs'; ## no critic: TestingAndDebugging::ProhibitNoStrict
  1         2  
  1         2218  
86 3         20 my $pkg = "Sah::SchemaR::$type";
87 3         16 (my $pkg_pm = "$pkg.pm") =~ s!::!/!g;
88 3         7 eval { require $pkg_pm; 1 };
  3         3772  
  2         114  
89 3 100       15 if ($@) {
90 1         5 log_trace("[compsah] couldn't load schema module %s: %s, skipped", $pkg, $@);
91 1         18 goto RETURN_RES;
92             }
93 2         10 my $rsch = ${"$pkg\::rschema"};
  2         10  
94 2 50       9 $type = ref $rsch eq 'ARRAY' ? $rsch->[0] : $rsch->{type}; # support older (v.009-) version of Data::Sah::Resolve result
95 2 50       9 my $clsets = ref $rsch eq 'ARRAY' ? $rsch->[1] : $rsch->{'clsets_after_type.alt.merge.merged'};
96             # let's just merge everything, for quick checking of clause
97 2         3 my $merged_clset = {};
98 2         4 for my $clset0 (@{ $clsets }) {
  2         6  
99 2         8 for (keys %$clset0) {
100 9         17 $merged_clset->{$_} = $clset0->{$_};
101             }
102             }
103 2         4 $clset = $merged_clset;
104 2         8 log_trace("[compsah] retrieving schema from module %s, base type=%s", $pkg, $type);
105             }
106              
107 41         103 my $static;
108             my $words;
109 41         0 my $summaries;
110 41         101 eval {
111 41 50       98 if (my $xcomp = $clset->{'x.completion'}) {
112 0         0 require Module::Installed::Tiny;
113 0         0 my $comp;
114 0 0       0 if (ref($xcomp) eq 'CODE') {
115 0         0 $comp = $xcomp;
116             } else {
117 0         0 my ($submod, $xcargs);
118 0 0       0 if (ref($xcomp) eq 'ARRAY') {
119 0         0 $submod = $xcomp->[0];
120 0         0 $xcargs = $xcomp->[1];
121             } else {
122 0         0 $submod = $xcomp;
123 0         0 $xcargs = {};
124             }
125 0         0 my $mod = "Perinci::Sub::XCompletion::$submod";
126 0 0       0 if (Module::Installed::Tiny::module_installed($mod)) {
127 0         0 log_trace("[compsah] loading module %s ...", $mod);
128 0         0 my $mod_pm = $mod; $mod_pm =~ s!::!/!g; $mod_pm .= ".pm";
  0         0  
  0         0  
129 0         0 require $mod_pm;
130 0         0 my $fref = \&{"$mod\::gen_completion"};
  0         0  
131 0         0 log_trace("[compsah] invoking %s\::gen_completion(%s) ...", $mod, $xcargs);
132 0         0 $comp = $fref->(%$xcargs);
133             } else {
134 0         0 log_trace("[compsah] module %s is not installed, skipped", $mod);
135             }
136             }
137 0 0       0 if ($comp) {
138             my %cargs = (
139 0   0     0 %{$args{extras} // {}},
140             word=>$word, arg=>$args{arg}, args=>$args{args},
141 0         0 );
142 0         0 log_trace("[compsah] using arg completion routine from schema's 'x.completion' attribute with args (%s)", \%cargs);
143 0         0 $fres = $comp->(%cargs);
144 0         0 return; # from eval
145             }
146             }
147              
148 41 100 100     108 if ($clset->{is} && !ref($clset->{is})) {
149 1         3 log_trace("[compsah] adding completion from schema's 'is' clause");
150 1         4 push @$words, $clset->{is};
151 1         2 push @$summaries, undef;
152 1         2 $static++;
153 1         2 return; # from eval. there should not be any other value
154             }
155 40 100       84 if ($clset->{in}) {
156 3         9 log_trace("[compsah] adding completion from schema's 'in' clause");
157 3         9 for my $i (0..$#{ $clset->{in} }) {
  3         13  
158 4 50       11 next if ref $clset->{in}[$i];
159 4         9 push @$words , $clset->{in}[$i];
160 4 50       15 push @$summaries, $clset->{'x.in.summaries'} ? $clset->{'x.in.summaries'}[$i] : undef;
161             }
162 3         5 $static++;
163 3         5 return; # from eval. there should not be any other value
164             }
165 37 100       70 if ($clset->{'examples'}) {
166 3         8 log_trace("[compsah] adding completion from schema's 'examples' clause");
167 3         8 for my $eg (@{ $clset->{'examples'} }) {
  3         9  
168 7 100       15 if (ref $eg eq 'HASH') {
169 6 100 100     26 next unless !exists($eg->{valid}) || $eg->{valid};
170 3 100       11 next unless defined $eg->{value};
171 1 50       3 next if ref $eg->{value};
172 1         2 push @$words, $eg->{value};
173 1         3 push @$summaries, $eg->{summary};
174             } else {
175 1 50       4 next unless defined $eg;
176 1 50       3 next if ref $eg;
177 1         2 push @$words, $eg;
178 1         2 push @$summaries, undef;
179             }
180             }
181             #$static++;
182             #return; # from eval. there should not be any other value
183             }
184 37 100       86 if ($type eq 'any') {
185             # because currently Data::Sah::Normalize doesn't recursively
186             # normalize schemas in 'of' clauses, etc.
187 2         10 require Data::Sah::Normalize;
188 2 50 33     9 if ($clset->{of} && @{ $clset->{of} }) {
  2         10  
189              
190             $fres = combine_answers(
191 4         14 grep { defined } map {
192 4         48 complete_from_schema(schema=>$_, word => $word)
193 2         3 } @{ $clset->{of} }
  2         6  
194             );
195 2         371 goto RETURN_RES; # directly return result
196             }
197             }
198 35 50       65 if ($type eq 'bool') {
199 0         0 log_trace("[compsah] adding completion from possible values of bool");
200 0         0 push @$words, 0, 1;
201 0         0 push @$summaries, undef, undef;
202 0         0 $static++;
203 0         0 return; # from eval
204             }
205 35 100       71 if ($type eq 'int') {
206 21         34 my $limit = 100;
207 21 100 66     314 if ($clset->{between} &&
    100 66        
    100 100        
    100 100        
    100 100        
    100 66        
    100 100        
      66        
      66        
      66        
      100        
208             $clset->{between}[0] - $clset->{between}[0] <= $limit) {
209 1         5 log_trace("[compsah] adding completion from schema's 'between' clause");
210 1         6 for ($clset->{between}[0] .. $clset->{between}[1]) {
211 13         17 push @$words, $_;
212 13         26 push @$summaries, undef;
213             }
214 1         2 $static++;
215             } elsif ($clset->{xbetween} &&
216             $clset->{xbetween}[0] - $clset->{xbetween}[0] <= $limit) {
217 1         5 log_trace("[compsah] adding completion from schema's 'xbetween' clause");
218 1         7 for ($clset->{xbetween}[0]+1 .. $clset->{xbetween}[1]-1) {
219 11         16 push @$words, $_;
220 11         17 push @$summaries, undef;
221             }
222 1         3 $static++;
223             } elsif (defined($clset->{min}) && defined($clset->{max}) &&
224             $clset->{max}-$clset->{min} <= $limit) {
225 2         8 log_trace("[compsah] adding completion from schema's 'min' & 'max' clauses");
226 2         12 for ($clset->{min} .. $clset->{max}) {
227 20         32 push @$words, $_;
228 20         27 push @$summaries, undef;
229             }
230 2         17 $static++;
231             } elsif (defined($clset->{min}) && defined($clset->{xmax}) &&
232             $clset->{xmax}-$clset->{min} <= $limit) {
233 1         4 log_trace("[compsah] adding completion from schema's 'min' & 'xmax' clauses");
234 1         6 for ($clset->{min} .. $clset->{xmax}-1) {
235 12         16 push @$words, $_;
236 12         23 push @$summaries, undef;
237             }
238 1         3 $static++;
239             } elsif (defined($clset->{xmin}) && defined($clset->{max}) &&
240             $clset->{max}-$clset->{xmin} <= $limit) {
241 1         5 log_trace("[compsah] adding completion from schema's 'xmin' & 'max' clauses");
242 1         7 for ($clset->{xmin}+1 .. $clset->{max}) {
243 12         19 push @$words, $_;
244 12         19 push @$summaries, undef;
245             }
246 1         7 $static++;
247             } elsif (defined($clset->{xmin}) && defined($clset->{xmax}) &&
248             $clset->{xmax}-$clset->{xmin} <= $limit) {
249 1         4 log_trace("[compsah] adding completion from schema's 'xmin' & 'xmax' clauses");
250 1         6 for ($clset->{xmin}+1 .. $clset->{xmax}-1) {
251 11         16 push @$words, $_;
252 11         17 push @$summaries, undef;
253             }
254 1         2 $static++;
255             } elsif (length($word) && $word !~ /\A-?\d*\z/) {
256 1         4 log_trace("[compsah] word not an int");
257 1         4 $words = [];
258 1         2 $summaries = [];
259             } else {
260             # do a digit by digit completion
261 13         22 $words = [];
262 13         20 $summaries = [];
263 13         22 for my $sign ("", "-") {
264 26         48 for ("", 0..9) {
265 286         524 my $i = $sign . $word . $_;
266 286 100       439 next unless length $i;
267 282 100       700 next unless $i =~ /\A-?\d+\z/;
268 245 100       399 next if $i eq '-0';
269 240 100       433 next if $i =~ /\A-?0\d/;
270             next if $clset->{between} &&
271             ($i < $clset->{between}[0] ||
272 220 0 0     364 $i > $clset->{between}[1]);
      33        
273             next if $clset->{xbetween} &&
274             ($i <= $clset->{xbetween}[0] ||
275 220 0 0     326 $i >= $clset->{xbetween}[1]);
      33        
276 220 100 100     550 next if defined($clset->{min} ) && $i < $clset->{min};
277 156 50 33     324 next if defined($clset->{xmin}) && $i <= $clset->{xmin};
278 156 100 100     336 next if defined($clset->{max} ) && $i > $clset->{max};
279 136 50 33     244 next if defined($clset->{xmin}) && $i >= $clset->{xmax};
280 136         220 push @$words, $i;
281 136         247 push @$summaries, undef;
282             }
283             }
284             }
285 21         81 return; # from eval
286             }
287 14 100       35 if ($type eq 'float') {
288 11 100 100     71 if (length($word) && $word !~ /\A-?\d*(\.\d*)?\z/) {
289 1         15 log_trace("[compsah] word not a float");
290 1         4 $words = [];
291 1         4 $summaries = [];
292             } else {
293 10         15 $words = [];
294 10         20 $summaries = [];
295 10         14 for my $sig ("", "-") {
296 20         35 for ("", 0..9,
297             ".0",".1",".2",".3",".4",".5",".6",".7",".8",".9") {
298 420         729 my $f = $sig . $word . $_;
299 420 100       645 next unless length $f;
300 418 100       1119 next unless $f =~ /\A-?\d+(\.\d+)?\z/;
301 291 100       499 next if $f eq '-0';
302 286 100       580 next if $f =~ /\A-?0\d\z/;
303             next if $clset->{between} &&
304             ($f < $clset->{between}[0] ||
305 236 0 0     391 $f > $clset->{between}[1]);
      33        
306             next if $clset->{xbetween} &&
307             ($f <= $clset->{xbetween}[0] ||
308 236 0 0     397 $f >= $clset->{xbetween}[1]);
      33        
309 236 100 100     561 next if defined($clset->{min} ) && $f < $clset->{min};
310 189 50 33     325 next if defined($clset->{xmin}) && $f <= $clset->{xmin};
311 189 100 100     353 next if defined($clset->{max} ) && $f > $clset->{max};
312 186 50 33     309 next if defined($clset->{xmin}) && $f >= $clset->{xmax};
313 186         316 push @$words, $f;
314 186         327 push @$summaries, undef;
315             }
316             }
317 546         737 my @orders = sort { $words->[$a] cmp $words->[$b] }
318 10         16 0..$#{$words};
  10         75  
319 10         27 my $words = [map {$words->[$_] } @orders];
  186         264  
320 10         63 my $summaries = [map {$summaries->[$_]} @orders];
  186         312  
321             }
322 11         40 return; # from eval
323             }
324             }; # eval
325 39 50       77 log_trace("[compsah] complete_from_schema died: %s", $@) if $@;
326              
327 39         50 my $replace_map;
328             GET_REPLACE_MAP:
329             {
330 39 50       55 last unless $clset->{prefilters};
  39         87  
331             # TODO: make replace_map in Complete::Util equivalent as
332             # Str::replace_map's map.
333 0         0 for my $entry (@{ $clset->{prefilters} }) {
  0         0  
334 0 0       0 next unless ref $entry eq 'ARRAY';
335 0 0       0 next unless $entry->[0] eq 'Str::replace_map';
336 0         0 $replace_map = {};
337 0         0 for my $k (keys %{ $entry->[1]{map} }) {
  0         0  
338 0         0 my $v = $entry->[1]{map}{$k};
339 0         0 $replace_map->{$v} = [$k];
340             }
341 0         0 last;
342             }
343             }
344              
345 39 100       83 goto RETURN_RES unless $words;
346 37 100 66     132 $fres = hashify_answer(
347             complete_array_elem(
348             array=>$words,
349             summaries=>$summaries,
350             word=>$word,
351             (replace_map => $replace_map) x !!$replace_map,
352             ),
353             {static=>$static && $word eq '' ? 1:0},
354             );
355              
356 42         9525 RETURN_RES:
357             log_trace("[compsah] leaving complete_from_schema, result=%s", $fres);
358 42         584 $fres;
359             }
360              
361             1;
362             # ABSTRACT: Sah-related completion routines
363              
364             __END__
365              
366             =pod
367              
368             =encoding UTF-8
369              
370             =head1 NAME
371              
372             Complete::Sah - Sah-related completion routines
373              
374             =head1 VERSION
375              
376             This document describes version 0.011 of Complete::Sah (from Perl distribution Complete-Sah), released on 2023-01-28.
377              
378             =head1 SYNOPSIS
379              
380             use Complete::Sah qw(complete_from_schema);
381             my $res = complete_from_schema(word => 'a', schema=>[str => {in=>[qw/apple apricot banana/]}]);
382             # -> {words=>['apple', 'apricot'], static=>0}
383              
384             =head1 FUNCTIONS
385              
386              
387             =head2 complete_from_schema
388              
389             Usage:
390              
391             complete_from_schema(%args) -> any
392              
393             Complete a value from schema.
394              
395             Employ some heuristics to complete a value from Sah schema. For example, if
396             schema is C<< [str =E<gt> in =E<gt> [qw/new open resolved rejected/]] >>, then we can
397             complete from the C<in> clause. Or for something like C<< [int =E<gt> between =E<gt> [1,
398             20]] >> we can complete using values from 1 to 20.
399              
400             Tip: If you want to give summary for each entry in C<in> clause, you can use the
401             C<x.in.summaries> attribute, example:
402              
403             # schema
404             ['str', {
405             in => ['b', 'g'],
406             'x.in.summaries' => ['Male/boy', 'Female/girl'],
407             }]
408              
409             This function is not exported by default, but exportable.
410              
411             Arguments ('*' denotes required arguments):
412              
413             =over 4
414              
415             =item * B<schema>* => I<str|array>
416              
417             Will be normalized, unless when C<schema_is_normalized> is set to true, in which
418             case schema must already be normalized.
419              
420             =item * B<schema_is_normalized> => I<bool> (default: 0)
421              
422             (No description)
423              
424             =item * B<word>* => I<str> (default: "")
425              
426             (No description)
427              
428              
429             =back
430              
431             Return value: (any)
432              
433             =head1 HOMEPAGE
434              
435             Please visit the project's homepage at L<https://metacpan.org/release/Complete-Sah>.
436              
437             =head1 SOURCE
438              
439             Source repository is at L<https://github.com/perlancar/perl-Complete-Sah>.
440              
441             =head1 AUTHOR
442              
443             perlancar <perlancar@cpan.org>
444              
445             =head1 CONTRIBUTING
446              
447              
448             To contribute, you can send patches by email/via RT, or send pull requests on
449             GitHub.
450              
451             Most of the time, you don't need to build the distribution yourself. You can
452             simply modify the code, then test via:
453              
454             % prove -l
455              
456             If you want to build the distribution (e.g. to try to install it locally on your
457             system), you can install L<Dist::Zilla>,
458             L<Dist::Zilla::PluginBundle::Author::PERLANCAR>,
459             L<Pod::Weaver::PluginBundle::Author::PERLANCAR>, and sometimes one or two other
460             Dist::Zilla- and/or Pod::Weaver plugins. Any additional steps required beyond
461             that are considered a bug and can be reported to me.
462              
463             =head1 COPYRIGHT AND LICENSE
464              
465             This software is copyright (c) 2023, 2020, 2019 by perlancar <perlancar@cpan.org>.
466              
467             This is free software; you can redistribute it and/or modify it under
468             the same terms as the Perl 5 programming language system itself.
469              
470             =head1 BUGS
471              
472             Please report any bugs or feature requests on the bugtracker website L<https://rt.cpan.org/Public/Dist/Display.html?Name=Complete-Sah>
473              
474             When submitting a bug or request, please include a test-file or a
475             patch to an existing test-file that illustrates the bug or desired
476             feature.
477              
478             =cut